diff --git a/compiler/package.json b/compiler/package.json index 3adb2d279f159..2d898b1305b9f 100644 --- a/compiler/package.json +++ b/compiler/package.json @@ -43,7 +43,7 @@ "prettier": "^3.3.3", "prettier-plugin-hermes-parser": "^0.23.0", "prompt-promise": "^1.0.3", - "rollup": "^4.13.2", + "rollup": "^4.22.4", "rollup-plugin-banner2": "^1.2.3", "rollup-plugin-prettier": "^4.1.1", "typescript": "^5.4.3", diff --git a/compiler/yarn.lock b/compiler/yarn.lock index 002e43a408bfb..f599318e2e4bc 100644 --- a/compiler/yarn.lock +++ b/compiler/yarn.lock @@ -2762,80 +2762,85 @@ estree-walker "^2.0.2" picomatch "^2.3.1" -"@rollup/rollup-android-arm-eabi@4.13.2": - version "4.13.2" - resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.13.2.tgz#fbf098f49d96a8cac9056f22f5fd80906ef3af85" - integrity sha512-3XFIDKWMFZrMnao1mJhnOT1h2g0169Os848NhhmGweEcfJ4rCi+3yMCOLG4zA61rbJdkcrM/DjVZm9Hg5p5w7g== - -"@rollup/rollup-android-arm64@4.13.2": - version "4.13.2" - resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.13.2.tgz#0d2448251040fce19a98eee505dff5b3c8ec9b98" - integrity sha512-GdxxXbAuM7Y/YQM9/TwwP+L0omeE/lJAR1J+olu36c3LqqZEBdsIWeQ91KBe6nxwOnb06Xh7JS2U5ooWU5/LgQ== - -"@rollup/rollup-darwin-arm64@4.13.2": - version "4.13.2" - resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.13.2.tgz#78db4d4da5b1b84c22adbe25c8a4961b3f22d3af" - integrity sha512-mCMlpzlBgOTdaFs83I4XRr8wNPveJiJX1RLfv4hggyIVhfB5mJfN4P8Z6yKh+oE4Luz+qq1P3kVdWrCKcMYrrA== - -"@rollup/rollup-darwin-x64@4.13.2": - version "4.13.2" - resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.13.2.tgz#fcc05af54379f8ee5c7e954987d4514c6fd0fb42" - integrity sha512-yUoEvnH0FBef/NbB1u6d3HNGyruAKnN74LrPAfDQL3O32e3k3OSfLrPgSJmgb3PJrBZWfPyt6m4ZhAFa2nZp2A== - -"@rollup/rollup-linux-arm-gnueabihf@4.13.2": - version "4.13.2" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.13.2.tgz#2ce200efa1ef4a56ee2af7b453edc74a259d7d31" - integrity sha512-GYbLs5ErswU/Xs7aGXqzc3RrdEjKdmoCrgzhJWyFL0r5fL3qd1NPcDKDowDnmcoSiGJeU68/Vy+OMUluRxPiLQ== - -"@rollup/rollup-linux-arm64-gnu@4.13.2": - version "4.13.2" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.13.2.tgz#5a24aac882bff9abfda3f45f6f1db2166c342a4a" - integrity sha512-L1+D8/wqGnKQIlh4Zre9i4R4b4noxzH5DDciyahX4oOz62CphY7WDWqJoQ66zNR4oScLNOqQJfNSIAe/6TPUmQ== - -"@rollup/rollup-linux-arm64-musl@4.13.2": - version "4.13.2" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.13.2.tgz#f1fb4c6f961d3f3397231a99e621d199200e4ea9" - integrity sha512-tK5eoKFkXdz6vjfkSTCupUzCo40xueTOiOO6PeEIadlNBkadH1wNOH8ILCPIl8by/Gmb5AGAeQOFeLev7iZDOA== - -"@rollup/rollup-linux-powerpc64le-gnu@4.13.2": - version "4.13.2" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.13.2.tgz#46b2463d94ac3af3e0f7a2947b695397bc13b755" - integrity sha512-zvXvAUGGEYi6tYhcDmb9wlOckVbuD+7z3mzInCSTACJ4DQrdSLPNUeDIcAQW39M3q6PDquqLWu7pnO39uSMRzQ== - -"@rollup/rollup-linux-riscv64-gnu@4.13.2": - version "4.13.2" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.13.2.tgz#47b932ee59a5395a3a341b0493e361d9e6032cf2" - integrity sha512-C3GSKvMtdudHCN5HdmAMSRYR2kkhgdOfye4w0xzyii7lebVr4riCgmM6lRiSCnJn2w1Xz7ZZzHKuLrjx5620kw== - -"@rollup/rollup-linux-s390x-gnu@4.13.2": - version "4.13.2" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.13.2.tgz#8e14a1b3c3b9a4440c70a9c1ba12d32aa21f9712" - integrity sha512-l4U0KDFwzD36j7HdfJ5/TveEQ1fUTjFFQP5qIt9gBqBgu1G8/kCaq5Ok05kd5TG9F8Lltf3MoYsUMw3rNlJ0Yg== - -"@rollup/rollup-linux-x64-gnu@4.13.2": - version "4.13.2" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.13.2.tgz#270e939194b66df77bcb33dd9a5ddf7784bd7997" - integrity sha512-xXMLUAMzrtsvh3cZ448vbXqlUa7ZL8z0MwHp63K2IIID2+DeP5iWIT6g1SN7hg1VxPzqx0xZdiDM9l4n9LRU1A== - -"@rollup/rollup-linux-x64-musl@4.13.2": - version "4.13.2" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.13.2.tgz#e8dd0f3c2046acbda2934490b36552e856a3bc6a" - integrity sha512-M/JYAWickafUijWPai4ehrjzVPKRCyDb1SLuO+ZyPfoXgeCEAlgPkNXewFZx0zcnoIe3ay4UjXIMdXQXOZXWqA== - -"@rollup/rollup-win32-arm64-msvc@4.13.2": - version "4.13.2" - resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.13.2.tgz#f8b65a4a7e7a6b383e7b14439129b2f474ff123c" - integrity sha512-2YWwoVg9KRkIKaXSh0mz3NmfurpmYoBBTAXA9qt7VXk0Xy12PoOP40EFuau+ajgALbbhi4uTj3tSG3tVseCjuA== - -"@rollup/rollup-win32-ia32-msvc@4.13.2": - version "4.13.2" - resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.13.2.tgz#bc1c5a4fbc4337d6cb15da80a4de95fd53ab3573" - integrity sha512-2FSsE9aQ6OWD20E498NYKEQLneShWes0NGMPQwxWOdws35qQXH+FplabOSP5zEe1pVjurSDOGEVCE2agFwSEsw== - -"@rollup/rollup-win32-x64-msvc@4.13.2": - version "4.13.2" - resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.13.2.tgz#851959c4c1c3c6647aba1f388198c8243aed6917" - integrity sha512-7h7J2nokcdPePdKykd8wtc8QqqkqxIrUz7MHj6aNr8waBRU//NLDVnNjQnqQO6fqtjrtCdftpbTuOKAyrAQETQ== +"@rollup/rollup-android-arm-eabi@4.22.4": + version "4.22.4" + resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.22.4.tgz#8b613b9725e8f9479d142970b106b6ae878610d5" + integrity sha512-Fxamp4aEZnfPOcGA8KSNEohV8hX7zVHOemC8jVBoBUHu5zpJK/Eu3uJwt6BMgy9fkvzxDaurgj96F/NiLukF2w== + +"@rollup/rollup-android-arm64@4.22.4": + version "4.22.4" + resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.22.4.tgz#654ca1049189132ff602bfcf8df14c18da1f15fb" + integrity sha512-VXoK5UMrgECLYaMuGuVTOx5kcuap1Jm8g/M83RnCHBKOqvPPmROFJGQaZhGccnsFtfXQ3XYa4/jMCJvZnbJBdA== + +"@rollup/rollup-darwin-arm64@4.22.4": + version "4.22.4" + resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.22.4.tgz#6d241d099d1518ef0c2205d96b3fa52e0fe1954b" + integrity sha512-xMM9ORBqu81jyMKCDP+SZDhnX2QEVQzTcC6G18KlTQEzWK8r/oNZtKuZaCcHhnsa6fEeOBionoyl5JsAbE/36Q== + +"@rollup/rollup-darwin-x64@4.22.4": + version "4.22.4" + resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.22.4.tgz#42bd19d292a57ee11734c980c4650de26b457791" + integrity sha512-aJJyYKQwbHuhTUrjWjxEvGnNNBCnmpHDvrb8JFDbeSH3m2XdHcxDd3jthAzvmoI8w/kSjd2y0udT+4okADsZIw== + +"@rollup/rollup-linux-arm-gnueabihf@4.22.4": + version "4.22.4" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.22.4.tgz#f23555ee3d8fe941c5c5fd458cd22b65eb1c2232" + integrity sha512-j63YtCIRAzbO+gC2L9dWXRh5BFetsv0j0va0Wi9epXDgU/XUi5dJKo4USTttVyK7fGw2nPWK0PbAvyliz50SCQ== + +"@rollup/rollup-linux-arm-musleabihf@4.22.4": + version "4.22.4" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.22.4.tgz#f3bbd1ae2420f5539d40ac1fde2b38da67779baa" + integrity sha512-dJnWUgwWBX1YBRsuKKMOlXCzh2Wu1mlHzv20TpqEsfdZLb3WoJW2kIEsGwLkroYf24IrPAvOT/ZQ2OYMV6vlrg== + +"@rollup/rollup-linux-arm64-gnu@4.22.4": + version "4.22.4" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.22.4.tgz#7abe900120113e08a1f90afb84c7c28774054d15" + integrity sha512-AdPRoNi3NKVLolCN/Sp4F4N1d98c4SBnHMKoLuiG6RXgoZ4sllseuGioszumnPGmPM2O7qaAX/IJdeDU8f26Aw== + +"@rollup/rollup-linux-arm64-musl@4.22.4": + version "4.22.4" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.22.4.tgz#9e655285c8175cd44f57d6a1e8e5dedfbba1d820" + integrity sha512-Gl0AxBtDg8uoAn5CCqQDMqAx22Wx22pjDOjBdmG0VIWX3qUBHzYmOKh8KXHL4UpogfJ14G4wk16EQogF+v8hmA== + +"@rollup/rollup-linux-powerpc64le-gnu@4.22.4": + version "4.22.4" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.22.4.tgz#9a79ae6c9e9d8fe83d49e2712ecf4302db5bef5e" + integrity sha512-3aVCK9xfWW1oGQpTsYJJPF6bfpWfhbRnhdlyhak2ZiyFLDaayz0EP5j9V1RVLAAxlmWKTDfS9wyRyY3hvhPoOg== + +"@rollup/rollup-linux-riscv64-gnu@4.22.4": + version "4.22.4" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.22.4.tgz#67ac70eca4ace8e2942fabca95164e8874ab8128" + integrity sha512-ePYIir6VYnhgv2C5Xe9u+ico4t8sZWXschR6fMgoPUK31yQu7hTEJb7bCqivHECwIClJfKgE7zYsh1qTP3WHUA== + +"@rollup/rollup-linux-s390x-gnu@4.22.4": + version "4.22.4" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.22.4.tgz#9f883a7440f51a22ed7f99e1d070bd84ea5005fc" + integrity sha512-GqFJ9wLlbB9daxhVlrTe61vJtEY99/xB3C8e4ULVsVfflcpmR6c8UZXjtkMA6FhNONhj2eA5Tk9uAVw5orEs4Q== + +"@rollup/rollup-linux-x64-gnu@4.22.4": + version "4.22.4" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.22.4.tgz#70116ae6c577fe367f58559e2cffb5641a1dd9d0" + integrity sha512-87v0ol2sH9GE3cLQLNEy0K/R0pz1nvg76o8M5nhMR0+Q+BBGLnb35P0fVz4CQxHYXaAOhE8HhlkaZfsdUOlHwg== + +"@rollup/rollup-linux-x64-musl@4.22.4": + version "4.22.4" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.22.4.tgz#f473f88219feb07b0b98b53a7923be716d1d182f" + integrity sha512-UV6FZMUgePDZrFjrNGIWzDo/vABebuXBhJEqrHxrGiU6HikPy0Z3LfdtciIttEUQfuDdCn8fqh7wiFJjCNwO+g== + +"@rollup/rollup-win32-arm64-msvc@4.22.4": + version "4.22.4" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.22.4.tgz#4349482d17f5d1c58604d1c8900540d676f420e0" + integrity sha512-BjI+NVVEGAXjGWYHz/vv0pBqfGoUH0IGZ0cICTn7kB9PyjrATSkX+8WkguNjWoj2qSr1im/+tTGRaY+4/PdcQw== + +"@rollup/rollup-win32-ia32-msvc@4.22.4": + version "4.22.4" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.22.4.tgz#a6fc39a15db618040ec3c2a24c1e26cb5f4d7422" + integrity sha512-SiWG/1TuUdPvYmzmYnmd3IEifzR61Tragkbx9D3+R8mzQqDBz8v+BvZNDlkiTtI9T15KYZhP0ehn3Dld4n9J5g== + +"@rollup/rollup-win32-x64-msvc@4.22.4": + version "4.22.4" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.22.4.tgz#3dd5d53e900df2a40841882c02e56f866c04d202" + integrity sha512-j8pPKp53/lq9lMXN57S8cFz0MynJk8OWNuUnXct/9KCpKU7DgU3bYMJhwWmcqC0UU29p8Lr0/7KEVcaM6bf47Q== "@rushstack/eslint-patch@^1.3.3": version "1.5.1" @@ -3013,12 +3018,7 @@ "@types/estree" "*" "@types/json-schema" "*" -"@types/estree@*", "@types/estree@^1.0.0": - version "1.0.1" - resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.1.tgz#aa22750962f3bf0e79d753d3cc067f010c95f194" - integrity sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA== - -"@types/estree@1.0.5": +"@types/estree@*", "@types/estree@1.0.5", "@types/estree@^1.0.0": version "1.0.5" resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.5.tgz#a6ce3e556e00fd9895dd872dd172ad0d4bd687f4" integrity sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw== @@ -8896,28 +8896,29 @@ rollup-plugin-prettier@^4.1.1: lodash.omitby "4.6.0" magic-string "0.30.5" -rollup@^4.13.2: - version "4.13.2" - resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.13.2.tgz#ac57d2dc48e8f5562f5a6daadb9caee590069262" - integrity sha512-MIlLgsdMprDBXC+4hsPgzWUasLO9CE4zOkj/u6j+Z6j5A4zRY+CtiXAdJyPtgCsc42g658Aeh1DlrdVEJhsL2g== +rollup@^4.22.4: + version "4.22.4" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.22.4.tgz#4135a6446671cd2a2453e1ad42a45d5973ec3a0f" + integrity sha512-vD8HJ5raRcWOyymsR6Z3o6+RzfEPCnVLMFJ6vRslO1jt4LO6dUo5Qnpg7y4RkZFM2DMe3WUirkI5c16onjrc6A== dependencies: "@types/estree" "1.0.5" optionalDependencies: - "@rollup/rollup-android-arm-eabi" "4.13.2" - "@rollup/rollup-android-arm64" "4.13.2" - "@rollup/rollup-darwin-arm64" "4.13.2" - "@rollup/rollup-darwin-x64" "4.13.2" - "@rollup/rollup-linux-arm-gnueabihf" "4.13.2" - "@rollup/rollup-linux-arm64-gnu" "4.13.2" - "@rollup/rollup-linux-arm64-musl" "4.13.2" - "@rollup/rollup-linux-powerpc64le-gnu" "4.13.2" - "@rollup/rollup-linux-riscv64-gnu" "4.13.2" - "@rollup/rollup-linux-s390x-gnu" "4.13.2" - "@rollup/rollup-linux-x64-gnu" "4.13.2" - "@rollup/rollup-linux-x64-musl" "4.13.2" - "@rollup/rollup-win32-arm64-msvc" "4.13.2" - "@rollup/rollup-win32-ia32-msvc" "4.13.2" - "@rollup/rollup-win32-x64-msvc" "4.13.2" + "@rollup/rollup-android-arm-eabi" "4.22.4" + "@rollup/rollup-android-arm64" "4.22.4" + "@rollup/rollup-darwin-arm64" "4.22.4" + "@rollup/rollup-darwin-x64" "4.22.4" + "@rollup/rollup-linux-arm-gnueabihf" "4.22.4" + "@rollup/rollup-linux-arm-musleabihf" "4.22.4" + "@rollup/rollup-linux-arm64-gnu" "4.22.4" + "@rollup/rollup-linux-arm64-musl" "4.22.4" + "@rollup/rollup-linux-powerpc64le-gnu" "4.22.4" + "@rollup/rollup-linux-riscv64-gnu" "4.22.4" + "@rollup/rollup-linux-s390x-gnu" "4.22.4" + "@rollup/rollup-linux-x64-gnu" "4.22.4" + "@rollup/rollup-linux-x64-musl" "4.22.4" + "@rollup/rollup-win32-arm64-msvc" "4.22.4" + "@rollup/rollup-win32-ia32-msvc" "4.22.4" + "@rollup/rollup-win32-x64-msvc" "4.22.4" fsevents "~2.3.2" rrweb-cssom@^0.6.0: diff --git a/packages/react-devtools-core/package.json b/packages/react-devtools-core/package.json index 4c7f54775f6ba..23167fa377847 100644 --- a/packages/react-devtools-core/package.json +++ b/packages/react-devtools-core/package.json @@ -1,6 +1,6 @@ { "name": "react-devtools-core", - "version": "5.3.1", + "version": "6.0.0", "description": "Use react-devtools outside of the browser", "license": "MIT", "main": "./dist/backend.js", diff --git a/packages/react-devtools-core/src/backend.js b/packages/react-devtools-core/src/backend.js index 1f2055832a3dd..dfc3db15138c6 100644 --- a/packages/react-devtools-core/src/backend.js +++ b/packages/react-devtools-core/src/backend.js @@ -13,7 +13,10 @@ import {installHook} from 'react-devtools-shared/src/hook'; import {initBackend} from 'react-devtools-shared/src/backend'; import {__DEBUG__} from 'react-devtools-shared/src/constants'; import setupNativeStyleEditor from 'react-devtools-shared/src/backend/NativeStyleEditor/setupNativeStyleEditor'; -import {getDefaultComponentFilters} from 'react-devtools-shared/src/utils'; +import { + getDefaultComponentFilters, + getIsReloadAndProfileSupported, +} from 'react-devtools-shared/src/utils'; import type {BackendBridge} from 'react-devtools-shared/src/bridge'; import type { @@ -36,6 +39,7 @@ type ConnectOptions = { isAppActive?: () => boolean, websocket?: ?WebSocket, onSettingsUpdated?: (settings: $ReadOnly) => void, + isReloadAndProfileSupported?: boolean, }; let savedComponentFilters: Array = @@ -77,6 +81,7 @@ export function connectToDevTools(options: ?ConnectOptions) { retryConnectionDelay = 2000, isAppActive = () => true, onSettingsUpdated, + isReloadAndProfileSupported = getIsReloadAndProfileSupported(), } = options || {}; const protocol = useHttps ? 'wss' : 'ws'; @@ -184,7 +189,7 @@ export function connectToDevTools(options: ?ConnectOptions) { hook.emit('shutdown'); }); - initBackend(hook, agent, window); + initBackend(hook, agent, window, isReloadAndProfileSupported); // Setup React Native style editor if the environment supports it. if (resolveRNStyle != null || hook.resolveRNStyle != null) { @@ -309,6 +314,7 @@ type ConnectWithCustomMessagingOptions = { nativeStyleEditorValidAttributes?: $ReadOnlyArray, resolveRNStyle?: ResolveNativeStyle, onSettingsUpdated?: (settings: $ReadOnly) => void, + isReloadAndProfileSupported?: boolean, }; export function connectWithCustomMessagingProtocol({ @@ -318,6 +324,7 @@ export function connectWithCustomMessagingProtocol({ nativeStyleEditorValidAttributes, resolveRNStyle, onSettingsUpdated, + isReloadAndProfileSupported = getIsReloadAndProfileSupported(), }: ConnectWithCustomMessagingOptions): Function { const hook: ?DevToolsHook = window.__REACT_DEVTOOLS_GLOBAL_HOOK__; if (hook == null) { @@ -368,7 +375,12 @@ export function connectWithCustomMessagingProtocol({ hook.emit('shutdown'); }); - const unsubscribeBackend = initBackend(hook, agent, window); + const unsubscribeBackend = initBackend( + hook, + agent, + window, + isReloadAndProfileSupported, + ); const nativeStyleResolver: ResolveNativeStyle | void = resolveRNStyle || hook.resolveRNStyle; diff --git a/packages/react-devtools-extensions/chrome/manifest.json b/packages/react-devtools-extensions/chrome/manifest.json index 1ab43194f2620..f0209aaa73eed 100644 --- a/packages/react-devtools-extensions/chrome/manifest.json +++ b/packages/react-devtools-extensions/chrome/manifest.json @@ -2,8 +2,8 @@ "manifest_version": 3, "name": "React Developer Tools", "description": "Adds React debugging tools to the Chrome Developer Tools.", - "version": "5.3.1", - "version_name": "5.3.1", + "version": "6.0.0", + "version_name": "6.0.0", "minimum_chrome_version": "102", "icons": { "16": "icons/16-production.png", diff --git a/packages/react-devtools-extensions/edge/manifest.json b/packages/react-devtools-extensions/edge/manifest.json index bd03dea08efb3..ca4d56b4453e9 100644 --- a/packages/react-devtools-extensions/edge/manifest.json +++ b/packages/react-devtools-extensions/edge/manifest.json @@ -2,8 +2,8 @@ "manifest_version": 3, "name": "React Developer Tools", "description": "Adds React debugging tools to the Microsoft Edge Developer Tools.", - "version": "5.3.1", - "version_name": "5.3.1", + "version": "6.0.0", + "version_name": "6.0.0", "minimum_chrome_version": "102", "icons": { "16": "icons/16-production.png", diff --git a/packages/react-devtools-extensions/firefox/manifest.json b/packages/react-devtools-extensions/firefox/manifest.json index 8a5a272fb4500..52f89104b4f80 100644 --- a/packages/react-devtools-extensions/firefox/manifest.json +++ b/packages/react-devtools-extensions/firefox/manifest.json @@ -2,7 +2,7 @@ "manifest_version": 3, "name": "React Developer Tools", "description": "Adds React debugging tools to the Firefox Developer Tools.", - "version": "5.3.1", + "version": "6.0.0", "browser_specific_settings": { "gecko": { "id": "@react-devtools", diff --git a/packages/react-devtools-extensions/src/contentScripts/backendManager.js b/packages/react-devtools-extensions/src/contentScripts/backendManager.js index 36a2ab19aa411..9d3ec414330ed 100644 --- a/packages/react-devtools-extensions/src/contentScripts/backendManager.js +++ b/packages/react-devtools-extensions/src/contentScripts/backendManager.js @@ -13,6 +13,7 @@ import type { } from 'react-devtools-shared/src/backend/types'; import {hasAssignedBackend} from 'react-devtools-shared/src/backend/utils'; import {COMPACT_VERSION_NAME} from 'react-devtools-extensions/src/utils'; +import {getIsReloadAndProfileSupported} from 'react-devtools-shared/src/utils'; let welcomeHasInitialized = false; @@ -140,7 +141,7 @@ function activateBackend(version: string, hook: DevToolsHook) { hook.emit('shutdown'); }); - initBackend(hook, agent, window); + initBackend(hook, agent, window, getIsReloadAndProfileSupported()); // Setup React Native style editor if a renderer like react-native-web has injected it. if (typeof setupNativeStyleEditor === 'function' && hook.resolveRNStyle) { diff --git a/packages/react-devtools-inline/package.json b/packages/react-devtools-inline/package.json index ead4c18380047..f0eadaf7e6d9e 100644 --- a/packages/react-devtools-inline/package.json +++ b/packages/react-devtools-inline/package.json @@ -1,6 +1,6 @@ { "name": "react-devtools-inline", - "version": "5.3.1", + "version": "6.0.0", "description": "Embed react-devtools within a website", "license": "MIT", "main": "./dist/backend.js", diff --git a/packages/react-devtools-inline/src/backend.js b/packages/react-devtools-inline/src/backend.js index fca1535c4e5ba..41af7809be0b1 100644 --- a/packages/react-devtools-inline/src/backend.js +++ b/packages/react-devtools-inline/src/backend.js @@ -8,6 +8,7 @@ import setupNativeStyleEditor from 'react-devtools-shared/src/backend/NativeStyl import type {BackendBridge} from 'react-devtools-shared/src/bridge'; import type {Wall} from 'react-devtools-shared/src/frontend/types'; +import {getIsReloadAndProfileSupported} from 'react-devtools-shared/src/utils'; function startActivation(contentWindow: any, bridge: BackendBridge) { const onSavedPreferences = (data: $FlowFixMe) => { @@ -66,7 +67,7 @@ function finishActivation(contentWindow: any, bridge: BackendBridge) { const hook = contentWindow.__REACT_DEVTOOLS_GLOBAL_HOOK__; if (hook) { - initBackend(hook, agent, contentWindow); + initBackend(hook, agent, contentWindow, getIsReloadAndProfileSupported()); // Setup React Native style editor if a renderer like react-native-web has injected it. if (hook.resolveRNStyle) { diff --git a/packages/react-devtools-shared/src/__tests__/store-test.js b/packages/react-devtools-shared/src/__tests__/store-test.js index 92e7fc6586111..93c7048b2bc5b 100644 --- a/packages/react-devtools-shared/src/__tests__/store-test.js +++ b/packages/react-devtools-shared/src/__tests__/store-test.js @@ -2148,8 +2148,8 @@ describe('Store', () => { act(() => render()); }); expect(store).toMatchInlineSnapshot(`[root]`); - expect(store.errorCount).toBe(0); - expect(store.warningCount).toBe(0); + expect(store.componentWithErrorCount).toBe(0); + expect(store.componentWithWarningCount).toBe(0); }); // Regression test for https://github.com/facebook/react/issues/23202 diff --git a/packages/react-devtools-shared/src/__tests__/storeComponentFilters-test.js b/packages/react-devtools-shared/src/__tests__/storeComponentFilters-test.js index 432fb1771b6ff..26cd51383297f 100644 --- a/packages/react-devtools-shared/src/__tests__/storeComponentFilters-test.js +++ b/packages/react-devtools-shared/src/__tests__/storeComponentFilters-test.js @@ -420,8 +420,8 @@ describe('Store component filters', () => { }); expect(store).toMatchInlineSnapshot(``); - expect(store.errorCount).toBe(0); - expect(store.warningCount).toBe(0); + expect(store.componentWithErrorCount).toBe(0); + expect(store.componentWithWarningCount).toBe(0); await actAsync(async () => (store.componentFilters = [])); expect(store).toMatchInlineSnapshot(` @@ -460,8 +460,8 @@ describe('Store component filters', () => { ]), ); expect(store).toMatchInlineSnapshot(`[root]`); - expect(store.errorCount).toBe(0); - expect(store.warningCount).toBe(0); + expect(store.componentWithErrorCount).toBe(0); + expect(store.componentWithWarningCount).toBe(0); await actAsync(async () => (store.componentFilters = [])); expect(store).toMatchInlineSnapshot(` @@ -510,8 +510,8 @@ describe('Store component filters', () => { }); expect(store).toMatchInlineSnapshot(``); - expect(store.errorCount).toBe(0); - expect(store.warningCount).toBe(0); + expect(store.componentWithErrorCount).toBe(0); + expect(store.componentWithWarningCount).toBe(0); await actAsync(async () => (store.componentFilters = [])); expect(store).toMatchInlineSnapshot(` @@ -550,8 +550,8 @@ describe('Store component filters', () => { ]), ); expect(store).toMatchInlineSnapshot(`[root]`); - expect(store.errorCount).toBe(0); - expect(store.warningCount).toBe(0); + expect(store.componentWithErrorCount).toBe(0); + expect(store.componentWithWarningCount).toBe(0); await actAsync(async () => (store.componentFilters = [])); expect(store).toMatchInlineSnapshot(` diff --git a/packages/react-devtools-shared/src/backend/agent.js b/packages/react-devtools-shared/src/backend/agent.js index e55e8a6d41e2b..a1e96bfcdeb39 100644 --- a/packages/react-devtools-shared/src/backend/agent.js +++ b/packages/react-devtools-shared/src/backend/agent.js @@ -38,7 +38,7 @@ import type { DevToolsHookSettings, } from './types'; import type {ComponentFilter} from 'react-devtools-shared/src/frontend/types'; -import {isSynchronousXHRSupported, isReactNativeEnvironment} from './utils'; +import {isReactNativeEnvironment} from './utils'; const debug = (methodName: string, ...args: Array) => { if (__DEBUG__) { @@ -242,16 +242,6 @@ export default class Agent extends EventEmitter<{ if (this._isProfiling) { bridge.send('profilingStatus', true); } - - // Notify the frontend if the backend supports the Storage API (e.g. localStorage). - // If not, features like reload-and-profile will not work correctly and must be disabled. - let isBackendStorageAPISupported = false; - try { - localStorage.getItem('test'); - isBackendStorageAPISupported = true; - } catch (error) {} - bridge.send('isBackendStorageAPISupported', isBackendStorageAPISupported); - bridge.send('isSynchronousXHRSupported', isSynchronousXHRSupported()); } get rendererInterfaces(): {[key: RendererID]: RendererInterface, ...} { @@ -675,6 +665,10 @@ export default class Agent extends EventEmitter<{ } }; + onReloadAndProfileSupportedByHost: () => void = () => { + this._bridge.send('isReloadAndProfileSupportedByBackend', true); + }; + reloadAndProfile: (recordChangeDescriptions: boolean) => void = recordChangeDescriptions => { sessionStorageSetItem(SESSION_STORAGE_RELOAD_AND_PROFILE_KEY, 'true'); diff --git a/packages/react-devtools-shared/src/backend/fiber/renderer.js b/packages/react-devtools-shared/src/backend/fiber/renderer.js index b8636c557ea25..22a7afcc4c632 100644 --- a/packages/react-devtools-shared/src/backend/fiber/renderer.js +++ b/packages/react-devtools-shared/src/backend/fiber/renderer.js @@ -763,16 +763,30 @@ const hostResourceToDevToolsInstanceMap: Map< Set, > = new Map(); +// Ideally, this should be injected from Reconciler config function getPublicInstance(instance: HostInstance): HostInstance { // Typically the PublicInstance and HostInstance is the same thing but not in Fabric. // So we need to detect this and use that as the public instance. - return typeof instance === 'object' && - instance !== null && - typeof instance.canonical === 'object' - ? (instance.canonical: any) - : typeof instance._nativeTag === 'number' - ? instance._nativeTag - : instance; + + // React Native. Modern. Fabric. + if (typeof instance === 'object' && instance !== null) { + if (typeof instance.canonical === 'object' && instance.canonical !== null) { + if ( + typeof instance.canonical.publicInstance === 'object' && + instance.canonical.publicInstance !== null + ) { + return instance.canonical.publicInstance; + } + } + + // React Native. Legacy. Paper. + if (typeof instance._nativeTag === 'number') { + return instance._nativeTag; + } + } + + // React Web. Usually a DOM element. + return instance; } function aquireHostInstance( @@ -1029,6 +1043,10 @@ export function attach( if (devtoolsInstance.kind === FIBER_INSTANCE) { const fiber = devtoolsInstance.data; componentLogsEntry = fiberToComponentLogsMap.get(fiber); + + if (componentLogsEntry === undefined && fiber.alternate !== null) { + componentLogsEntry = fiberToComponentLogsMap.get(fiber.alternate); + } } else { const componentInfo = devtoolsInstance.data; componentLogsEntry = componentInfoToComponentLogsMap.get(componentInfo); @@ -4248,7 +4266,10 @@ export function attach( source = getSourceForFiberInstance(fiberInstance); } - const componentLogsEntry = fiberToComponentLogsMap.get(fiber); + let componentLogsEntry = fiberToComponentLogsMap.get(fiber); + if (componentLogsEntry === undefined && fiber.alternate !== null) { + componentLogsEntry = fiberToComponentLogsMap.get(fiber.alternate); + } return { id: fiberInstance.id, diff --git a/packages/react-devtools-shared/src/backend/index.js b/packages/react-devtools-shared/src/backend/index.js index 5893424b394c8..86714b7f61476 100644 --- a/packages/react-devtools-shared/src/backend/index.js +++ b/packages/react-devtools-shared/src/backend/index.js @@ -17,6 +17,7 @@ export function initBackend( hook: DevToolsHook, agent: Agent, global: Object, + isReloadAndProfileSupported: boolean, ): () => void { if (hook == null) { // DevTools didn't get injected into this page (maybe b'c of the contentType). @@ -94,6 +95,10 @@ export function initBackend( } }); + if (isReloadAndProfileSupported) { + agent.onReloadAndProfileSupportedByHost(); + } + return () => { subs.forEach(fn => fn()); }; diff --git a/packages/react-devtools-shared/src/bridge.js b/packages/react-devtools-shared/src/bridge.js index fcf9b2d21e88d..dde6e7c3ffff8 100644 --- a/packages/react-devtools-shared/src/bridge.js +++ b/packages/react-devtools-shared/src/bridge.js @@ -181,8 +181,7 @@ export type BackendEvents = { fastRefreshScheduled: [], getSavedPreferences: [], inspectedElement: [InspectedElementPayload], - isBackendStorageAPISupported: [boolean], - isSynchronousXHRSupported: [boolean], + isReloadAndProfileSupportedByBackend: [boolean], operations: [Array], ownersList: [OwnersList], overrideComponentFilters: [Array], diff --git a/packages/react-devtools-shared/src/devtools/store.js b/packages/react-devtools-shared/src/devtools/store.js index b54907338b372..8af997d9287ef 100644 --- a/packages/react-devtools-shared/src/devtools/store.js +++ b/packages/react-devtools-shared/src/devtools/store.js @@ -114,8 +114,8 @@ export default class Store extends EventEmitter<{ _bridge: FrontendBridge; // Computed whenever _errorsAndWarnings Map changes. - _cachedErrorCount: number = 0; - _cachedWarningCount: number = 0; + _cachedComponentWithErrorCount: number = 0; + _cachedComponentWithWarningCount: number = 0; _cachedErrorAndWarningTuples: ErrorAndWarningTuples | null = null; // Should new nodes be collapsed by default when added to the tree? @@ -138,16 +138,6 @@ export default class Store extends EventEmitter<{ // Should the React Native style editor panel be shown? _isNativeStyleEditorSupported: boolean = false; - // Can the backend use the Storage API (e.g. localStorage)? - // If not, features like reload-and-profile will not work correctly and must be disabled. - _isBackendStorageAPISupported: boolean = false; - - // Can DevTools use sync XHR requests? - // If not, features like reload-and-profile will not work correctly and must be disabled. - // This current limitation applies only to web extension builds - // and will need to be reconsidered in the future if we add support for reload to React Native. - _isSynchronousXHRSupported: boolean = false; - _nativeStyleEditorValidAttributes: $ReadOnlyArray | null = null; // Older backends don't support an explicit bridge protocol, @@ -178,10 +168,12 @@ export default class Store extends EventEmitter<{ // These options may be initially set by a configuration option when constructing the Store. _supportsInspectMatchingDOMElement: boolean = false; _supportsClickToInspect: boolean = false; - _supportsReloadAndProfile: boolean = false; _supportsTimeline: boolean = false; _supportsTraceUpdates: boolean = false; + _isReloadAndProfileFrontendSupported: boolean = false; + _isReloadAndProfileBackendSupported: boolean = false; + // These options default to false but may be updated as roots are added and removed. _rootSupportsBasicProfiling: boolean = false; _rootSupportsTimelineProfiling: boolean = false; @@ -196,6 +188,7 @@ export default class Store extends EventEmitter<{ _shouldCheckBridgeProtocolCompatibility: boolean = false; _hookSettings: $ReadOnly | null = null; + _shouldShowWarningsAndErrors: boolean = false; constructor(bridge: FrontendBridge, config?: Config) { super(); @@ -233,7 +226,7 @@ export default class Store extends EventEmitter<{ this._supportsClickToInspect = true; } if (supportsReloadAndProfile) { - this._supportsReloadAndProfile = true; + this._isReloadAndProfileFrontendSupported = true; } if (supportsTimeline) { this._supportsTimeline = true; @@ -254,17 +247,13 @@ export default class Store extends EventEmitter<{ ); bridge.addListener('shutdown', this.onBridgeShutdown); bridge.addListener( - 'isBackendStorageAPISupported', - this.onBackendStorageAPISupported, + 'isReloadAndProfileSupportedByBackend', + this.onBackendReloadAndProfileSupported, ); bridge.addListener( 'isNativeStyleEditorSupported', this.onBridgeNativeStyleEditorSupported, ); - bridge.addListener( - 'isSynchronousXHRSupported', - this.onBridgeSynchronousXHRSupported, - ); bridge.addListener( 'unsupportedRendererVersion', this.onBridgeUnsupportedRendererVersion, @@ -383,8 +372,24 @@ export default class Store extends EventEmitter<{ return this._bridgeProtocol; } - get errorCount(): number { - return this._cachedErrorCount; + get componentWithErrorCount(): number { + if (!this._shouldShowWarningsAndErrors) { + return 0; + } + + return this._cachedComponentWithErrorCount; + } + + get componentWithWarningCount(): number { + if (!this._shouldShowWarningsAndErrors) { + return 0; + } + + return this._cachedComponentWithWarningCount; + } + + get displayingErrorsAndWarningsEnabled(): boolean { + return this._shouldShowWarningsAndErrors; } get hasOwnerMetadata(): boolean { @@ -452,13 +457,9 @@ export default class Store extends EventEmitter<{ } get supportsReloadAndProfile(): boolean { - // Does the DevTools shell support reloading and eagerly injecting the renderer interface? - // And if so, can the backend use the localStorage API and sync XHR? - // All of these are currently required for the reload-and-profile feature to work. return ( - this._supportsReloadAndProfile && - this._isBackendStorageAPISupported && - this._isSynchronousXHRSupported + this._isReloadAndProfileFrontendSupported && + this._isReloadAndProfileBackendSupported ); } @@ -480,10 +481,6 @@ export default class Store extends EventEmitter<{ return this._unsupportedRendererVersionDetected; } - get warningCount(): number { - return this._cachedWarningCount; - } - containsElement(id: number): boolean { return this._idToElement.has(id); } @@ -581,7 +578,11 @@ export default class Store extends EventEmitter<{ } // Returns a tuple of [id, index] - getElementsWithErrorsAndWarnings(): Array<{id: number, index: number}> { + getElementsWithErrorsAndWarnings(): ErrorAndWarningTuples { + if (!this._shouldShowWarningsAndErrors) { + return []; + } + if (this._cachedErrorAndWarningTuples !== null) { return this._cachedErrorAndWarningTuples; } @@ -615,6 +616,10 @@ export default class Store extends EventEmitter<{ errorCount: number, warningCount: number, } { + if (!this._shouldShowWarningsAndErrors) { + return {errorCount: 0, warningCount: 0}; + } + return this._errorsAndWarnings.get(id) || {errorCount: 0, warningCount: 0}; } @@ -1325,16 +1330,21 @@ export default class Store extends EventEmitter<{ this._cachedErrorAndWarningTuples = null; if (haveErrorsOrWarningsChanged) { - let errorCount = 0; - let warningCount = 0; + let componentWithErrorCount = 0; + let componentWithWarningCount = 0; this._errorsAndWarnings.forEach(entry => { - errorCount += entry.errorCount; - warningCount += entry.warningCount; + if (entry.errorCount > 0) { + componentWithErrorCount++; + } + + if (entry.warningCount > 0) { + componentWithWarningCount++; + } }); - this._cachedErrorCount = errorCount; - this._cachedWarningCount = warningCount; + this._cachedComponentWithErrorCount = componentWithErrorCount; + this._cachedComponentWithWarningCount = componentWithWarningCount; } if (haveRootsChanged) { @@ -1407,17 +1417,13 @@ export default class Store extends EventEmitter<{ ); bridge.removeListener('shutdown', this.onBridgeShutdown); bridge.removeListener( - 'isBackendStorageAPISupported', - this.onBackendStorageAPISupported, + 'isReloadAndProfileSupportedByBackend', + this.onBackendReloadAndProfileSupported, ); bridge.removeListener( 'isNativeStyleEditorSupported', this.onBridgeNativeStyleEditorSupported, ); - bridge.removeListener( - 'isSynchronousXHRSupported', - this.onBridgeSynchronousXHRSupported, - ); bridge.removeListener( 'unsupportedRendererVersion', this.onBridgeUnsupportedRendererVersion, @@ -1432,18 +1438,10 @@ export default class Store extends EventEmitter<{ } }; - onBackendStorageAPISupported: ( - isBackendStorageAPISupported: boolean, - ) => void = isBackendStorageAPISupported => { - this._isBackendStorageAPISupported = isBackendStorageAPISupported; - - this.emit('supportsReloadAndProfile'); - }; - - onBridgeSynchronousXHRSupported: ( - isSynchronousXHRSupported: boolean, - ) => void = isSynchronousXHRSupported => { - this._isSynchronousXHRSupported = isSynchronousXHRSupported; + onBackendReloadAndProfileSupported: ( + isReloadAndProfileSupported: boolean, + ) => void = isReloadAndProfileSupported => { + this._isReloadAndProfileBackendSupported = isReloadAndProfileSupported; this.emit('supportsReloadAndProfile'); }; @@ -1528,9 +1526,21 @@ export default class Store extends EventEmitter<{ onHookSettings: (settings: $ReadOnly) => void = settings => { this._hookSettings = settings; + + this.setShouldShowWarningsAndErrors(settings.showInlineWarningsAndErrors); this.emit('hookSettings', settings); }; + setShouldShowWarningsAndErrors(status: boolean): void { + const previousStatus = this._shouldShowWarningsAndErrors; + this._shouldShowWarningsAndErrors = status; + + if (previousStatus !== status) { + // Propagate to subscribers, although tree state has not changed + this.emit('mutated', [[], new Map()]); + } + } + // The Store should never throw an Error without also emitting an event. // Otherwise Store errors will be invisible to users, // but the downstream errors they cause will be reported as bugs. diff --git a/packages/react-devtools-shared/src/devtools/views/Components/Element.js b/packages/react-devtools-shared/src/devtools/views/Components/Element.js index f5c30d2d2b0d6..48bfbe90906ff 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/Element.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/Element.js @@ -12,7 +12,6 @@ import {Fragment, useContext, useMemo, useState} from 'react'; import Store from 'react-devtools-shared/src/devtools/store'; import ButtonIcon from '../ButtonIcon'; import {TreeDispatcherContext, TreeStateContext} from './TreeContext'; -import {SettingsContext} from '../Settings/SettingsContext'; import {StoreContext} from '../context'; import {useSubscription} from '../hooks'; import {logEvent} from 'react-devtools-shared/src/Logger'; @@ -37,7 +36,6 @@ export default function Element({data, index, style}: Props): React.Node { const {ownerFlatTree, ownerID, selectedElementID} = useContext(TreeStateContext); const dispatch = useContext(TreeDispatcherContext); - const {showInlineWarningsAndErrors} = React.useContext(SettingsContext); const element = ownerFlatTree !== null @@ -181,7 +179,7 @@ export default function Element({data, index, style}: Props): React.Node { className={styles.BadgesBlock} /> - {showInlineWarningsAndErrors && errorCount > 0 && ( + {errorCount > 0 && ( )} - {showInlineWarningsAndErrors && warningCount > 0 && ( + {warningCount > 0 && ( diff --git a/packages/react-devtools-shared/src/devtools/views/Components/Tree.js b/packages/react-devtools-shared/src/devtools/views/Components/Tree.js index 136baa1205de2..db1bf9a98c135 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/Tree.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/Tree.js @@ -73,7 +73,7 @@ export default function Tree(props: Props): React.Node { const [treeFocused, setTreeFocused] = useState(false); - const {lineHeight, showInlineWarningsAndErrors} = useContext(SettingsContext); + const {lineHeight} = useContext(SettingsContext); // Make sure a newly selected element is visible in the list. // This is helpful for things like the owners list and search. @@ -325,8 +325,8 @@ export default function Tree(props: Props): React.Node { const errorsOrWarningsSubscription = useMemo( () => ({ getCurrentValue: () => ({ - errors: store.errorCount, - warnings: store.warningCount, + errors: store.componentWithErrorCount, + warnings: store.componentWithWarningCount, }), subscribe: (callback: Function) => { store.addListener('mutated', callback); @@ -370,40 +370,38 @@ export default function Tree(props: Props): React.Node { }> {ownerID !== null ? : } - {showInlineWarningsAndErrors && - ownerID === null && - (errors > 0 || warnings > 0) && ( - -
- {errors > 0 && ( -
- - {errors} -
- )} - {warnings > 0 && ( -
- - {warnings} -
- )} - - - - - )} + {ownerID === null && (errors > 0 || warnings > 0) && ( + +
+ {errors > 0 && ( +
+ + {errors} +
+ )} + {warnings > 0 && ( +
+ + {warnings} +
+ )} + + + + + )} {!hideSettings && (
diff --git a/packages/react-devtools-shared/src/devtools/views/Settings/DebuggingSettings.js b/packages/react-devtools-shared/src/devtools/views/Settings/DebuggingSettings.js index 8bde14e62e606..c48cdb58e3e4c 100644 --- a/packages/react-devtools-shared/src/devtools/views/Settings/DebuggingSettings.js +++ b/packages/react-devtools-shared/src/devtools/views/Settings/DebuggingSettings.js @@ -37,6 +37,10 @@ export default function DebuggingSettings({ const [showInlineWarningsAndErrors, setShowInlineWarningsAndErrors] = useState(usedHookSettings.showInlineWarningsAndErrors); + useEffect(() => { + store.setShouldShowWarningsAndErrors(showInlineWarningsAndErrors); + }, [showInlineWarningsAndErrors]); + useEffect(() => { store.updateHookSettings({ appendComponentStack, diff --git a/packages/react-devtools-shared/src/devtools/views/Settings/SettingsContext.js b/packages/react-devtools-shared/src/devtools/views/Settings/SettingsContext.js index 17514d94648ac..196ea806f6aac 100644 --- a/packages/react-devtools-shared/src/devtools/views/Settings/SettingsContext.js +++ b/packages/react-devtools-shared/src/devtools/views/Settings/SettingsContext.js @@ -43,21 +43,9 @@ type Context = { // Specified as a separate prop so it can trigger a re-render of FixedSizeList. lineHeight: number, - appendComponentStack: boolean, - setAppendComponentStack: (value: boolean) => void, - - breakOnConsoleErrors: boolean, - setBreakOnConsoleErrors: (value: boolean) => void, - parseHookNames: boolean, setParseHookNames: (value: boolean) => void, - hideConsoleLogsInStrictMode: boolean, - setHideConsoleLogsInStrictMode: (value: boolean) => void, - - showInlineWarningsAndErrors: boolean, - setShowInlineWarningsAndErrors: (value: boolean) => void, - theme: Theme, setTheme(value: Theme): void, @@ -176,7 +164,7 @@ function SettingsContextController({ bridge.send('setTraceUpdatesEnabled', traceUpdatesEnabled); }, [bridge, traceUpdatesEnabled]); - const value = useMemo( + const value: Context = useMemo( () => ({ displayDensity, lineHeight: diff --git a/packages/react-devtools-shared/src/utils.js b/packages/react-devtools-shared/src/utils.js index e24414812c5b3..a9ebeaaa129da 100644 --- a/packages/react-devtools-shared/src/utils.js +++ b/packages/react-devtools-shared/src/utils.js @@ -61,6 +61,7 @@ import type { LRUCache, } from 'react-devtools-shared/src/frontend/types'; import type {SerializedElement as SerializedElementBackend} from 'react-devtools-shared/src/backend/types'; +import {isSynchronousXHRSupported} from './backend/utils'; // $FlowFixMe[method-unbinding] const hasOwnProperty = Object.prototype.hasOwnProperty; @@ -965,3 +966,15 @@ export function backendToFrontendSerializedElementMapper( export function normalizeUrl(url: string): string { return url.replace('/./', '/'); } + +export function getIsReloadAndProfileSupported(): boolean { + // Notify the frontend if the backend supports the Storage API (e.g. localStorage). + // If not, features like reload-and-profile will not work correctly and must be disabled. + let isBackendStorageAPISupported = false; + try { + localStorage.getItem('test'); + isBackendStorageAPISupported = true; + } catch (error) {} + + return isBackendStorageAPISupported && isSynchronousXHRSupported(); +} diff --git a/packages/react-devtools-timeline/package.json b/packages/react-devtools-timeline/package.json index b9a8aab901d54..883aad785f6b2 100644 --- a/packages/react-devtools-timeline/package.json +++ b/packages/react-devtools-timeline/package.json @@ -1,7 +1,7 @@ { "private": true, "name": "react-devtools-timeline", - "version": "5.3.1", + "version": "6.0.0", "license": "MIT", "dependencies": { "@elg/speedscope": "1.9.0-a6f84db", diff --git a/packages/react-devtools/CHANGELOG.md b/packages/react-devtools/CHANGELOG.md index bfebd3ea0a705..aab41edf090f3 100644 --- a/packages/react-devtools/CHANGELOG.md +++ b/packages/react-devtools/CHANGELOG.md @@ -4,6 +4,38 @@ --- +### 6.0.0 +September 25, 2024 + +#### Features +* Support Server Components in Tree ([sebmarkbage](https://github.com/sebmarkbage) in [#30684](https://github.com/facebook/react/pull/30684)) +* feat: expose installHook with settings argument from react-devtools-core/backend ([hoxyq](https://github.com/hoxyq) in [#30987](https://github.com/facebook/react/pull/30987)) +* Add Filtering of Environment Names ([sebmarkbage](https://github.com/sebmarkbage) in [#30850](https://github.com/facebook/react/pull/30850)) +* Support secondary environment name when it changes ([sebmarkbage](https://github.com/sebmarkbage) in [#30842](https://github.com/facebook/react/pull/30842)) +* Filter Server Components ([sebmarkbage](https://github.com/sebmarkbage) in [#30839](https://github.com/facebook/react/pull/30839)) +* Make function inspection instant ([sebmarkbage](https://github.com/sebmarkbage) in [#30786](https://github.com/facebook/react/pull/30786)) +* Make Functions Clickable to Jump to Definition ([sebmarkbage](https://github.com/sebmarkbage) in [#30769](https://github.com/facebook/react/pull/30769)) +* [Flight/DevTools] Pass the Server Component's "key" as Part of the ReactComponentInfo ([sebmarkbage](https://github.com/sebmarkbage) in [#30703](https://github.com/facebook/react/pull/30703)) +* Make Element Inspection Feel Snappy ([sebmarkbage](https://github.com/sebmarkbage) in [#30555](https://github.com/facebook/react/pull/30555)) +* Print component stacks as error objects to get source mapping ([sebmarkbage](https://github.com/sebmarkbage) in [#30289](https://github.com/facebook/react/pull/30289)) + +#### Bugfixes +* Fix: profiling crashes #30661 #28838 ([EdmondChuiHW](https://github.com/EdmondChuiHW) in [#31024](https://github.com/facebook/react/pull/31024)) +* chore: remove settings manager from react-devtools-core ([hoxyq](https://github.com/hoxyq) in [#30986](https://github.com/facebook/react/pull/30986)) +* fix[react-devtools/extensions]: fixed tabs API calls and displaying restricted access popup ([hoxyq](https://github.com/hoxyq) in [#30825](https://github.com/facebook/react/pull/30825)) +* fix[react-devtools/InspectedElement]: fixed border stylings when some of the panels are not rendered ([hoxyq](https://github.com/hoxyq) in [#30676](https://github.com/facebook/react/pull/30676)) +* fix: path handling in react devtools ([Jack-Works](https://github.com/Jack-Works) in [#29199](https://github.com/facebook/react/pull/29199)) + +#### Other +* feat: display message if user ended up opening hook script ([hoxyq](https://github.com/hoxyq) in [#31000](https://github.com/facebook/react/pull/31000)) +* refactor[react-devtools]: move console patching to global hook ([hoxyq](https://github.com/hoxyq) in [#30596](https://github.com/facebook/react/pull/30596)) +* refactor[react-devtools]: initialize renderer interface early ([hoxyq](https://github.com/hoxyq) in [#30946](https://github.com/facebook/react/pull/30946)) +* Use Unicode Atom Symbol instead of Atom Emoji ([sebmarkbage](https://github.com/sebmarkbage) in [#30832](https://github.com/facebook/react/pull/30832)) +* feat[react-devtools]: support Manifest v3 for Firefox extension ([hoxyq](https://github.com/hoxyq) in [#30824](https://github.com/facebook/react/pull/30824)) +* Enable pointEvents while scrolling ([sebmarkbage](https://github.com/sebmarkbage) in [#30560](https://github.com/facebook/react/pull/30560)) + +--- + ### 5.3.1 July 3, 2024 diff --git a/packages/react-devtools/package.json b/packages/react-devtools/package.json index c9db18aca46a1..0972fd8c94e7a 100644 --- a/packages/react-devtools/package.json +++ b/packages/react-devtools/package.json @@ -1,6 +1,6 @@ { "name": "react-devtools", - "version": "5.3.1", + "version": "6.0.0", "description": "Use react-devtools outside of the browser", "license": "MIT", "repository": { @@ -26,7 +26,7 @@ "electron": "^23.1.2", "internal-ip": "^6.2.0", "minimist": "^1.2.3", - "react-devtools-core": "5.3.1", + "react-devtools-core": "6.0.0", "update-notifier": "^2.1.0" } } diff --git a/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js b/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js index d46f61035b8d4..af53a2a6acf47 100644 --- a/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js +++ b/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js @@ -3401,7 +3401,7 @@ export function suspendResource( } } -export function waitForCommitToBeReady(): null | (Function => Function) { +export function waitForCommitToBeReady(): null | ((() => void) => () => void) { if (suspendedState === null) { throw new Error( 'Internal React Error: suspendedState null when it was expected to exists. Please report this as a React bug.', diff --git a/packages/react-dom/src/__tests__/ReactDOMFiberAsync-test.js b/packages/react-dom/src/__tests__/ReactDOMFiberAsync-test.js index ee843996bef1c..027099d54707c 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFiberAsync-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFiberAsync-test.js @@ -744,7 +744,7 @@ describe('ReactDOMFiberAsync', () => { // Because it suspended, it remains on the current path expect(div.textContent).toBe('/path/a'); }); - assertLog([]); + assertLog(gate('enableSiblingPrerendering') ? ['Suspend! [/path/b]'] : []); await act(async () => { resolvePromise(); diff --git a/packages/react-dom/src/__tests__/ReactLegacyUpdates-test.js b/packages/react-dom/src/__tests__/ReactLegacyUpdates-test.js index 26f56a938c551..57f2acfa53465 100644 --- a/packages/react-dom/src/__tests__/ReactLegacyUpdates-test.js +++ b/packages/react-dom/src/__tests__/ReactLegacyUpdates-test.js @@ -1427,7 +1427,7 @@ describe('ReactLegacyUpdates', () => { } } - let limit = 55; + let limit = 105; await expect(async () => { await act(() => { ReactDOM.render(, container); diff --git a/packages/react-dom/src/__tests__/ReactUpdates-test.js b/packages/react-dom/src/__tests__/ReactUpdates-test.js index faf4b29551350..247a53531c659 100644 --- a/packages/react-dom/src/__tests__/ReactUpdates-test.js +++ b/packages/react-dom/src/__tests__/ReactUpdates-test.js @@ -1542,7 +1542,7 @@ describe('ReactUpdates', () => { } } - let limit = 55; + let limit = 105; const root = ReactDOMClient.createRoot(container); await expect(async () => { await act(() => { diff --git a/packages/react-native-renderer/src/ReactNativeFiberInspector.js b/packages/react-native-renderer/src/ReactNativeFiberInspector.js index b0859471c463e..d0423f1d48cfa 100644 --- a/packages/react-native-renderer/src/ReactNativeFiberInspector.js +++ b/packages/react-native-renderer/src/ReactNativeFiberInspector.js @@ -209,6 +209,8 @@ function getInspectorDataForViewAtPoint( closestInstance = internalInstanceHandle.stateNode.canonical.internalInstanceHandle; + const closestPublicInstance = + internalInstanceHandle.stateNode.canonical.publicInstance; // Note: this is deprecated and we want to remove it ASAP. Keeping it here for React DevTools compatibility for now. const nativeViewTag = @@ -224,6 +226,7 @@ function getInspectorDataForViewAtPoint( pointerY: locationY, frame: {left: pageX, top: pageY, width, height}, touchedViewTag: nativeViewTag, + closestPublicInstance, }); }, ); @@ -243,6 +246,7 @@ function getInspectorDataForViewAtPoint( pointerY: locationY, frame: {left, top, width, height}, touchedViewTag: nativeViewTag, + closestPublicInstance: nativeViewTag, }); }, ); diff --git a/packages/react-native-renderer/src/ReactNativeTypes.js b/packages/react-native-renderer/src/ReactNativeTypes.js index 000ea0d0f766d..9692a1256acff 100644 --- a/packages/react-native-renderer/src/ReactNativeTypes.js +++ b/packages/react-native-renderer/src/ReactNativeTypes.js @@ -179,6 +179,7 @@ export type TouchedViewDataAtPoint = $ReadOnly<{ width: number, height: number, }>, + closestPublicInstance?: PublicInstance, ...InspectorData, }>; diff --git a/packages/react-reconciler/src/ReactFiberCompleteWork.js b/packages/react-reconciler/src/ReactFiberCompleteWork.js index e3fba900dd116..b24351ac383b9 100644 --- a/packages/react-reconciler/src/ReactFiberCompleteWork.js +++ b/packages/react-reconciler/src/ReactFiberCompleteWork.js @@ -42,6 +42,7 @@ import { enableRenderableContext, passChildrenWhenCloningPersistedNodes, disableLegacyMode, + enableSiblingPrerendering, } from 'shared/ReactFeatureFlags'; import {now} from './Scheduler'; @@ -622,7 +623,9 @@ function scheduleRetryEffect( // Track the lanes that have been scheduled for an immediate retry so that // we can mark them as suspended upon committing the root. - markSpawnedRetryLane(retryLane); + if (enableSiblingPrerendering) { + markSpawnedRetryLane(retryLane); + } } } diff --git a/packages/react-reconciler/src/ReactFiberLane.js b/packages/react-reconciler/src/ReactFiberLane.js index b8c051def4eef..b98019046e3c2 100644 --- a/packages/react-reconciler/src/ReactFiberLane.js +++ b/packages/react-reconciler/src/ReactFiberLane.js @@ -27,6 +27,7 @@ import { transitionLaneExpirationMs, retryLaneExpirationMs, disableLegacyMode, + enableSiblingPrerendering, } from 'shared/ReactFeatureFlags'; import {isDevToolsPresent} from './ReactFiberDevToolsHook'; import {clz32} from './clz32'; @@ -270,11 +271,13 @@ export function getNextLanes(root: FiberRoot, wipLanes: Lanes): Lanes { if (nonIdlePingedLanes !== NoLanes) { nextLanes = getHighestPriorityLanes(nonIdlePingedLanes); } else { - // Nothing has been pinged. Check for lanes that need to be prewarmed. - if (!rootHasPendingCommit) { - const lanesToPrewarm = nonIdlePendingLanes & ~warmLanes; - if (lanesToPrewarm !== NoLanes) { - nextLanes = getHighestPriorityLanes(lanesToPrewarm); + if (enableSiblingPrerendering) { + // Nothing has been pinged. Check for lanes that need to be prewarmed. + if (!rootHasPendingCommit) { + const lanesToPrewarm = nonIdlePendingLanes & ~warmLanes; + if (lanesToPrewarm !== NoLanes) { + nextLanes = getHighestPriorityLanes(lanesToPrewarm); + } } } } @@ -294,11 +297,13 @@ export function getNextLanes(root: FiberRoot, wipLanes: Lanes): Lanes { if (pingedLanes !== NoLanes) { nextLanes = getHighestPriorityLanes(pingedLanes); } else { - // Nothing has been pinged. Check for lanes that need to be prewarmed. - if (!rootHasPendingCommit) { - const lanesToPrewarm = pendingLanes & ~warmLanes; - if (lanesToPrewarm !== NoLanes) { - nextLanes = getHighestPriorityLanes(lanesToPrewarm); + if (enableSiblingPrerendering) { + // Nothing has been pinged. Check for lanes that need to be prewarmed. + if (!rootHasPendingCommit) { + const lanesToPrewarm = pendingLanes & ~warmLanes; + if (lanesToPrewarm !== NoLanes) { + nextLanes = getHighestPriorityLanes(lanesToPrewarm); + } } } } @@ -760,12 +765,14 @@ export function markRootSuspended( root: FiberRoot, suspendedLanes: Lanes, spawnedLane: Lane, - didSkipSuspendedSiblings: boolean, + didAttemptEntireTree: boolean, ) { + // TODO: Split this into separate functions for marking the root at the end of + // a render attempt versus suspending while the root is still in progress. root.suspendedLanes |= suspendedLanes; root.pingedLanes &= ~suspendedLanes; - if (!didSkipSuspendedSiblings) { + if (enableSiblingPrerendering && didAttemptEntireTree) { // Mark these lanes as warm so we know there's nothing else to work on. root.warmLanes |= suspendedLanes; } else { @@ -876,6 +883,7 @@ export function markRootFinished( // suspended) instead of the regular mode (i.e. unwind and skip the siblings // as soon as something suspends to unblock the rest of the update). if ( + enableSiblingPrerendering && suspendedRetryLanes !== NoLanes && // Note that we only do this if there were no updates since we started // rendering. This mirrors the logic in markRootUpdated — whenever we diff --git a/packages/react-reconciler/src/ReactFiberRootScheduler.js b/packages/react-reconciler/src/ReactFiberRootScheduler.js index 15730d3e5b624..39f6f466ed983 100644 --- a/packages/react-reconciler/src/ReactFiberRootScheduler.js +++ b/packages/react-reconciler/src/ReactFiberRootScheduler.js @@ -15,6 +15,10 @@ import type {BatchConfigTransition} from './ReactFiberTracingMarkerComponent'; import { disableLegacyMode, enableDeferRootSchedulingToMicrotask, + disableSchedulerTimeoutInWorkLoop, + enableProfilerTimer, + enableProfilerNestedUpdatePhase, + enableSiblingPrerendering, } from 'shared/ReactFeatureFlags'; import { NoLane, @@ -26,17 +30,18 @@ import { markStarvedLanesAsExpired, claimNextTransitionLane, getNextLanesToFlushSync, + checkIfRootIsPrerendering, } from './ReactFiberLane'; import { CommitContext, NoContext, RenderContext, + flushPassiveEffects, getExecutionContext, getWorkInProgressRoot, getWorkInProgressRootRenderLanes, isWorkLoopSuspendedOnData, - performConcurrentWorkOnRoot, - performSyncWorkOnRoot, + performWorkOnRoot, } from './ReactFiberWorkLoop'; import {LegacyRoot} from './ReactRootTags'; import { @@ -62,6 +67,10 @@ import { } from './ReactFiberConfig'; import ReactSharedInternals from 'shared/ReactSharedInternals'; +import { + resetNestedUpdateFlag, + syncNestedUpdateFlag, +} from './ReactProfilerTimer'; // A linked list of all the roots with pending work. In an idiomatic app, // there's only a single root, but we do support multi root apps, hence this @@ -199,7 +208,10 @@ function flushSyncWorkAcrossRoots_impl( ? workInProgressRootRenderLanes : NoLanes, ); - if (includesSyncLane(nextLanes)) { + if ( + includesSyncLane(nextLanes) && + !checkIfRootIsPrerendering(root, nextLanes) + ) { // This root has pending sync work. Flush it now. didPerformSomeWork = true; performSyncWorkOnRoot(root, nextLanes); @@ -334,7 +346,13 @@ function scheduleTaskForRootDuringMicrotask( } // Schedule a new callback in the host environment. - if (includesSyncLane(nextLanes)) { + if ( + includesSyncLane(nextLanes) && + // If we're prerendering, then we should use the concurrent work loop + // even if the lanes are synchronous, so that prerendering never blocks + // the main thread. + !(enableSiblingPrerendering && checkIfRootIsPrerendering(root, nextLanes)) + ) { // Synchronous work is always flushed at the end of the microtask, so we // don't need to schedule an additional task. if (existingCallbackNode !== null) { @@ -368,9 +386,10 @@ function scheduleTaskForRootDuringMicrotask( let schedulerPriorityLevel; switch (lanesToEventPriority(nextLanes)) { + // Scheduler does have an "ImmediatePriority", but now that we use + // microtasks for sync work we no longer use that. Any sync work that + // reaches this path is meant to be time sliced. case DiscreteEventPriority: - schedulerPriorityLevel = ImmediateSchedulerPriority; - break; case ContinuousEventPriority: schedulerPriorityLevel = UserBlockingSchedulerPriority; break; @@ -387,7 +406,7 @@ function scheduleTaskForRootDuringMicrotask( const newCallbackNode = scheduleCallback( schedulerPriorityLevel, - performConcurrentWorkOnRoot.bind(null, root), + performWorkOnRootViaSchedulerTask.bind(null, root), ); root.callbackPriority = newCallbackPriority; @@ -396,15 +415,67 @@ function scheduleTaskForRootDuringMicrotask( } } -export type RenderTaskFn = (didTimeout: boolean) => RenderTaskFn | null; +type RenderTaskFn = (didTimeout: boolean) => RenderTaskFn | null; -export function getContinuationForRoot( +function performWorkOnRootViaSchedulerTask( root: FiberRoot, - originalCallbackNode: mixed, + didTimeout: boolean, ): RenderTaskFn | null { - // This is called at the end of `performConcurrentWorkOnRoot` to determine - // if we need to schedule a continuation task. - // + // This is the entry point for concurrent tasks scheduled via Scheduler (and + // postTask, in the future). + + if (enableProfilerTimer && enableProfilerNestedUpdatePhase) { + resetNestedUpdateFlag(); + } + + // Flush any pending passive effects before deciding which lanes to work on, + // in case they schedule additional work. + const originalCallbackNode = root.callbackNode; + const didFlushPassiveEffects = flushPassiveEffects(); + if (didFlushPassiveEffects) { + // Something in the passive effect phase may have canceled the current task. + // Check if the task node for this root was changed. + if (root.callbackNode !== originalCallbackNode) { + // The current task was canceled. Exit. We don't need to call + // `ensureRootIsScheduled` because the check above implies either that + // there's a new task, or that there's no remaining work on this root. + return null; + } else { + // Current task was not canceled. Continue. + } + } + + // Determine the next lanes to work on, using the fields stored on the root. + // TODO: We already called getNextLanes when we scheduled the callback; we + // should be able to avoid calling it again by stashing the result on the + // root object. However, because we always schedule the callback during + // a microtask (scheduleTaskForRootDuringMicrotask), it's possible that + // an update was scheduled earlier during this same browser task (and + // therefore before the microtasks have run). That's because Scheduler batches + // together multiple callbacks into a single browser macrotask, without + // yielding to microtasks in between. We should probably change this to align + // with the postTask behavior (and literally use postTask when + // it's available). + const workInProgressRoot = getWorkInProgressRoot(); + const workInProgressRootRenderLanes = getWorkInProgressRootRenderLanes(); + const lanes = getNextLanes( + root, + root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes, + ); + if (lanes === NoLanes) { + // No more work on this root. + return null; + } + + // Enter the work loop. + // TODO: We only check `didTimeout` defensively, to account for a Scheduler + // bug we're still investigating. Once the bug in Scheduler is fixed, + // we can remove this, since we track expiration ourselves. + const forceSync = !disableSchedulerTimeoutInWorkLoop && didTimeout; + performWorkOnRoot(root, lanes, forceSync); + + // The work loop yielded, but there may or may not be work left at the current + // priority. Need to determine whether we need to schedule a continuation. // Usually `scheduleTaskForRootDuringMicrotask` only runs inside a microtask; // however, since most of the logic for determining if we need a continuation // versus a new task is the same, we cheat a bit and call it here. This is @@ -414,11 +485,27 @@ export function getContinuationForRoot( if (root.callbackNode === originalCallbackNode) { // The task node scheduled for this root is the same one that's // currently executed. Need to return a continuation. - return performConcurrentWorkOnRoot.bind(null, root); + return performWorkOnRootViaSchedulerTask.bind(null, root); } return null; } +function performSyncWorkOnRoot(root: FiberRoot, lanes: Lanes) { + // This is the entry point for synchronous tasks that don't go + // through Scheduler. + const didFlushPassiveEffects = flushPassiveEffects(); + if (didFlushPassiveEffects) { + // If passive effects were flushed, exit to the outer work loop in the root + // scheduler, so we can recompute the priority. + return null; + } + if (enableProfilerTimer && enableProfilerNestedUpdatePhase) { + syncNestedUpdateFlag(); + } + const forceSync = true; + performWorkOnRoot(root, lanes, forceSync); +} + const fakeActCallbackNode = {}; function scheduleCallback( diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.js b/packages/react-reconciler/src/ReactFiberWorkLoop.js index c13b9c65a65a4..ff50ff6973d1e 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.js @@ -22,7 +22,6 @@ import type { TransitionAbort, } from './ReactFiberTracingMarkerComponent'; import type {OffscreenInstance} from './ReactFiberActivityComponent'; -import type {RenderTaskFn} from './ReactFiberRootScheduler'; import type {Resource} from './ReactFiberConfig'; import { @@ -32,7 +31,6 @@ import { enableProfilerNestedUpdatePhase, enableDebugTracing, enableSchedulingProfiler, - disableSchedulerTimeoutInWorkLoop, enableUpdaterTracking, enableCache, enableTransitionTracing, @@ -250,11 +248,9 @@ import { recordRenderTime, recordCommitTime, recordCommitEndTime, - resetNestedUpdateFlag, startProfilerTimer, stopProfilerTimerIfRunningAndRecordDuration, stopProfilerTimerIfRunningAndRecordIncompleteDuration, - syncNestedUpdateFlag, } from './ReactProfilerTimer'; import {setCurrentTrackFromLanes} from './ReactFiberPerformanceTrack'; @@ -308,7 +304,6 @@ import { ensureRootIsScheduled, flushSyncWorkOnAllRoots, flushSyncWorkOnLegacyRootsOnly, - getContinuationForRoot, requestTransitionLane, } from './ReactFiberRootScheduler'; import {getMaskedContext, getUnmaskedContext} from './ReactFiberContext'; @@ -613,13 +608,13 @@ let pendingPassiveEffectsRenderEndTime: number = -0; // Profiling-only let pendingPassiveTransitions: Array | null = null; // Use these to prevent an infinite loop of nested updates -const NESTED_UPDATE_LIMIT = 50; +const NESTED_UPDATE_LIMIT = 100; let nestedUpdateCount: number = 0; let rootWithNestedUpdates: FiberRoot | null = null; let isFlushingPassiveEffects = false; let didScheduleUpdateDuringPassiveEffects = false; -const NESTED_PASSIVE_UPDATE_LIMIT = 50; +const NESTED_PASSIVE_UPDATE_LIMIT = 100; let nestedPassiveUpdateCount: number = 0; let rootWithPassiveNestedUpdates: FiberRoot | null = null; @@ -770,11 +765,12 @@ export function scheduleUpdateOnFiber( // The incoming update might unblock the current render. Interrupt the // current attempt and restart from the top. prepareFreshStack(root, NoLanes); + const didAttemptEntireTree = false; markRootSuspended( root, workInProgressRootRenderLanes, workInProgressDeferredLane, - workInProgressRootDidSkipSuspendedSiblings, + didAttemptEntireTree, ); } @@ -837,11 +833,12 @@ export function scheduleUpdateOnFiber( // effect of interrupting the current render and switching to the update. // TODO: Make sure this doesn't override pings that happen while we've // already started rendering. + const didAttemptEntireTree = false; markRootSuspended( root, workInProgressRootRenderLanes, workInProgressDeferredLane, - workInProgressRootDidSkipSuspendedSiblings, + didAttemptEntireTree, ); } } @@ -890,150 +887,135 @@ export function isUnsafeClassRenderPhaseUpdate(fiber: Fiber): boolean { return (executionContext & RenderContext) !== NoContext; } -// This is the entry point for every concurrent task, i.e. anything that -// goes through Scheduler. -export function performConcurrentWorkOnRoot( +export function performWorkOnRoot( root: FiberRoot, - didTimeout: boolean, -): RenderTaskFn | null { - if (enableProfilerTimer && enableProfilerNestedUpdatePhase) { - resetNestedUpdateFlag(); - } - + lanes: Lanes, + forceSync: boolean, +): void { if ((executionContext & (RenderContext | CommitContext)) !== NoContext) { throw new Error('Should not already be working.'); } - // Flush any pending passive effects before deciding which lanes to work on, - // in case they schedule additional work. - const originalCallbackNode = root.callbackNode; - const didFlushPassiveEffects = flushPassiveEffects(); - if (didFlushPassiveEffects) { - // Something in the passive effect phase may have canceled the current task. - // Check if the task node for this root was changed. - if (root.callbackNode !== originalCallbackNode) { - // The current task was canceled. Exit. We don't need to call - // `ensureRootIsScheduled` because the check above implies either that - // there's a new task, or that there's no remaining work on this root. - return null; - } else { - // Current task was not canceled. Continue. - } - } - - // Determine the next lanes to work on, using the fields stored - // on the root. - // TODO: This was already computed in the caller. Pass it as an argument. - let lanes = getNextLanes( - root, - root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes, - ); - if (lanes === NoLanes) { - // Defensive coding. This is never expected to happen. - return null; - } - // We disable time-slicing in some cases: if the work has been CPU-bound // for too long ("expired" work, to prevent starvation), or we're in // sync-updates-by-default mode. - // TODO: We only check `didTimeout` defensively, to account for a Scheduler - // bug we're still investigating. Once the bug in Scheduler is fixed, - // we can remove this, since we track expiration ourselves. const shouldTimeSlice = - !includesBlockingLane(lanes) && - !includesExpiredLane(root, lanes) && - (disableSchedulerTimeoutInWorkLoop || !didTimeout); + (!forceSync && + !includesBlockingLane(lanes) && + !includesExpiredLane(root, lanes)) || + // If we're prerendering, then we should use the concurrent work loop + // even if the lanes are synchronous, so that prerendering never blocks + // the main thread. + // TODO: We should consider doing this whenever a sync lane is suspended, + // even for regular pings. + (enableSiblingPrerendering && checkIfRootIsPrerendering(root, lanes)); + let exitStatus = shouldTimeSlice ? renderRootConcurrent(root, lanes) - : renderRootSync(root, lanes); + : renderRootSync(root, lanes, true); - if (exitStatus !== RootInProgress) { + do { let renderWasConcurrent = shouldTimeSlice; - do { - if (exitStatus === RootDidNotComplete) { - // The render unwound without completing the tree. This happens in special - // cases where need to exit the current render without producing a - // consistent tree or committing. - markRootSuspended( + if (exitStatus === RootInProgress) { + // Render phase is still in progress. + if ( + enableSiblingPrerendering && + workInProgressRootIsPrerendering && + !shouldTimeSlice + ) { + // We're in prerendering mode, but time slicing is not enabled. This + // happens when something suspends during a synchronous update. Exit the + // the work loop. When we resume, we'll use the concurrent work loop so + // that prerendering is non-blocking. + // + // Mark the root as suspended. Usually we do this at the end of the + // render phase, but we do it here so that we resume in + // prerendering mode. + // TODO: Consider always calling markRootSuspended immediately. + // Needs to be *after* we attach a ping listener, though. + const didAttemptEntireTree = false; + markRootSuspended(root, lanes, NoLane, didAttemptEntireTree); + } + break; + } else if (exitStatus === RootDidNotComplete) { + // The render unwound without completing the tree. This happens in special + // cases where need to exit the current render without producing a + // consistent tree or committing. + const didAttemptEntireTree = !workInProgressRootDidSkipSuspendedSiblings; + markRootSuspended(root, lanes, NoLane, didAttemptEntireTree); + } else { + // The render completed. + + // Check if this render may have yielded to a concurrent event, and if so, + // confirm that any newly rendered stores are consistent. + // TODO: It's possible that even a concurrent render may never have yielded + // to the main thread, if it was fast enough, or if it expired. We could + // skip the consistency check in that case, too. + const finishedWork: Fiber = (root.current.alternate: any); + if ( + renderWasConcurrent && + !isRenderConsistentWithExternalStores(finishedWork) + ) { + // A store was mutated in an interleaved event. Render again, + // synchronously, to block further mutations. + exitStatus = renderRootSync(root, lanes, false); + // We assume the tree is now consistent because we didn't yield to any + // concurrent events. + renderWasConcurrent = false; + // Need to check the exit status again. + continue; + } + + // Check if something threw + if ( + (disableLegacyMode || root.tag !== LegacyRoot) && + exitStatus === RootErrored + ) { + const lanesThatJustErrored = lanes; + const errorRetryLanes = getLanesToRetrySynchronouslyOnError( root, - lanes, - NoLane, - workInProgressRootDidSkipSuspendedSiblings, + lanesThatJustErrored, ); - } else { - // The render completed. - - // Check if this render may have yielded to a concurrent event, and if so, - // confirm that any newly rendered stores are consistent. - // TODO: It's possible that even a concurrent render may never have yielded - // to the main thread, if it was fast enough, or if it expired. We could - // skip the consistency check in that case, too. - const finishedWork: Fiber = (root.current.alternate: any); - if ( - renderWasConcurrent && - !isRenderConsistentWithExternalStores(finishedWork) - ) { - // A store was mutated in an interleaved event. Render again, - // synchronously, to block further mutations. - exitStatus = renderRootSync(root, lanes); - // We assume the tree is now consistent because we didn't yield to any - // concurrent events. - renderWasConcurrent = false; - // Need to check the exit status again. - continue; - } - - // Check if something threw - if (exitStatus === RootErrored) { - const lanesThatJustErrored = lanes; - const errorRetryLanes = getLanesToRetrySynchronouslyOnError( + if (errorRetryLanes !== NoLanes) { + lanes = errorRetryLanes; + exitStatus = recoverFromConcurrentError( root, lanesThatJustErrored, + errorRetryLanes, ); - if (errorRetryLanes !== NoLanes) { - lanes = errorRetryLanes; - exitStatus = recoverFromConcurrentError( - root, - lanesThatJustErrored, - errorRetryLanes, - ); - renderWasConcurrent = false; - // Need to check the exit status again. - if (exitStatus !== RootErrored) { - // The root did not error this time. Restart the exit algorithm - // from the beginning. - // TODO: Refactor the exit algorithm to be less confusing. Maybe - // more branches + recursion instead of a loop. I think the only - // thing that causes it to be a loop is the RootDidNotComplete - // check. If that's true, then we don't need a loop/recursion - // at all. - continue; - } else { - // The root errored yet again. Proceed to commit the tree. - } + renderWasConcurrent = false; + // Need to check the exit status again. + if (exitStatus !== RootErrored) { + // The root did not error this time. Restart the exit algorithm + // from the beginning. + // TODO: Refactor the exit algorithm to be less confusing. Maybe + // more branches + recursion instead of a loop. I think the only + // thing that causes it to be a loop is the RootDidNotComplete + // check. If that's true, then we don't need a loop/recursion + // at all. + continue; + } else { + // The root errored yet again. Proceed to commit the tree. } } - if (exitStatus === RootFatalErrored) { - prepareFreshStack(root, NoLanes); - markRootSuspended( - root, - lanes, - NoLane, - workInProgressRootDidSkipSuspendedSiblings, - ); - break; - } - - // We now have a consistent tree. The next step is either to commit it, - // or, if something suspended, wait to commit it after a timeout. - finishConcurrentRender(root, exitStatus, finishedWork, lanes); } - break; - } while (true); - } + if (exitStatus === RootFatalErrored) { + prepareFreshStack(root, NoLanes); + // Since this is a fatal error, we're going to pretend we attempted + // the entire tree, to avoid scheduling a prerender. + const didAttemptEntireTree = true; + markRootSuspended(root, lanes, NoLane, didAttemptEntireTree); + break; + } + + // We now have a consistent tree. The next step is either to commit it, + // or, if something suspended, wait to commit it after a timeout. + finishConcurrentRender(root, exitStatus, finishedWork, lanes); + } + break; + } while (true); ensureRootIsScheduled(root); - return getContinuationForRoot(root, originalCallbackNode); } function recoverFromConcurrentError( @@ -1064,7 +1046,7 @@ function recoverFromConcurrentError( rootWorkInProgress.flags |= ForceClientRender; } - const exitStatus = renderRootSync(root, errorRetryLanes); + const exitStatus = renderRootSync(root, errorRetryLanes, false); if (exitStatus !== RootErrored) { // Successfully finished rendering on retry @@ -1148,11 +1130,13 @@ function finishConcurrentRender( // This is a transition, so we should exit without committing a // placeholder and without scheduling a timeout. Delay indefinitely // until we receive more data. + const didAttemptEntireTree = + !workInProgressRootDidSkipSuspendedSiblings; markRootSuspended( root, lanes, workInProgressDeferredLane, - workInProgressRootDidSkipSuspendedSiblings, + didAttemptEntireTree, ); return; } @@ -1208,11 +1192,13 @@ function finishConcurrentRender( // Don't bother with a very short suspense time. if (msUntilTimeout > 10) { + const didAttemptEntireTree = + !workInProgressRootDidSkipSuspendedSiblings; markRootSuspended( root, lanes, workInProgressDeferredLane, - workInProgressRootDidSkipSuspendedSiblings, + didAttemptEntireTree, ); const nextLanes = getNextLanes(root, NoLanes); @@ -1322,9 +1308,12 @@ function commitRootWhenReady( updatedLanes, suspendedRetryLanes, SUSPENDED_COMMIT, + completedRenderStartTime, + completedRenderEndTime, ), ); - markRootSuspended(root, lanes, spawnedLane, didSkipSuspendedSiblings); + const didAttemptEntireTree = !didSkipSuspendedSiblings; + markRootSuspended(root, lanes, spawnedLane, didAttemptEntireTree); return; } } @@ -1447,7 +1436,7 @@ function markRootSuspended( root: FiberRoot, suspendedLanes: Lanes, spawnedLane: Lane, - didSkipSuspendedSiblings: boolean, + didAttemptEntireTree: boolean, ) { // When suspending, we should always exclude lanes that were pinged or (more // rarely, since we try to avoid it) updated during the render phase. @@ -1456,110 +1445,7 @@ function markRootSuspended( suspendedLanes, workInProgressRootInterleavedUpdatedLanes, ); - _markRootSuspended( - root, - suspendedLanes, - spawnedLane, - didSkipSuspendedSiblings, - ); -} - -// This is the entry point for synchronous tasks that don't go -// through Scheduler -export function performSyncWorkOnRoot(root: FiberRoot, lanes: Lanes): null { - if ((executionContext & (RenderContext | CommitContext)) !== NoContext) { - throw new Error('Should not already be working.'); - } - - const didFlushPassiveEffects = flushPassiveEffects(); - if (didFlushPassiveEffects) { - // If passive effects were flushed, exit to the outer work loop in the root - // scheduler, so we can recompute the priority. - // TODO: We don't actually need this `ensureRootIsScheduled` call because - // this path is only reachable if the root is already part of the schedule. - // I'm including it only for consistency with the other exit points from - // this function. Can address in a subsequent refactor. - ensureRootIsScheduled(root); - return null; - } - - if (enableProfilerTimer && enableProfilerNestedUpdatePhase) { - syncNestedUpdateFlag(); - } - - let exitStatus = renderRootSync(root, lanes); - if ( - (disableLegacyMode || root.tag !== LegacyRoot) && - exitStatus === RootErrored - ) { - // If something threw an error, try rendering one more time. We'll render - // synchronously to block concurrent data mutations, and we'll includes - // all pending updates are included. If it still fails after the second - // attempt, we'll give up and commit the resulting tree. - const originallyAttemptedLanes = lanes; - const errorRetryLanes = getLanesToRetrySynchronouslyOnError( - root, - originallyAttemptedLanes, - ); - if (errorRetryLanes !== NoLanes) { - lanes = errorRetryLanes; - exitStatus = recoverFromConcurrentError( - root, - originallyAttemptedLanes, - errorRetryLanes, - ); - } - } - - if (exitStatus === RootFatalErrored) { - prepareFreshStack(root, NoLanes); - markRootSuspended(root, lanes, NoLane, false); - ensureRootIsScheduled(root); - return null; - } - - if (exitStatus === RootDidNotComplete) { - // The render unwound without completing the tree. This happens in special - // cases where need to exit the current render without producing a - // consistent tree or committing. - markRootSuspended( - root, - lanes, - workInProgressDeferredLane, - workInProgressRootDidSkipSuspendedSiblings, - ); - ensureRootIsScheduled(root); - return null; - } - - let renderEndTime = 0; - if (enableProfilerTimer && enableComponentPerformanceTrack) { - renderEndTime = now(); - } - - // We now have a consistent tree. Because this is a sync render, we - // will commit it even if something suspended. - const finishedWork: Fiber = (root.current.alternate: any); - root.finishedWork = finishedWork; - root.finishedLanes = lanes; - commitRoot( - root, - workInProgressRootRecoverableErrors, - workInProgressTransitions, - workInProgressRootDidIncludeRecursiveRenderUpdate, - workInProgressDeferredLane, - workInProgressRootInterleavedUpdatedLanes, - workInProgressSuspendedRetryLanes, - IMMEDIATE_COMMIT, - renderStartTime, - renderEndTime, - ); - - // Before exiting, make sure there's a callback scheduled for the next - // pending level. - ensureRootIsScheduled(root); - - return null; + _markRootSuspended(root, suspendedLanes, spawnedLane, didAttemptEntireTree); } export function flushRoot(root: FiberRoot, lanes: Lanes) { @@ -2101,7 +1987,12 @@ export function renderDidSuspendDelayIfPossible(): void { if ( !workInProgressRootDidSkipSuspendedSiblings && - !includesBlockingLane(workInProgressRootRenderLanes) + // Check if the root will be blocked from committing. + // TODO: Consider aligning this better with the rest of the logic. Maybe + // we should only set the exit status to RootSuspendedWithDelay if this + // condition is true? And remove the equivalent checks elsewhere. + (includesOnlyTransitions(workInProgressRootRenderLanes) || + getSuspenseHandler() === null) ) { // This render may not have originally been scheduled as a prerender, but // something suspended inside the visible part of the tree, which means we @@ -2127,11 +2018,12 @@ export function renderDidSuspendDelayIfPossible(): void { // pinged or updated while we were rendering. // TODO: Consider unwinding immediately, using the // SuspendedOnHydration mechanism. + const didAttemptEntireTree = false; markRootSuspended( workInProgressRoot, workInProgressRootRenderLanes, workInProgressDeferredLane, - workInProgressRootDidSkipSuspendedSiblings, + didAttemptEntireTree, ); } } @@ -2161,7 +2053,11 @@ export function renderHasNotSuspendedYet(): boolean { // TODO: Over time, this function and renderRootConcurrent have become more // and more similar. Not sure it makes sense to maintain forked paths. Consider // unifying them again. -function renderRootSync(root: FiberRoot, lanes: Lanes) { +function renderRootSync( + root: FiberRoot, + lanes: Lanes, + shouldYieldForPrerendering: boolean, +): RootExitStatus { const prevExecutionContext = executionContext; executionContext |= RenderContext; const prevDispatcher = pushDispatcher(root.containerInfo); @@ -2201,6 +2097,7 @@ function renderRootSync(root: FiberRoot, lanes: Lanes) { } let didSuspendInShell = false; + let exitStatus = workInProgressRootExitStatus; outer: do { try { if ( @@ -2222,16 +2119,37 @@ function renderRootSync(root: FiberRoot, lanes: Lanes) { // Selective hydration. An update flowed into a dehydrated tree. // Interrupt the current render so the work loop can switch to the // hydration lane. + // TODO: I think we might not need to reset the stack here; we can + // just yield and reset the stack when we re-enter the work loop, + // like normal. resetWorkInProgressStack(); - workInProgressRootExitStatus = RootDidNotComplete; + exitStatus = RootDidNotComplete; break outer; } case SuspendedOnImmediate: - case SuspendedOnData: { - if (!didSuspendInShell && getSuspenseHandler() === null) { + case SuspendedOnData: + case SuspendedOnDeprecatedThrowPromise: { + if (getSuspenseHandler() === null) { didSuspendInShell = true; } - // Intentional fallthrough + const reason = workInProgressSuspendedReason; + workInProgressSuspendedReason = NotSuspended; + workInProgressThrownValue = null; + throwAndUnwindWorkLoop(root, unitOfWork, thrownValue, reason); + if ( + enableSiblingPrerendering && + shouldYieldForPrerendering && + workInProgressRootIsPrerendering + ) { + // We've switched into prerendering mode. This implies that we + // suspended outside of a Suspense boundary, which means this + // render will be blocked from committing. Yield to the main + // thread so we can switch to prerendering using the concurrent + // work loop. + exitStatus = RootInProgress; + break outer; + } + break; } default: { // Unwind then continue with the normal work loop. @@ -2244,6 +2162,7 @@ function renderRootSync(root: FiberRoot, lanes: Lanes) { } } workLoopSync(); + exitStatus = workInProgressRootExitStatus; break; } catch (thrownValue) { handleThrow(root, thrownValue); @@ -2266,14 +2185,6 @@ function renderRootSync(root: FiberRoot, lanes: Lanes) { popDispatcher(prevDispatcher); popAsyncDispatcher(prevAsyncDispatcher); - if (workInProgress !== null) { - // This is a sync render, so we should have finished the whole tree. - throw new Error( - 'Cannot commit an incomplete root. This error is likely caused by a ' + - 'bug in React. Please file an issue.', - ); - } - if (__DEV__) { if (enableDebugTracing) { logRenderStopped(); @@ -2284,14 +2195,21 @@ function renderRootSync(root: FiberRoot, lanes: Lanes) { markRenderStopped(); } - // Set this to null to indicate there's no in-progress render. - workInProgressRoot = null; - workInProgressRootRenderLanes = NoLanes; + if (workInProgress !== null) { + // Did not complete the tree. This can happen if something suspended in + // the shell. + } else { + // Normal case. We completed the whole tree. - // It's safe to process the queue now that the render phase is complete. - finishQueueingConcurrentUpdates(); + // Set this to null to indicate there's no in-progress render. + workInProgressRoot = null; + workInProgressRootRenderLanes = NoLanes; + + // It's safe to process the queue now that the render phase is complete. + finishQueueingConcurrentUpdates(); + } - return workInProgressRootExitStatus; + return exitStatus; } // The work loop is an extremely hot path. Tell Closure not to inline it. @@ -2337,9 +2255,7 @@ function renderRootConcurrent(root: FiberRoot, lanes: Lanes) { // // If we were previously in prerendering mode, check if we received any new // data during an interleaved event. - if (workInProgressRootIsPrerendering) { - workInProgressRootIsPrerendering = checkIfRootIsPrerendering(root, lanes); - } + workInProgressRootIsPrerendering = checkIfRootIsPrerendering(root, lanes); } if (__DEV__) { @@ -3087,8 +3003,6 @@ function commitRoot( ReactSharedInternals.T = prevTransition; setCurrentUpdatePriority(previousUpdateLanePriority); } - - return null; } function commitRootImpl( @@ -3891,6 +3805,9 @@ function pingSuspendedRoot( // the logic of whether or not a root suspends once it completes. // TODO: If we're rendering sync either due to Sync, Batched or expired, // we should probably never restart. + // TODO: Attach different listeners depending on whether the listener was + // attached during prerendering. Prerender pings should not interrupt + // normal renders. // If we're suspended with delay, or if it's a retry, we'll always suspend // so we can always restart. diff --git a/packages/react-reconciler/src/__tests__/ReactDeferredValue-test.js b/packages/react-reconciler/src/__tests__/ReactDeferredValue-test.js index fd03ba7310f83..8ca142cb50517 100644 --- a/packages/react-reconciler/src/__tests__/ReactDeferredValue-test.js +++ b/packages/react-reconciler/src/__tests__/ReactDeferredValue-test.js @@ -420,6 +420,10 @@ describe('ReactDeferredValue', () => { // The initial value suspended, so we attempt the final value, which // also suspends. 'Suspend! [Final]', + + ...(gate('enableSiblingPrerendering') + ? ['Suspend! [Loading...]', 'Suspend! [Final]'] + : []), ]); expect(root).toMatchRenderedOutput(null); @@ -459,6 +463,10 @@ describe('ReactDeferredValue', () => { // The initial value suspended, so we attempt the final value, which // also suspends. 'Suspend! [Final]', + + ...(gate('enableSiblingPrerendering') + ? ['Suspend! [Loading...]', 'Suspend! [Final]'] + : []), ]); expect(root).toMatchRenderedOutput(null); @@ -533,6 +541,10 @@ describe('ReactDeferredValue', () => { // The initial value suspended, so we attempt the final value, which // also suspends. 'Suspend! [Final]', + + ...(gate('enableSiblingPrerendering') + ? ['Suspend! [Loading...]', 'Suspend! [Final]'] + : []), ]); expect(root).toMatchRenderedOutput(null); diff --git a/packages/react-reconciler/src/__tests__/ReactSiblingPrerendering-test.js b/packages/react-reconciler/src/__tests__/ReactSiblingPrerendering-test.js index 6c24fdc980285..235e8df2201de 100644 --- a/packages/react-reconciler/src/__tests__/ReactSiblingPrerendering-test.js +++ b/packages/react-reconciler/src/__tests__/ReactSiblingPrerendering-test.js @@ -349,6 +349,7 @@ describe('ReactSiblingPrerendering', () => {
}> +
@@ -370,10 +371,17 @@ describe('ReactSiblingPrerendering', () => { // is throttled because it's been less than a Just Noticeable Difference // since the outer fallback was committed. // - // In the meantime, we could choose to start prerendering B, but instead + // In the meantime, we could choose to start prerendering C, but instead // we wait for a JND to elapse and the commit to finish — it's not // worth discarding the work we've already done. - await waitForAll(['A', 'Suspend! [B]', 'Loading inner...']); + await waitForAll([ + 'A', + 'Suspend! [B]', + + // C is skipped because we're no longer in prerendering mode; there's + // a new fallback we can show. + 'Loading inner...', + ]); expect(root).toMatchRenderedOutput(
Loading outer...
); // Fire the timer to commit the outer fallback. @@ -385,8 +393,10 @@ describe('ReactSiblingPrerendering', () => {
, ); }); - // Once the outer fallback is committed, we can start prerendering B. - assertLog(gate('enableSiblingPrerendering') ? ['Suspend! [B]'] : []); + // Once the inner fallback is committed, we can start prerendering C. + assertLog( + gate('enableSiblingPrerendering') ? ['Suspend! [B]', 'Suspend! [C]'] : [], + ); }); it( @@ -469,4 +479,75 @@ describe('ReactSiblingPrerendering', () => { assertLog([]); }, ); + + it( + 'when a synchronous update suspends outside a boundary, the resulting' + + 'prerender is concurrent', + async () => { + function App() { + return ( + <> + + + + + + + ); + } + + const root = ReactNoop.createRoot(); + // Mount the root synchronously + ReactNoop.flushSync(() => root.render()); + + // Synchronously render everything until we suspend in the shell + assertLog(['A', 'B', 'Suspend! [Async]']); + + if (gate('enableSiblingPrerendering')) { + // The rest of the siblings begin to prerender concurrently. Notice + // that we don't unwind here; we pick up where we left off above. + await waitFor(['C']); + await waitFor(['D']); + } + + assertLog([]); + expect(root).toMatchRenderedOutput(null); + + await resolveText('Async'); + assertLog(['A', 'B', 'Async', 'C', 'D']); + expect(root).toMatchRenderedOutput('ABAsyncCD'); + }, + ); + + it('restart a suspended sync render if something suspends while prerendering the siblings', async () => { + function App() { + return ( + <> + + + + + + + ); + } + + const root = ReactNoop.createRoot(); + // Mount the root synchronously + ReactNoop.flushSync(() => root.render()); + + // Synchronously render everything until we suspend in the shell + assertLog(['A', 'B', 'Suspend! [Async]']); + + if (gate('enableSiblingPrerendering')) { + // The rest of the siblings begin to prerender concurrently + await waitFor(['C']); + } + + // While we're prerendering, Async resolves. We should unwind and + // start over, rather than continue prerendering D. + await resolveText('Async'); + assertLog(['A', 'B', 'Async', 'C', 'D']); + expect(root).toMatchRenderedOutput('ABAsyncCD'); + }); }); diff --git a/packages/react-reconciler/src/__tests__/ReactSuspenseyCommitPhase-test.js b/packages/react-reconciler/src/__tests__/ReactSuspenseyCommitPhase-test.js index 972416eddedb8..4dbba1bca21f0 100644 --- a/packages/react-reconciler/src/__tests__/ReactSuspenseyCommitPhase-test.js +++ b/packages/react-reconciler/src/__tests__/ReactSuspenseyCommitPhase-test.js @@ -239,11 +239,7 @@ describe('ReactSuspenseyCommitPhase', () => { expect(root).toMatchRenderedOutput(); }); - // @TODO This isn't actually ideal behavior. We would really want the commit to suspend - // even if it is forced to be sync because we don't want to FOUC but refactoring the sync - // pathway is too risky to land right now so we just accept that we can still FOUC in this - // very specific case. - it('does not suspend commit during urgent initial mount at the root when sync rendering', async () => { + it('does suspend commit during urgent initial mount at the root when sync rendering', async () => { const root = ReactNoop.createRoot(); await act(async () => { ReactNoop.flushSync(() => { @@ -252,19 +248,15 @@ describe('ReactSuspenseyCommitPhase', () => { }); assertLog(['Image requested [A]']); expect(getSuspenseyThingStatus('A')).toBe('pending'); - // We would expect this to be null if we did in fact suspend this commit - expect(root).toMatchRenderedOutput(); + // Suspend the initial mount + expect(root).toMatchRenderedOutput(null); resolveSuspenseyThing('A'); expect(getSuspenseyThingStatus('A')).toBe('fulfilled'); expect(root).toMatchRenderedOutput(); }); - // @TODO This isn't actually ideal behavior. We would really want the commit to suspend - // even if it is forced to be sync because we don't want to FOUC but refactoring the sync - // pathway is too risky to land right now so we just accept that we can still FOUC in this - // very specific case. - it('does not suspend commit during urgent update at the root when sync rendering', async () => { + it('does suspend commit during urgent update at the root when sync rendering', async () => { const root = ReactNoop.createRoot(); await act(() => resolveSuspenseyThing('A')); expect(getSuspenseyThingStatus('A')).toBe('fulfilled'); @@ -283,8 +275,8 @@ describe('ReactSuspenseyCommitPhase', () => { }); assertLog(['Image requested [B]']); expect(getSuspenseyThingStatus('B')).toBe('pending'); - // We would expect this to be hidden if we did in fact suspend this commit - expect(root).toMatchRenderedOutput(); + // Suspend and remain on previous screen + expect(root).toMatchRenderedOutput(); resolveSuspenseyThing('B'); expect(getSuspenseyThingStatus('B')).toBe('fulfilled');