diff --git a/.circleci/config.yml b/.circleci/config.yml index 9b222f2b6415..74dd57fb15b4 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -7,40 +7,11 @@ aliases: - &environment TZ: /usr/share/zoneinfo/America/Los_Angeles - - &restore_yarn_cache - restore_cache: - name: Restore yarn cache - keys: - - v1-yarn_cache-{{ arch }}-{{ checksum "yarn.lock" }} - - v1-yarn_cache-{{ arch }}- - - v1-yarn_cache- - - - &yarn_install - run: - name: Install dependencies - command: yarn install --frozen-lockfile --cache-folder ~/.cache/yarn - - - &yarn_install_retry - run: - name: Install dependencies (retry) - when: on_fail - command: yarn install --frozen-lockfile --cache-folder ~/.cache/yarn - - - &save_yarn_cache - save_cache: - name: Save yarn cache - key: v1-yarn_cache-{{ arch }}-{{ checksum "yarn.lock" }} - paths: - - ~/.cache/yarn - - &restore_yarn_cache_fixtures_dom restore_cache: name: Restore yarn cache for fixtures/dom keys: - - v1-yarn_cache-{{ arch }}-{{ checksum "yarn.lock" }}-fixtures/dom - - v1-yarn_cache-{{ arch }}-{{ checksum "yarn.lock" }} - - v1-yarn_cache-{{ arch }}- - - v1-yarn_cache- + - v2-yarn_cache-{{ arch }}-{{ checksum "yarn.lock" }}-fixtures/dom - &yarn_install_fixtures_dom run: @@ -58,51 +29,36 @@ aliases: - &save_yarn_cache_fixtures_dom save_cache: name: Save yarn cache for fixtures/dom - key: v1-yarn_cache-{{ arch }}-{{ checksum "yarn.lock" }}-fixtures/dom + key: v2-yarn_cache-{{ arch }}-{{ checksum "yarn.lock" }}-fixtures/dom paths: - ~/.cache/yarn - - &save_node_modules - save_cache: - name: Save node_modules cache - # Cache only for the current revision to prevent cache injections from - # malicious PRs. - key: v1-node_modules-{{ arch }}-{{ .Revision }} - paths: - - node_modules - - packages/eslint-plugin-react-hooks/node_modules - - packages/react-art/node_modules - - packages/react-client/node_modules - - packages/react-devtools-core/node_modules - - packages/react-devtools-extensions/node_modules - - packages/react-devtools-inline/node_modules - - packages/react-devtools-shared/node_modules - - packages/react-devtools-shell/node_modules - - packages/react-devtools-timeline/node_modules - - packages/react-devtools/node_modules - - packages/react-dom/node_modules - - packages/react-interactions/node_modules - - packages/react-native-renderer/node_modules - - packages/react-reconciler/node_modules - - packages/react-server-dom-relay/node_modules - - packages/react-server-dom-webpack/node_modules - - packages/react-server-native-relay/node_modules - - packages/react-server/node_modules - - packages/react-test-renderer/node_modules - - packages/react/node_modules - - packages/scheduler/node_modules - - - &restore_node_modules - restore_cache: - name: Restore node_modules cache - keys: - - v1-node_modules-{{ arch }}-{{ .Revision }} - - &TEST_PARALLELISM 20 - &attach_workspace at: build +commands: + setup_node_modules: + description: "Restore node_modules" + steps: + - restore_cache: + name: Restore yarn cache + keys: + - v2-yarn_cache-{{ arch }}-{{ checksum "yarn.lock" }} + - run: + name: Install dependencies + command: | + yarn install --frozen-lockfile --cache-folder ~/.cache/yarn + if [ $? -ne 0 ]; then + yarn install --frozen-lockfile --cache-folder ~/.cache/yarn + fi + - save_cache: + name: Save yarn cache + key: v2-yarn_cache-{{ arch }}-{{ checksum "yarn.lock" }} + paths: + - ~/.cache/yarn + # The CircleCI API doesn't yet support triggering a specific workflow, but it # does support triggering a pipeline. So as a workaround you can triggger the # entire pipeline and use parameters to disable everything except the workflow @@ -115,28 +71,13 @@ parameters: default: '' jobs: - setup: - docker: *docker - environment: *environment - steps: - - checkout - - run: - name: NodeJS Version - command: node --version - - *restore_yarn_cache - - *restore_node_modules - - *yarn_install - - *yarn_install_retry - - *save_yarn_cache - - *save_node_modules - yarn_lint: docker: *docker environment: *environment steps: - checkout - - *restore_node_modules + - setup_node_modules - run: node ./scripts/prettier/index - run: node ./scripts/tasks/eslint - run: ./scripts/circleci/check_license.sh @@ -150,7 +91,7 @@ jobs: steps: - checkout - - *restore_node_modules + - setup_node_modules - run: node ./scripts/tasks/flow-ci scrape_warning_messages: @@ -159,7 +100,7 @@ jobs: steps: - checkout - - *restore_node_modules + - setup_node_modules - run: command: | mkdir -p ./build @@ -169,14 +110,14 @@ jobs: paths: - build - yarn_build_combined: + yarn_build: docker: *docker environment: *environment parallelism: 40 steps: - checkout - - *restore_node_modules - - run: yarn build-combined + - setup_node_modules + - run: yarn build - persist_to_workspace: root: . paths: @@ -190,7 +131,7 @@ jobs: type: string steps: - checkout - - *restore_node_modules + - setup_node_modules - run: name: Download artifacts for revision command: | @@ -207,14 +148,36 @@ jobs: environment: *environment steps: - checkout - - *restore_node_modules + - setup_node_modules - run: name: Download artifacts for base revision + # TODO: The download-experimental-build.js script works by fetching + # artifacts from CI. CircleCI recently updated this endpoint to + # require an auth token. This is a problem for PR branches, where + # sizebot needs to run, because we don't want to leak the token to + # arbitrary code written by an outside contributor. + # + # This only affects PR branches. CI workflows that run on the main + # branch are allowed to access environment variables, because only those + # with push access can land code in main. + # + # As a temporary workaround, we'll fetch the assets from a mirror. + # Need to figure out a longer term solution for this. + # + # Original code + # + # command: | + # git fetch origin main + # cd ./scripts/release && yarn && cd ../../ + # scripts/release/download-experimental-build.js --commit=$(git merge-base HEAD origin/main) --allowBrokenCI + # mv ./build ./base-build + # + # Workaround. Fetch the artifacts from react-builds.vercel.app. This + # is the same app that hosts the sizebot diff previews. command: | - git fetch origin main - cd ./scripts/release && yarn && cd ../../ - scripts/release/download-experimental-build.js --commit=$(git merge-base HEAD origin/main) --allowBrokenCI + curl -L --retry 60 --retry-delay 10 --retry-max-time 600 https://react-builds.vercel.app/api/commits/$(git merge-base HEAD origin/main)/artifacts/build.tgz | tar -xz mv ./build ./base-build + - run: # TODO: The `download-experimental-build` script copies the npm # packages into the `node_modules` directory. This is a historical @@ -235,7 +198,7 @@ jobs: - checkout - attach_workspace: at: . - - *restore_node_modules + - setup_node_modules - run: echo "<< pipeline.git.revision >>" >> build/COMMIT_SHA # Compress build directory into a single tarball for easy download - run: tar -zcvf ./build.tgz ./build @@ -254,7 +217,7 @@ jobs: - attach_workspace: at: . - run: echo "<< pipeline.git.revision >>" >> build/COMMIT_SHA - - *restore_node_modules + - setup_node_modules - run: command: node ./scripts/tasks/danger @@ -265,7 +228,7 @@ jobs: - checkout - attach_workspace: at: . - - *restore_node_modules + - setup_node_modules - run: environment: RELEASE_CHANNEL: experimental @@ -280,7 +243,7 @@ jobs: - checkout - attach_workspace: at: . - - *restore_node_modules + - setup_node_modules - run: name: Playwright install deps command: | @@ -302,7 +265,7 @@ jobs: - checkout - attach_workspace: at: . - - *restore_node_modules + - setup_node_modules - run: ./scripts/circleci/download_devtools_regression_build.js << parameters.version >> --replaceBuild - run: node ./scripts/jest/jest-cli.js --build --project devtools --release-channel=experimental --reactVersion << parameters.version >> --ci @@ -317,7 +280,7 @@ jobs: - checkout - attach_workspace: at: . - - *restore_node_modules + - setup_node_modules - run: name: Playwright install deps command: | @@ -341,7 +304,7 @@ jobs: - checkout - attach_workspace: at: . - - *restore_node_modules + - setup_node_modules - run: yarn lint-build yarn_check_release_dependencies: @@ -351,7 +314,7 @@ jobs: - checkout - attach_workspace: at: . - - *restore_node_modules + - setup_node_modules - run: yarn check-release-dependencies @@ -361,7 +324,7 @@ jobs: steps: - checkout - attach_workspace: *attach_workspace - - *restore_node_modules + - setup_node_modules - run: name: Search build artifacts for unminified errors command: | @@ -374,7 +337,7 @@ jobs: steps: - checkout - attach_workspace: *attach_workspace - - *restore_node_modules + - setup_node_modules - run: name: Confirm generated inline Fizz runtime is up to date command: | @@ -390,7 +353,7 @@ jobs: type: string steps: - checkout - - *restore_node_modules + - setup_node_modules - run: yarn test <> --ci yarn_test_build: @@ -404,7 +367,7 @@ jobs: - checkout - attach_workspace: at: . - - *restore_node_modules + - setup_node_modules - run: yarn test --build <> --ci RELEASE_CHANNEL_stable_yarn_test_dom_fixtures: @@ -414,7 +377,7 @@ jobs: - checkout - attach_workspace: at: . - - *restore_node_modules + - setup_node_modules - *restore_yarn_cache_fixtures_dom - *yarn_install_fixtures_dom - *yarn_install_fixtures_dom_retry @@ -425,7 +388,7 @@ jobs: RELEASE_CHANNEL: stable working_directory: fixtures/dom command: | - yarn prestart + yarn predev yarn test --maxWorkers=2 test_fuzz: @@ -433,7 +396,7 @@ jobs: environment: *environment steps: - checkout - - *restore_node_modules + - setup_node_modules - run: name: Run fuzz tests command: | @@ -452,7 +415,7 @@ jobs: environment: *environment steps: - checkout - - *restore_node_modules + - setup_node_modules - run: name: Run publish script command: | @@ -465,27 +428,29 @@ jobs: workflows: version: 2 - # New workflow that will replace "stable" and "experimental" build_and_test: unless: << pipeline.parameters.prerelease_commit_sha >> jobs: - - setup: + - yarn_flow: filters: branches: ignore: - builds/facebook-www - - yarn_flow: - requires: - - setup - check_generated_fizz_runtime: - requires: - - setup + filters: + branches: + ignore: + - builds/facebook-www - yarn_lint: - requires: - - setup + filters: + branches: + ignore: + - builds/facebook-www - yarn_test: - requires: - - setup + filters: + branches: + ignore: + - builds/facebook-www matrix: parameters: args: @@ -508,19 +473,23 @@ workflows: # TODO: Test more persistent configurations? - '-r=stable --env=development --persistent' - '-r=experimental --env=development --persistent' - - yarn_build_combined: - requires: - - setup + - yarn_build: + filters: + branches: + ignore: + - builds/facebook-www - scrape_warning_messages: - requires: - - setup + filters: + branches: + ignore: + - builds/facebook-www - process_artifacts_combined: requires: - scrape_warning_messages - - yarn_build_combined + - yarn_build - yarn_test_build: requires: - - yarn_build_combined + - yarn_build matrix: parameters: args: @@ -551,8 +520,7 @@ workflows: branches: ignore: - main - requires: - - setup + - builds/facebook-www - sizebot: filters: branches: @@ -560,22 +528,22 @@ workflows: - main requires: - download_base_build_for_sizebot - - yarn_build_combined + - yarn_build - yarn_lint_build: requires: - - yarn_build_combined + - yarn_build - yarn_check_release_dependencies: requires: - - yarn_build_combined + - yarn_build - check_error_codes: requires: - - yarn_build_combined + - yarn_build - RELEASE_CHANNEL_stable_yarn_test_dom_fixtures: requires: - - yarn_build_combined + - yarn_build - build_devtools_and_process_artifacts: requires: - - yarn_build_combined + - yarn_build - run_devtools_e2e_tests: requires: - build_devtools_and_process_artifacts @@ -591,10 +559,7 @@ workflows: only: - main jobs: - - setup - - test_fuzz: - requires: - - setup + - test_fuzz devtools_regression_tests: unless: << pipeline.parameters.prerelease_commit_sha >> @@ -607,10 +572,7 @@ workflows: only: - main jobs: - - setup - download_build: - requires: - - setup revision: << pipeline.git.revision >> - build_devtools_and_process_artifacts: requires: @@ -642,11 +604,8 @@ workflows: publish_preleases: when: << pipeline.parameters.prerelease_commit_sha >> jobs: - - setup - publish_prerelease: name: Publish to Next channel - requires: - - setup commit_sha: << pipeline.parameters.prerelease_commit_sha >> release_channel: stable dist_tag: "next" @@ -674,11 +633,8 @@ workflows: only: - main jobs: - - setup - publish_prerelease: name: Publish to Next channel - requires: - - setup commit_sha: << pipeline.git.revision >> release_channel: stable dist_tag: "next" diff --git a/.codesandbox/ci.json b/.codesandbox/ci.json index 897513de495f..f395115a4bc2 100644 --- a/.codesandbox/ci.json +++ b/.codesandbox/ci.json @@ -1,7 +1,7 @@ { "packages": ["packages/react", "packages/react-dom", "packages/scheduler"], "buildCommand": "download-build-in-codesandbox-ci", - "node": "14", + "node": "18", "publishDirectory": { "react": "build/oss-experimental/react", "react-dom": "build/oss-experimental/react-dom", diff --git a/.eslintrc.js b/.eslintrc.js index c14105b0c08a..bcab1b2756c1 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -8,15 +8,18 @@ const { const restrictedGlobals = require('confusing-browser-globals'); const OFF = 0; +const WARNING = 1; const ERROR = 2; module.exports = { - extends: ['fbjs', 'prettier'], + extends: ['prettier'], // Stop ESLint from looking for a configuration file in parent folders root: true, plugins: [ + 'babel', + 'ft-flow', 'jest', 'no-for-of-loops', 'no-function-declare-after-return', @@ -24,7 +27,7 @@ module.exports = { 'react-internal', ], - parser: 'babel-eslint', + parser: 'hermes-eslint', parserOptions: { ecmaVersion: 9, sourceType: 'script', @@ -33,6 +36,190 @@ module.exports = { // We're stricter than the default config, mostly. We'll override a few rules // and then enable some React specific ones. rules: { + 'ft-flow/array-style-complex-type': [OFF, 'verbose'], + 'ft-flow/array-style-simple-type': [OFF, 'verbose'], // TODO should be WARNING + 'ft-flow/boolean-style': ERROR, + 'ft-flow/no-dupe-keys': ERROR, + 'ft-flow/no-primitive-constructor-types': ERROR, + 'ft-flow/no-types-missing-file-annotation': OFF, // TODO should be ERROR + 'ft-flow/no-unused-expressions': ERROR, + // 'ft-flow/no-weak-types': WARNING, + // 'ft-flow/require-valid-file-annotation': ERROR, + + 'no-cond-assign': OFF, + 'no-constant-condition': OFF, + 'no-control-regex': OFF, + 'no-debugger': ERROR, + 'no-dupe-args': ERROR, + 'no-dupe-keys': ERROR, + 'no-duplicate-case': WARNING, + 'no-empty-character-class': WARNING, + 'no-empty': OFF, + 'no-ex-assign': WARNING, + 'no-extra-boolean-cast': WARNING, + 'no-func-assign': ERROR, + 'no-invalid-regexp': WARNING, + 'no-irregular-whitespace': WARNING, + 'no-negated-in-lhs': ERROR, + 'no-obj-calls': ERROR, + 'no-regex-spaces': WARNING, + 'no-sparse-arrays': ERROR, + 'no-unreachable': ERROR, + 'use-isnan': ERROR, + 'valid-jsdoc': OFF, + 'block-scoped-var': OFF, + complexity: OFF, + 'default-case': OFF, + 'guard-for-in': OFF, + 'no-alert': OFF, + 'no-caller': ERROR, + 'no-case-declarations': OFF, + 'no-div-regex': OFF, + 'no-else-return': OFF, + 'no-empty-pattern': WARNING, + 'no-eq-null': OFF, + 'no-eval': ERROR, + 'no-extend-native': WARNING, + 'no-extra-bind': WARNING, + 'no-fallthrough': WARNING, + 'no-implicit-coercion': OFF, + 'no-implied-eval': ERROR, + 'no-invalid-this': OFF, + 'no-iterator': OFF, + 'no-labels': [ERROR, {allowLoop: true, allowSwitch: true}], + 'no-lone-blocks': WARNING, + 'no-loop-func': OFF, + 'no-magic-numbers': OFF, + 'no-multi-str': ERROR, + 'no-native-reassign': [ERROR, {exceptions: ['Map', 'Set']}], + 'no-new-func': ERROR, + 'no-new': WARNING, + 'no-new-wrappers': WARNING, + 'no-octal-escape': WARNING, + 'no-octal': WARNING, + 'no-param-reassign': OFF, + 'no-process-env': OFF, + 'no-proto': ERROR, + 'no-redeclare': OFF, // TODO should be WARNING? + 'no-return-assign': OFF, + 'no-script-url': ERROR, + 'no-self-compare': WARNING, + 'no-sequences': WARNING, + 'no-throw-literal': ERROR, + 'no-useless-call': WARNING, + 'no-void': OFF, + 'no-warning-comments': OFF, + 'no-with': OFF, + radix: WARNING, + 'vars-on-top': OFF, + yoda: OFF, + 'init-declarations': OFF, + 'no-catch-shadow': ERROR, + 'no-delete-var': ERROR, + 'no-label-var': WARNING, + 'no-shadow-restricted-names': WARNING, + 'no-undef-init': OFF, + 'no-undef': ERROR, + 'no-undefined': OFF, + 'callback-return': OFF, + 'global-require': OFF, + 'handle-callback-err': OFF, + 'no-mixed-requires': OFF, + 'no-new-require': OFF, + 'no-path-concat': OFF, + 'no-process-exit': OFF, + 'no-restricted-modules': OFF, + 'no-sync': OFF, + camelcase: [OFF, {properties: 'always'}], + 'consistent-this': [OFF, 'self'], + 'func-names': OFF, + 'func-style': [OFF, 'declaration'], + 'id-length': OFF, + 'id-match': OFF, + 'max-depth': OFF, + 'max-nested-callbacks': OFF, + 'max-params': OFF, + 'max-statements': OFF, + 'new-cap': OFF, + 'newline-after-var': OFF, + 'no-array-constructor': ERROR, + 'no-continue': OFF, + 'no-inline-comments': OFF, + 'no-lonely-if': OFF, + 'no-negated-condition': OFF, + 'no-nested-ternary': OFF, + 'no-new-object': WARNING, + 'no-plusplus': OFF, + 'no-ternary': OFF, + 'no-underscore-dangle': OFF, + 'no-unneeded-ternary': WARNING, + 'one-var': [WARNING, {initialized: 'never'}], + 'operator-assignment': [WARNING, 'always'], + 'require-jsdoc': OFF, + 'sort-vars': OFF, + 'spaced-comment': [ + OFF, + 'always', + {exceptions: ['jshint', 'jslint', 'eslint', 'global']}, + ], + 'constructor-super': ERROR, + 'no-class-assign': WARNING, + 'no-const-assign': ERROR, + 'no-dupe-class-members': ERROR, + 'no-this-before-super': ERROR, + 'object-shorthand': OFF, + 'prefer-const': OFF, + 'prefer-spread': OFF, + 'prefer-reflect': OFF, + 'prefer-template': OFF, + 'require-yield': OFF, + 'babel/generator-star-spacing': OFF, + 'babel/new-cap': OFF, + 'babel/array-bracket-spacing': OFF, + 'babel/object-curly-spacing': OFF, + 'babel/object-shorthand': OFF, + 'babel/arrow-parens': OFF, + 'babel/no-await-in-loop': OFF, + 'babel/flow-object-type': OFF, + 'react/display-name': OFF, + 'react/forbid-prop-types': OFF, + 'react/jsx-closing-bracket-location': OFF, + 'react/jsx-curly-spacing': OFF, + 'react/jsx-equals-spacing': WARNING, + 'react/jsx-filename-extension': OFF, + 'react/jsx-first-prop-new-line': OFF, + 'react/jsx-handler-names': OFF, + 'react/jsx-indent': OFF, + 'react/jsx-indent-props': OFF, + 'react/jsx-key': OFF, + 'react/jsx-max-props-per-line': OFF, + 'react/jsx-no-bind': OFF, + 'react/jsx-no-duplicate-props': ERROR, + 'react/jsx-no-literals': OFF, + 'react/jsx-no-target-blank': OFF, + 'react/jsx-pascal-case': OFF, + 'react/jsx-sort-props': OFF, + 'react/jsx-uses-vars': ERROR, + 'react/no-comment-textnodes': OFF, + 'react/no-danger': OFF, + 'react/no-deprecated': OFF, + 'react/no-did-mount-set-state': OFF, + 'react/no-did-update-set-state': OFF, + 'react/no-direct-mutation-state': OFF, + 'react/no-multi-comp': OFF, + 'react/no-render-return-value': OFF, + 'react/no-set-state': OFF, + 'react/no-string-refs': OFF, + 'react/no-unknown-property': OFF, + 'react/prefer-es6-class': OFF, + 'react/prefer-stateless-function': OFF, + 'react/prop-types': OFF, + 'react/require-extension': OFF, + 'react/require-optimization': OFF, + 'react/require-render-return': OFF, + 'react/sort-comp': OFF, + 'react/sort-prop-types': OFF, + 'accessor-pairs': OFF, 'brace-style': [ERROR, '1tbs'], 'consistent-return': OFF, @@ -51,7 +238,6 @@ module.exports = { 'no-restricted-globals': [ERROR].concat(restrictedGlobals), 'no-restricted-syntax': [ERROR, 'WithStatement'], 'no-shadow': ERROR, - 'no-unused-expressions': ERROR, 'no-unused-vars': [ERROR, {args: 'none'}], 'no-use-before-define': OFF, 'no-useless-concat': OFF, @@ -74,8 +260,6 @@ module.exports = { // deal. But I turned it off because loading the plugin causes some obscure // syntax error and it didn't seem worth investigating. 'max-len': OFF, - // Prettier forces semicolons in a few places - 'flowtype/object-type-delimiter': OFF, // React & JSX // Our transforms set this automatically @@ -141,6 +325,7 @@ module.exports = { 'packages/react-native-renderer/**/*.js', 'packages/eslint-plugin-react-hooks/**/*.js', 'packages/jest-react/**/*.js', + 'packages/internal-test-utils/**/*.js', 'packages/**/__tests__/*.js', 'packages/**/npm/*.js', ], @@ -166,7 +351,7 @@ module.exports = { // We apply these settings to the source files that get compiled. // They can use all features including JSX (but shouldn't use `var`). files: esNextPaths, - parser: 'babel-eslint', + parser: 'hermes-eslint', parserOptions: { ecmaVersion: 8, sourceType: 'module', @@ -204,14 +389,6 @@ module.exports = { ERROR, {isProductionUserAppCode: false}, ], - - // Disable accessibility checks - 'jsx-a11y/aria-role': OFF, - 'jsx-a11y/no-noninteractive-element-interactions': OFF, - 'jsx-a11y/no-static-element-interactions': OFF, - 'jsx-a11y/role-has-required-aria-props': OFF, - 'jsx-a11y/no-noninteractive-tabindex': OFF, - 'jsx-a11y/tabindex-no-positive': OFF, }, }, { @@ -253,10 +430,71 @@ module.exports = { }, ], + env: { + browser: true, + es6: true, + node: true, + jest: true, + }, + globals: { + $Call: 'readonly', + $ElementType: 'readonly', + $Flow$ModuleRef: 'readonly', + $FlowFixMe: 'readonly', + $Keys: 'readonly', + $NonMaybeType: 'readonly', + $PropertyType: 'readonly', + $ReadOnly: 'readonly', + $ReadOnlyArray: 'readonly', + $Shape: 'readonly', + AnimationFrameID: 'readonly', + // For Flow type annotation. Only `BigInt` is valid at runtime. + bigint: 'readonly', + BigInt: 'readonly', + Class: 'readonly', + ClientRect: 'readonly', + CopyInspectedElementPath: 'readonly', + DOMHighResTimeStamp: 'readonly', + EventListener: 'readonly', + Iterable: 'readonly', + Iterator: 'readonly', + JSONValue: 'readonly', + JSResourceReference: 'readonly', + MouseEventHandler: 'readonly', + PropagationPhases: 'readonly', + PropertyDescriptor: 'readonly', + React$AbstractComponent: 'readonly', + React$Component: 'readonly', + React$ComponentType: 'readonly', + React$Config: 'readonly', + React$Context: 'readonly', + React$Element: 'readonly', + React$ElementConfig: 'readonly', + React$ElementProps: 'readonly', + React$ElementRef: 'readonly', + React$ElementType: 'readonly', + React$Key: 'readonly', + React$Node: 'readonly', + React$Portal: 'readonly', + React$Ref: 'readonly', + React$StatelessFunctionalComponent: 'readonly', + ReadableStreamController: 'readonly', + RequestInfo: 'readonly', + RequestOptions: 'readonly', + ResponseState: 'readonly', + StoreAsGlobal: 'readonly', + symbol: 'readonly', + SyntheticEvent: 'readonly', + SyntheticMouseEvent: 'readonly', + Thenable: 'readonly', + TimeoutID: 'readonly', + WheelEventHandler: 'readonly', + spyOnDev: 'readonly', spyOnDevAndProd: 'readonly', spyOnProd: 'readonly', + __DEV__: 'readonly', __EXPERIMENTAL__: 'readonly', __EXTENSION__: 'readonly', __PROFILE__: 'readonly', diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 21ff46dbab29..23fbc65ab576 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -9,7 +9,7 @@ 3. If you've fixed a bug or added code that should be tested, add tests! 4. Ensure the test suite passes (`yarn test`). Tip: `yarn test --watch TestName` is helpful in development. 5. Run `yarn test --prod` to test in the production environment. It supports the same options as `yarn test`. - 6. If you need a debugger, run `yarn debug-test --watch TestName`, open `chrome://inspect`, and press "Inspect". + 6. If you need a debugger, run `yarn test --debug --watch TestName`, open `chrome://inspect`, and press "Inspect". 7. Format your code with [prettier](https://github.com/prettier/prettier) (`yarn prettier`). 8. Make sure your code lints (`yarn lint`). Tip: `yarn linc` to only check changed files. 9. Run the [Flow](https://flowtype.org/) type checks (`yarn flow`). diff --git a/.github/workflows/commit_artifacts.yml b/.github/workflows/commit_artifacts.yml index 9505643dc3fe..fe6da856587d 100644 --- a/.github/workflows/commit_artifacts.yml +++ b/.github/workflows/commit_artifacts.yml @@ -1,4 +1,4 @@ -name: Commit Artifacts for Facebook WWW +name: Commit Artifacts for Facebook WWW and fbsource on: push: @@ -8,17 +8,13 @@ jobs: download_artifacts: runs-on: ubuntu-latest steps: - - uses: actions/setup-node@v3 - with: - node-version: 18.x - - run: npm init -y - - run: npm install node-fetch@2 - name: Download and unzip artifacts uses: actions/github-script@v6 + env: + CIRCLECI_TOKEN: ${{secrets.CIRCLECI_TOKEN_DIFFTRAIN}} with: script: | const cp = require('child_process'); - const fetch = require('node-fetch'); function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); @@ -64,10 +60,10 @@ jobs: const ciBuildId = /\/facebook\/react\/([0-9]+)/.exec( status.target_url, )[1]; - console.log(`CircleCI build id found: ${ciBuildId}`); if (Number.parseInt(ciBuildId, 10) + '' === ciBuildId) { artifactsUrl = `https://circleci.com/api/v1.1/project/github/facebook/react/${ciBuildId}/artifacts`; + console.log(`Found artifactsUrl: ${artifactsUrl}`); break spinloop; } else { throw new Error(`${ciBuildId} isn't a number`); @@ -86,13 +82,21 @@ jobs: await sleep(60_000); } if (artifactsUrl != null) { - const res = await fetch(artifactsUrl); + const {CIRCLECI_TOKEN} = process.env; + const res = await fetch(artifactsUrl, { + headers: { + 'Circle-Token': CIRCLECI_TOKEN + } + }); const data = await res.json(); + if (!Array.isArray(data) && data.message != null) { + throw `CircleCI returned: ${data.message}`; + } for (const artifact of data) { if (artifact.path === 'build.tgz') { console.log(`Downloading and unzipping ${artifact.url}`); await execHelper( - `curl -L ${artifact.url} | tar -xvz` + `curl -L ${artifact.url} -H "Circle-Token: ${CIRCLECI_TOKEN}" | tar -xvz` ); } } @@ -102,9 +106,9 @@ jobs: - name: Strip @license from eslint plugin and react-refresh run: | sed -i -e 's/ @license React*//' \ - build/oss-stable/eslint-plugin-react-hooks/cjs/eslint-plugin-react-hooks.development.js \ - build/oss-stable/react-refresh/cjs/react-refresh-babel.development.js - - name: Move relevant files into compiled + build/oss-experimental/eslint-plugin-react-hooks/cjs/eslint-plugin-react-hooks.development.js \ + build/oss-experimental/react-refresh/cjs/react-refresh-babel.development.js + - name: Move relevant files for React in www into compiled run: | mkdir -p ./compiled mkdir -p ./compiled/facebook-www @@ -117,7 +121,7 @@ jobs: mv build/WARNINGS ./compiled/facebook-www/WARNINGS # Copy eslint-plugin-react-hooks into facebook-www - mv build/oss-stable/eslint-plugin-react-hooks/cjs/eslint-plugin-react-hooks.development.js \ + mv build/oss-experimental/eslint-plugin-react-hooks/cjs/eslint-plugin-react-hooks.development.js \ ./compiled/facebook-www/eslint-plugin-react-hooks.js # Copy unstable_server-external-runtime.js into facebook-www @@ -125,20 +129,44 @@ jobs: ./compiled/facebook-www/unstable_server-external-runtime.js # Copy react-refresh-babel.development.js into babel-plugin-react-refresh - mv build/oss-stable/react-refresh/cjs/react-refresh-babel.development.js \ + mv build/oss-experimental/react-refresh/cjs/react-refresh-babel.development.js \ ./compiled/babel-plugin-react-refresh/index.js ls -R ./compiled - - name: Add REVISION files + - name: Move relevant files for React in fbsource into compiled-rn + run: | + BASE_FOLDER='compiled-rn/facebook-fbsource/xplat/js' + mkdir -p ${BASE_FOLDER}/react-native-github/Libraries/Renderer/ + mkdir -p ${BASE_FOLDER}/RKJSModules/vendor/{scheduler,react,react-is,react-test-renderer}/ + + # Move React Native renderer + mv build/react-native/implementations/ $BASE_FOLDER/react-native-github/Libraries/Renderer/ + mv build/react-native/shims/ $BASE_FOLDER/react-native-github/Libraries/Renderer/ + mv build/facebook-react-native/scheduler/cjs/ $BASE_FOLDER/RKJSModules/vendor/scheduler/ + mv build/facebook-react-native/react/cjs/ $BASE_FOLDER/RKJSModules/vendor/react/ + mv build/facebook-react-native/react-is/cjs/ $BASE_FOLDER/RKJSModules/vendor/react-is/ + mv build/facebook-react-native/react-test-renderer/cjs/ $BASE_FOLDER/RKJSModules/vendor/react-test-renderer/ + + # Delete OSS renderer. OSS renderer is synced through internal script. + RENDERER_FOLDER=$BASE_FOLDER/react-native-github/Libraries/Renderer/implementations/ + rm $RENDERER_FOLDER/ReactFabric-{dev,prod,profiling}.js + rm $RENDERER_FOLDER/ReactNativeRenderer-{dev,prod,profiling}.js + + ls -R ./compiled + - name: Add REVISION file run: | echo ${{ github.sha }} >> ./compiled/facebook-www/REVISION - cp ./compiled/facebook-www/REVISION ./compiled/facebook-www/REVISION_TRANSFORMS + echo ${{ github.sha }} >> ./compiled-rn/facebook-fbsource/xplat/js/react-native-github/Libraries/Renderer/REVISION - uses: actions/upload-artifact@v3 with: name: compiled path: compiled/ + - uses: actions/upload-artifact@v3 + with: + name: compiled-rn + path: compiled-rn/ - commit_artifacts: + commit_www_artifacts: needs: download_artifacts runs-on: ubuntu-latest steps: @@ -152,14 +180,49 @@ jobs: name: compiled path: compiled/ - run: git status -u + - name: Check if only the REVISION file has changed + id: check_should_commit + run: | + if git status --porcelain | grep -qv '/REVISION$'; then + echo "should_commit=true" >> "$GITHUB_OUTPUT" + else + echo "should_commit=false" >> "$GITHUB_OUTPUT" + fi - name: Commit changes to branch + if: steps.check_should_commit.outputs.should_commit == 'true' uses: stefanzweifel/git-auto-commit-action@v4 with: commit_message: | ${{ github.event.head_commit.message }} - DiffTrain build for `${{ github.sha }}` + DiffTrain build for [${{ github.sha }}](https://github.com/facebook/react/commit/${{ github.sha }}) branch: builds/facebook-www commit_user_name: ${{ github.actor }} commit_user_email: ${{ github.actor }}@users.noreply.github.com create_branch: true + + commit_fbsource_artifacts: + needs: download_artifacts + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + ref: main + repository: facebook/react-fbsource-import + token: ${{secrets.FBSOURCE_SYNC_PUSH_TOKEN}} + - name: Ensure clean directory + run: rm -rf compiled-rn + - uses: actions/download-artifact@v3 + with: + name: compiled-rn + path: compiled-rn/ + - run: git status -u + - name: Commit changes to branch + uses: stefanzweifel/git-auto-commit-action@v4 + with: + commit_message: | + ${{ github.event.head_commit.message }} + + DiffTrain build for commit https://github.com/facebook/react/commit/${{ github.sha }}. + commit_user_name: ${{ github.actor }} + commit_user_email: ${{ github.actor }}@users.noreply.github.com diff --git a/.nvmrc b/.nvmrc index 4ec320b217c7..e329619ca224 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -v14.17.6 +v16.19.1 diff --git a/.prettierrc.js b/.prettierrc.js index 6e5dcb710f0f..4f7ef193130c 100644 --- a/.prettierrc.js +++ b/.prettierrc.js @@ -8,8 +8,8 @@ module.exports = { jsxBracketSameLine: true, trailingComma: 'es5', printWidth: 80, - parser: 'babel', - + parser: 'flow', + arrowParens: 'avoid', overrides: [ { files: esNextPaths, diff --git a/CHANGELOG.md b/CHANGELOG.md index f180bf6be8ca..8f6df415e32a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,7 +17,7 @@ ### Server Components (Experimental) -* Add support for `useId()` inside Server Components. ([@gnoff](https://github.com/gnoff)) in [#24172](https://github.com/facebook/react/pull/24172) +* Add support for `useId()` inside Server Components. ([@gnoff](https://github.com/gnoff) in [#24172](https://github.com/facebook/react/pull/24172)) ## 18.1.0 (April 26, 2022) @@ -102,6 +102,10 @@ The existing `renderToString` method keeps working but is discouraged. * **Layout Effects with Suspense**: When a tree re-suspends and reverts to a fallback, React will now clean up layout effects, and then re-create them when the content inside the boundary is shown again. This fixes an issue which prevented component libraries from correctly measuring layout when used with Suspense. * **New JS Environment Requirements**: React now depends on modern browsers features including `Promise`, `Symbol`, and `Object.assign`. If you support older browsers and devices such as Internet Explorer which do not provide modern browser features natively or have non-compliant implementations, consider including a global polyfill in your bundled application. +### Scheduler (Experimental) + +* Remove unstable `scheduler/tracing` API + ## Notable Changes ### React @@ -193,6 +197,10 @@ The existing `renderToString` method keeps working but is discouraged. * Fix a mistake in the Node loader. ([#22537](https://github.com/facebook/react/pull/22537) by [@btea](https://github.com/btea)) * Use `globalThis` instead of `window` for edge environments. ([#22777](https://github.com/facebook/react/pull/22777) by [@huozhi](https://github.com/huozhi)) +### Scheduler (Experimental) + +* Remove unstable `scheduler/tracing` API ([#20037](https://github.com/facebook/react/pull/20037) by [@bvaughn](https://github.com/bvaughn)) + ## 17.0.2 (March 22, 2021) ### React DOM diff --git a/dangerfile.js b/dangerfile.js index b0c95d27b0f3..e29426afda7a 100644 --- a/dangerfile.js +++ b/dangerfile.js @@ -46,7 +46,6 @@ const CRITICAL_ARTIFACT_PATHS = new Set([ 'oss-experimental/react-dom/cjs/react-dom.production.min.js', 'facebook-www/ReactDOM-prod.classic.js', 'facebook-www/ReactDOM-prod.modern.js', - 'facebook-www/ReactDOMForked-prod.classic.js', ]); const kilobyteFormatter = new Intl.NumberFormat('en', { @@ -98,7 +97,7 @@ function row(result, baseSha, headSha) { return rowArr.join(' | '); } -(async function() { +(async function () { // Use git locally to grab the commit which represents the place // where the branches differ @@ -242,8 +241,9 @@ Comparing: ${baseSha}...${headSha} ## Critical size changes -Includes critical production bundles, as well as any change greater than ${CRITICAL_THRESHOLD * - 100}%: +Includes critical production bundles, as well as any change greater than ${ + CRITICAL_THRESHOLD * 100 + }%: ${header} ${criticalResults.join('\n')} diff --git a/fixtures/art/package.json b/fixtures/art/package.json index 82e84bb49a4f..ff3229769bdb 100644 --- a/fixtures/art/package.json +++ b/fixtures/art/package.json @@ -5,12 +5,13 @@ "@babel/preset-env": "^7.10.4", "@babel/preset-react": "^7.10.4", "babel-loader": "^8.1.0", - "react": "link:../../build/node_modules/react", - "react-art": "link:../../build/node_modules/react-art/", - "react-dom": "link:../../build/node_modules/react-dom", + "react": "^18.2.0", + "react-art": "^18.2.0", + "react-dom": "^18.2.0", "webpack": "^1.14.0" }, "scripts": { + "prebuild": "cp -r ../../build/oss-experimental/* ./node_modules/", "build": "webpack app.js bundle.js" } } diff --git a/fixtures/art/yarn.lock b/fixtures/art/yarn.lock index 048ab6810318..c2db5e016a10 100644 --- a/fixtures/art/yarn.lock +++ b/fixtures/art/yarn.lock @@ -1007,6 +1007,11 @@ arrify@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d" +art@^0.10.1: + version "0.10.3" + resolved "https://registry.yarnpkg.com/art/-/art-0.10.3.tgz#b01d84a968ccce6208df55a733838c96caeeaea2" + integrity sha512-HXwbdofRTiJT6qZX/FnchtldzJjS3vkLJxQilc3Xj+ma2MXjY4UAyQ0ls1XZYVnDvVIBiFZbC6QsvtW86TD6tQ== + asn1@~0.2.3: version "0.2.3" resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.3.tgz#dac8787713c9966849fc8180777ebe9c1ddf3b86" @@ -1286,6 +1291,14 @@ core-util-is@~1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" +create-react-class@^15.6.2: + version "15.7.0" + resolved "https://registry.yarnpkg.com/create-react-class/-/create-react-class-15.7.0.tgz#7499d7ca2e69bb51d13faf59bd04f0c65a1d6c1e" + integrity sha512-QZv4sFWG9S5RUvkTYWbflxeZX+JG7Cz0Tn33rQBJ+WFQTqTfUTjMjiv9tnfXazjsO5r0KhPs+AqCjyrQX6h2ng== + dependencies: + loose-envify "^1.3.1" + object-assign "^4.1.1" + cryptiles@2.x.x: version "2.0.5" resolved "https://registry.yarnpkg.com/cryptiles/-/cryptiles-2.0.5.tgz#3bdfecdc608147c1c67202fa291e7dca59eaa3b8" @@ -1761,7 +1774,7 @@ js-tokens@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.1.tgz#08e9f132484a2c45a30907e9dc4d5567b7f114d7" -js-tokens@^4.0.0: +"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== @@ -1897,6 +1910,13 @@ loose-envify@^1.0.0: dependencies: js-tokens "^3.0.0" +loose-envify@^1.1.0, loose-envify@^1.3.1: + version "1.4.0" + resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" + integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== + dependencies: + js-tokens "^3.0.0 || ^4.0.0" + make-dir@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-2.1.0.tgz#5f0310e18b8be898cc07009295a30ae41e91e6f5" @@ -2066,9 +2086,10 @@ oauth-sign@~0.8.1: version "0.8.2" resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.8.2.tgz#46a6ab7f0aead8deae9ec0565780b7d4efeb9d43" -object-assign@^4.0.1, object-assign@^4.1.0: +object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== object-keys@^1.0.11, object-keys@^1.0.12: version "1.1.1" @@ -2251,17 +2272,30 @@ rc@^1.1.7: minimist "^1.2.0" strip-json-comments "~2.0.1" -"react-art@link:../../build/node_modules/react-art": - version "0.0.0" - uid "" +react-art@^18.2.0: + version "18.2.0" + resolved "https://registry.yarnpkg.com/react-art/-/react-art-18.2.0.tgz#73b3929b1a0ce781b088bce76186d3793f0106c5" + integrity sha512-HfSK9OBtPIpkdI4QZAyWSxCIbM/I7+nqjybxp2FLmiFS7RN+eCEvOY0KdPw+6//B1oIU31t2o35s5EEhe0Fodw== + dependencies: + art "^0.10.1" + create-react-class "^15.6.2" + loose-envify "^1.1.0" + scheduler "^0.23.0" -"react-dom@link:../../build/node_modules/react-dom": - version "0.0.0" - uid "" +react-dom@^18.2.0: + version "18.2.0" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.2.0.tgz#22aaf38708db2674ed9ada224ca4aa708d821e3d" + integrity sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g== + dependencies: + loose-envify "^1.1.0" + scheduler "^0.23.0" -"react@link:../../build/node_modules/react": - version "0.0.0" - uid "" +react@^18.2.0: + version "18.2.0" + resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5" + integrity sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ== + dependencies: + loose-envify "^1.1.0" "readable-stream@^2.0.0 || ^1.1.13", readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.5, readable-stream@^2.1.4, readable-stream@^2.2.6: version "2.2.6" @@ -2441,6 +2475,13 @@ safe-buffer@~5.1.1: resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== +scheduler@^0.23.0: + version "0.23.0" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.0.tgz#ba8041afc3d30eb206a487b6b384002e4e61fdfe" + integrity sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw== + dependencies: + loose-envify "^1.1.0" + schema-utils@^2.6.5: version "2.7.0" resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.7.0.tgz#17151f76d8eae67fbbf77960c33c676ad9f4efc7" diff --git a/fixtures/attribute-behavior/AttributeTableSnapshot.md b/fixtures/attribute-behavior/AttributeTableSnapshot.md index 52a6bc31ab31..89e126ba234c 100644 --- a/fixtures/attribute-behavior/AttributeTableSnapshot.md +++ b/fixtures/attribute-behavior/AttributeTableSnapshot.md @@ -1998,7 +1998,7 @@ | `colSpan=(null)`| (initial, ssr error, ssr mismatch)| `` | | `colSpan=(undefined)`| (initial, ssr error, ssr mismatch)| `` | -## `content` (on `` inside `
`) +## `content` (on `` inside ``) | Test Case | Flags | Result | | --- | --- | --- | | `content=(string)`| (changed)| `"a string"` | @@ -3348,6 +3348,81 @@ | `externalResourcesRequired=(null)`| (initial)| `` | | `externalResourcesRequired=(undefined)`| (initial)| `` | +## `fetchPriority` (on `` inside `
`) +| Test Case | Flags | Result | +| --- | --- | --- | +| `fetchPriority=(string)`| (changed)| `"high"` | +| `fetchPriority=(empty string)`| (initial)| `"auto"` | +| `fetchPriority=(array with string)`| (changed)| `"high"` | +| `fetchPriority=(empty array)`| (initial)| `"auto"` | +| `fetchPriority=(object)`| (initial)| `"auto"` | +| `fetchPriority=(numeric string)`| (initial)| `"auto"` | +| `fetchPriority=(-1)`| (initial)| `"auto"` | +| `fetchPriority=(0)`| (initial)| `"auto"` | +| `fetchPriority=(integer)`| (initial)| `"auto"` | +| `fetchPriority=(NaN)`| (initial, warning)| `"auto"` | +| `fetchPriority=(float)`| (initial)| `"auto"` | +| `fetchPriority=(true)`| (initial, warning)| `"auto"` | +| `fetchPriority=(false)`| (initial, warning)| `"auto"` | +| `fetchPriority=(string 'true')`| (initial)| `"auto"` | +| `fetchPriority=(string 'false')`| (initial)| `"auto"` | +| `fetchPriority=(string 'on')`| (initial)| `"auto"` | +| `fetchPriority=(string 'off')`| (initial)| `"auto"` | +| `fetchPriority=(symbol)`| (initial, warning)| `"auto"` | +| `fetchPriority=(function)`| (initial, warning)| `"auto"` | +| `fetchPriority=(null)`| (initial)| `"auto"` | +| `fetchPriority=(undefined)`| (initial)| `"auto"` | + +## `fetchpriority` (on `` inside `
`) +| Test Case | Flags | Result | +| --- | --- | --- | +| `fetchpriority=(string)`| (changed, warning)| `"high"` | +| `fetchpriority=(empty string)`| (initial, warning)| `"auto"` | +| `fetchpriority=(array with string)`| (changed, warning)| `"high"` | +| `fetchpriority=(empty array)`| (initial, warning)| `"auto"` | +| `fetchpriority=(object)`| (initial, warning)| `"auto"` | +| `fetchpriority=(numeric string)`| (initial, warning)| `"auto"` | +| `fetchpriority=(-1)`| (initial, warning)| `"auto"` | +| `fetchpriority=(0)`| (initial, warning)| `"auto"` | +| `fetchpriority=(integer)`| (initial, warning)| `"auto"` | +| `fetchpriority=(NaN)`| (initial, warning)| `"auto"` | +| `fetchpriority=(float)`| (initial, warning)| `"auto"` | +| `fetchpriority=(true)`| (initial, warning)| `"auto"` | +| `fetchpriority=(false)`| (initial, warning)| `"auto"` | +| `fetchpriority=(string 'true')`| (initial, warning)| `"auto"` | +| `fetchpriority=(string 'false')`| (initial, warning)| `"auto"` | +| `fetchpriority=(string 'on')`| (initial, warning)| `"auto"` | +| `fetchpriority=(string 'off')`| (initial, warning)| `"auto"` | +| `fetchpriority=(symbol)`| (initial, warning)| `"auto"` | +| `fetchpriority=(function)`| (initial, warning)| `"auto"` | +| `fetchpriority=(null)`| (initial, warning)| `"auto"` | +| `fetchpriority=(undefined)`| (initial, warning)| `"auto"` | + +## `fetchPriority` (on `` inside `
`) +| Test Case | Flags | Result | +| --- | --- | --- | +| `fetchPriority=(string)`| (changed)| `"high"` | +| `fetchPriority=(empty string)`| (initial)| `"auto"` | +| `fetchPriority=(array with string)`| (changed)| `"high"` | +| `fetchPriority=(empty array)`| (initial)| `"auto"` | +| `fetchPriority=(object)`| (initial)| `"auto"` | +| `fetchPriority=(numeric string)`| (initial)| `"auto"` | +| `fetchPriority=(-1)`| (initial)| `"auto"` | +| `fetchPriority=(0)`| (initial)| `"auto"` | +| `fetchPriority=(integer)`| (initial)| `"auto"` | +| `fetchPriority=(NaN)`| (initial, warning)| `"auto"` | +| `fetchPriority=(float)`| (initial)| `"auto"` | +| `fetchPriority=(true)`| (initial, warning)| `"auto"` | +| `fetchPriority=(false)`| (initial, warning)| `"auto"` | +| `fetchPriority=(string 'true')`| (initial)| `"auto"` | +| `fetchPriority=(string 'false')`| (initial)| `"auto"` | +| `fetchPriority=(string 'on')`| (initial)| `"auto"` | +| `fetchPriority=(string 'off')`| (initial)| `"auto"` | +| `fetchPriority=(symbol)`| (initial, warning)| `"auto"` | +| `fetchPriority=(function)`| (initial, warning)| `"auto"` | +| `fetchPriority=(null)`| (initial)| `"auto"` | +| `fetchPriority=(undefined)`| (initial)| `"auto"` | + ## `fill` (on `` inside ``) | Test Case | Flags | Result | | --- | --- | --- | @@ -5048,7 +5123,7 @@ | `htmlFor=(null)`| (initial)| `` | | `htmlFor=(undefined)`| (initial)| `` | -## `http-equiv` (on `` inside `
`) +## `http-equiv` (on `` inside ``) | Test Case | Flags | Result | | --- | --- | --- | | `http-equiv=(string)`| (changed, warning)| `"a string"` | @@ -5073,7 +5148,7 @@ | `http-equiv=(null)`| (initial, warning)| `` | | `http-equiv=(undefined)`| (initial, warning)| `` | -## `httpEquiv` (on `` inside `
`) +## `httpEquiv` (on `` inside ``) | Test Case | Flags | Result | | --- | --- | --- | | `httpEquiv=(string)`| (changed)| `"a string"` | @@ -6123,6 +6198,31 @@ | `lang=(null)`| (initial)| `` | | `lang=(undefined)`| (initial)| `` | +## `lang` (on `` inside ``) +| Test Case | Flags | Result | +| --- | --- | --- | +| `lang=(string)`| (changed, ssr mismatch)| `"a string"` | +| `lang=(empty string)`| (initial)| `` | +| `lang=(array with string)`| (changed, ssr mismatch)| `"string"` | +| `lang=(empty array)`| (initial)| `` | +| `lang=(object)`| (changed, ssr mismatch)| `"result of toString()"` | +| `lang=(numeric string)`| (changed, ssr mismatch)| `"42"` | +| `lang=(-1)`| (changed, ssr mismatch)| `"-1"` | +| `lang=(0)`| (changed, ssr mismatch)| `"0"` | +| `lang=(integer)`| (changed, ssr mismatch)| `"1"` | +| `lang=(NaN)`| (changed, warning, ssr mismatch)| `"NaN"` | +| `lang=(float)`| (changed, ssr mismatch)| `"99.99"` | +| `lang=(true)`| (initial, warning)| `` | +| `lang=(false)`| (initial, warning)| `` | +| `lang=(string 'true')`| (changed, ssr mismatch)| `"true"` | +| `lang=(string 'false')`| (changed, ssr mismatch)| `"false"` | +| `lang=(string 'on')`| (changed, ssr mismatch)| `"on"` | +| `lang=(string 'off')`| (changed, ssr mismatch)| `"off"` | +| `lang=(symbol)`| (initial, warning)| `` | +| `lang=(function)`| (initial, warning)| `` | +| `lang=(null)`| (initial)| `` | +| `lang=(undefined)`| (initial)| `` | + ## `length` (on `
` inside `
`) | Test Case | Flags | Result | | --- | --- | --- | @@ -11298,6 +11398,56 @@ | `transform=(null)`| (initial)| `[]` | | `transform=(undefined)`| (initial)| `[]` | +## `transform-origin` (on `` inside `
`) +| Test Case | Flags | Result | +| --- | --- | --- | +| `transform-origin=(string)`| (changed, warning)| `"a string"` | +| `transform-origin=(empty string)`| (changed, warning)| `` | +| `transform-origin=(array with string)`| (changed, warning)| `"string"` | +| `transform-origin=(empty array)`| (changed, warning)| `` | +| `transform-origin=(object)`| (changed, warning)| `"result of toString()"` | +| `transform-origin=(numeric string)`| (changed, warning)| `"42"` | +| `transform-origin=(-1)`| (changed, warning)| `"-1"` | +| `transform-origin=(0)`| (changed, warning)| `"0"` | +| `transform-origin=(integer)`| (changed, warning)| `"1"` | +| `transform-origin=(NaN)`| (changed, warning)| `"NaN"` | +| `transform-origin=(float)`| (changed, warning)| `"99.99"` | +| `transform-origin=(true)`| (initial, warning)| `` | +| `transform-origin=(false)`| (initial, warning)| `` | +| `transform-origin=(string 'true')`| (changed, warning)| `"true"` | +| `transform-origin=(string 'false')`| (changed, warning)| `"false"` | +| `transform-origin=(string 'on')`| (changed, warning)| `"on"` | +| `transform-origin=(string 'off')`| (changed, warning)| `"off"` | +| `transform-origin=(symbol)`| (initial, warning)| `` | +| `transform-origin=(function)`| (initial, warning)| `` | +| `transform-origin=(null)`| (initial, warning)| `` | +| `transform-origin=(undefined)`| (initial, warning)| `` | + +## `transformOrigin` (on `` inside `
`) +| Test Case | Flags | Result | +| --- | --- | --- | +| `transformOrigin=(string)`| (changed)| `"a string"` | +| `transformOrigin=(empty string)`| (changed)| `` | +| `transformOrigin=(array with string)`| (changed)| `"string"` | +| `transformOrigin=(empty array)`| (changed)| `` | +| `transformOrigin=(object)`| (changed)| `"result of toString()"` | +| `transformOrigin=(numeric string)`| (changed)| `"42"` | +| `transformOrigin=(-1)`| (changed)| `"-1"` | +| `transformOrigin=(0)`| (changed)| `"0"` | +| `transformOrigin=(integer)`| (changed)| `"1"` | +| `transformOrigin=(NaN)`| (changed, warning)| `"NaN"` | +| `transformOrigin=(float)`| (changed)| `"99.99"` | +| `transformOrigin=(true)`| (initial, warning)| `` | +| `transformOrigin=(false)`| (initial, warning)| `` | +| `transformOrigin=(string 'true')`| (changed)| `"true"` | +| `transformOrigin=(string 'false')`| (changed)| `"false"` | +| `transformOrigin=(string 'on')`| (changed)| `"on"` | +| `transformOrigin=(string 'off')`| (changed)| `"off"` | +| `transformOrigin=(symbol)`| (initial, warning)| `` | +| `transformOrigin=(function)`| (initial, warning)| `` | +| `transformOrigin=(null)`| (initial)| `` | +| `transformOrigin=(undefined)`| (initial)| `` | + ## `type` (on `
diff --git a/packages/react-devtools-shell/perf-regression.html b/packages/react-devtools-shell/perf-regression.html new file mode 100644 index 000000000000..27fd87f88dd8 --- /dev/null +++ b/packages/react-devtools-shell/perf-regression.html @@ -0,0 +1,45 @@ + + + + + React DevTools + + + + + +
+ +
+ + + \ No newline at end of file diff --git a/packages/react-devtools-shell/src/app/DeeplyNestedComponents/index.js b/packages/react-devtools-shell/src/app/DeeplyNestedComponents/index.js index dc1de76dfa73..f1369896eda0 100644 --- a/packages/react-devtools-shell/src/app/DeeplyNestedComponents/index.js +++ b/packages/react-devtools-shell/src/app/DeeplyNestedComponents/index.js @@ -10,19 +10,18 @@ import * as React from 'react'; import {Fragment} from 'react'; -function wrapWithHoc(Component, index) { +function wrapWithHoc(Component: () => any, index: number) { function HOC() { return ; } - const displayName = Component.displayName || Component.name; + const displayName = (Component: any).displayName || Component.name; - // $FlowFixMe[incompatible-type] found when upgrading Flow HOC.displayName = `withHoc${index}(${displayName})`; return HOC; } -function wrapWithNested(Component, times) { +function wrapWithNested(Component: () => any, times: number) { for (let i = 0; i < times; i++) { Component = wrapWithHoc(Component, i); } diff --git a/packages/react-devtools-shell/src/app/EditableProps/index.js b/packages/react-devtools-shell/src/app/EditableProps/index.js index fe617337faeb..2d4955b5211f 100644 --- a/packages/react-devtools-shell/src/app/EditableProps/index.js +++ b/packages/react-devtools-shell/src/app/EditableProps/index.js @@ -7,6 +7,7 @@ * @flow */ +import type {ReactContext} from 'shared/ReactTypes'; import * as React from 'react'; import { createContext, @@ -23,7 +24,8 @@ import { const initialData = {foo: 'FOO', bar: 'BAR'}; -function reducer(state, action) { +// $FlowFixMe[missing-local-annot] +function reducer(state, action: {type: string}) { switch (action.type) { case 'swap': return {foo: state.bar, bar: state.foo}; @@ -37,9 +39,10 @@ type StatefulFunctionProps = {name: string}; function StatefulFunction({name}: StatefulFunctionProps) { const [count, updateCount] = useState(0); const debouncedCount = useDebounce(count, 1000); - const handleUpdateCountClick = useCallback(() => updateCount(count + 1), [ - count, - ]); + const handleUpdateCountClick = useCallback( + () => updateCount(count + 1), + [count], + ); const [data, dispatch] = useReducer(reducer, initialData); const handleUpdateReducerClick = useCallback( @@ -72,19 +75,20 @@ type Props = {name: string, toggle: boolean}; type State = {cities: Array, state: string}; class StatefulClass extends Component { - static contextType = BoolContext; + static contextType: ReactContext = BoolContext; state: State = { cities: ['San Francisco', 'San Jose'], state: 'California', }; - handleChange = ({target}) => + // $FlowFixMe[missing-local-annot] + handleChange = ({target}): any => this.setState({ state: target.value, }); - render() { + render(): any { return (
  • Name: {this.props.name}
  • @@ -106,9 +110,10 @@ const ForwardRef = forwardRef<{name: string}, HTMLUListElement>( ({name}, ref) => { const [count, updateCount] = useState(0); const debouncedCount = useDebounce(count, 1000); - const handleUpdateCountClick = useCallback(() => updateCount(count + 1), [ - count, - ]); + const handleUpdateCountClick = useCallback( + () => updateCount(count + 1), + [count], + ); return (
    • Name: {name}
    • @@ -141,7 +146,7 @@ export default function EditableProps(): React.Node { } // Below copied from https://usehooks.com/ -function useDebounce(value, delay) { +function useDebounce(value: number, delay: number) { // State and setters for debounced value const [debouncedValue, setDebouncedValue] = useState(value); diff --git a/packages/react-devtools-shell/src/app/ElementTypes/index.js b/packages/react-devtools-shell/src/app/ElementTypes/index.js index 6268c75e88b6..4450c5682d16 100644 --- a/packages/react-devtools-shell/src/app/ElementTypes/index.js +++ b/packages/react-devtools-shell/src/app/ElementTypes/index.js @@ -25,7 +25,7 @@ const Context = createContext('abc'); Context.displayName = 'ExampleContext'; class ClassComponent extends Component { - render() { + render(): null { return null; } } @@ -58,7 +58,7 @@ export default function ElementTypes(): React.Node { {}}> - {value => null} + {(value: $FlowFixMe) => null} diff --git a/packages/react-devtools-shell/src/app/ErrorBoundaries/index.js b/packages/react-devtools-shell/src/app/ErrorBoundaries/index.js index bdd9666fbec7..a87d853ea1b2 100644 --- a/packages/react-devtools-shell/src/app/ErrorBoundaries/index.js +++ b/packages/react-devtools-shell/src/app/ErrorBoundaries/index.js @@ -11,13 +11,13 @@ import * as React from 'react'; import {Fragment} from 'react'; class ErrorBoundary extends React.Component { - state = {hasError: false}; + state: {hasError: boolean} = {hasError: false}; - static getDerivedStateFromError(error) { + static getDerivedStateFromError(error: any): {hasError: boolean} { return {hasError: true}; } - render() { + render(): any { const {hasError} = this.state; if (hasError) { return ( @@ -49,6 +49,7 @@ class ErrorBoundary extends React.Component { } } +// $FlowFixMe[missing-local-annot] function Component({label}) { return
      {label}
      ; } diff --git a/packages/react-devtools-shell/src/app/Iframe/index.js b/packages/react-devtools-shell/src/app/Iframe/index.js index 50beadd137aa..7f3964971cf8 100644 --- a/packages/react-devtools-shell/src/app/Iframe/index.js +++ b/packages/react-devtools-shell/src/app/Iframe/index.js @@ -19,12 +19,13 @@ export default function Iframe(): React.Node { const iframeStyle = {border: '2px solid #eee', height: 80}; +// $FlowFixMe[missing-local-annot] function Frame(props) { const [element, setElement] = React.useState(null); const ref = React.useRef(); - React.useLayoutEffect(function() { + React.useLayoutEffect(function () { const iframe = ref.current; if (iframe) { diff --git a/packages/react-devtools-shell/src/app/InlineWarnings/index.js b/packages/react-devtools-shell/src/app/InlineWarnings/index.js index 6b7b189f7017..ddfac6acb4c3 100644 --- a/packages/react-devtools-shell/src/app/InlineWarnings/index.js +++ b/packages/react-devtools-shell/src/app/InlineWarnings/index.js @@ -3,11 +3,13 @@ import * as React from 'react'; import {Fragment, useEffect, useRef, useState} from 'react'; +// $FlowFixMe[missing-local-annot] function WarnDuringRender({children = null}) { console.warn('This warning fires during every render'); return children; } +// $FlowFixMe[missing-local-annot] function WarnOnMount({children = null}) { useEffect(() => { console.warn('This warning fires on initial mount only'); @@ -15,6 +17,7 @@ function WarnOnMount({children = null}) { return children; } +// $FlowFixMe[missing-local-annot] function WarnOnUpdate({children = null}) { const didMountRef = useRef(false); useEffect(() => { @@ -27,6 +30,7 @@ function WarnOnUpdate({children = null}) { return children; } +// $FlowFixMe[missing-local-annot] function WarnOnUnmount({children = null}) { useEffect(() => { return () => { @@ -36,11 +40,13 @@ function WarnOnUnmount({children = null}) { return children; } +// $FlowFixMe[missing-local-annot] function ErrorDuringRender({children = null}) { console.error('This error fires during every render'); return children; } +// $FlowFixMe[missing-local-annot] function ErrorOnMount({children = null}) { useEffect(() => { console.error('This error fires on initial mount only'); @@ -48,6 +54,7 @@ function ErrorOnMount({children = null}) { return children; } +// $FlowFixMe[missing-local-annot] function ErrorOnUpdate({children = null}) { const didMountRef = useRef(false); useEffect(() => { @@ -60,6 +67,7 @@ function ErrorOnUpdate({children = null}) { return children; } +// $FlowFixMe[missing-local-annot] function ErrorOnUnmount({children = null}) { useEffect(() => { return () => { @@ -69,12 +77,14 @@ function ErrorOnUnmount({children = null}) { return children; } +// $FlowFixMe[missing-local-annot] function ErrorAndWarningDuringRender({children = null}) { console.warn('This warning fires during every render'); console.error('This error fires during every render'); return children; } +// $FlowFixMe[missing-local-annot] function ErrorAndWarningOnMount({children = null}) { useEffect(() => { console.warn('This warning fires on initial mount only'); @@ -83,6 +93,7 @@ function ErrorAndWarningOnMount({children = null}) { return children; } +// $FlowFixMe[missing-local-annot] function ErrorAndWarningOnUpdate({children = null}) { const didMountRef = useRef(false); useEffect(() => { @@ -96,6 +107,7 @@ function ErrorAndWarningOnUpdate({children = null}) { return children; } +// $FlowFixMe[missing-local-annot] function ErrorAndWarningOnUnmount({children = null}) { useEffect(() => { return () => { @@ -106,6 +118,7 @@ function ErrorAndWarningOnUnmount({children = null}) { return children; } +// $FlowFixMe[missing-local-annot] function ReallyLongErrorMessageThatWillCauseTextToBeTruncated({ children = null, }) { @@ -115,20 +128,24 @@ function ReallyLongErrorMessageThatWillCauseTextToBeTruncated({ return children; } +// $FlowFixMe[missing-local-annot] function ErrorWithMultipleArgs({children = null}) { console.error('This error', 'passes console', 4, 'arguments'); return children; } +// $FlowFixMe[missing-local-annot] function ErrorWithStringSubstitutions({children = null}) { console.error('This error uses "%s" substitutions', 'string'); return children; } +// $FlowFixMe[missing-local-annot] function ReactErrorOnHostComponent({children = null}) { return
      {children}
      ; } +// $FlowFixMe[missing-local-annot] function DuplicateWarningsAndErrors({children = null}) { console.warn('this warning is logged twice per render'); console.warn('this warning is logged twice per render'); @@ -137,6 +154,7 @@ function DuplicateWarningsAndErrors({children = null}) { return
      {children}
      ; } +// $FlowFixMe[missing-local-annot] function MultipleWarningsAndErrors({children = null}) { console.warn('this is the first warning logged'); console.warn('this is the second warning logged'); @@ -145,6 +163,7 @@ function MultipleWarningsAndErrors({children = null}) { return
      {children}
      ; } +// $FlowFixMe[missing-local-annot] function ComponentWithMissingKey({children}) { return [
      ]; } diff --git a/packages/react-devtools-shell/src/app/InspectableElements/CircularReferences.js b/packages/react-devtools-shell/src/app/InspectableElements/CircularReferences.js index c1bc9dcf1e45..e918325eb1b9 100644 --- a/packages/react-devtools-shell/src/app/InspectableElements/CircularReferences.js +++ b/packages/react-devtools-shell/src/app/InspectableElements/CircularReferences.js @@ -9,13 +9,20 @@ import * as React from 'react'; -const arrayOne = []; -const arrayTwo = []; +const arrayOne: $FlowFixMe = []; +const arrayTwo: $FlowFixMe = []; arrayTwo.push(arrayOne); arrayOne.push(arrayTwo); -const objectOne = {}; -const objectTwo = {objectOne}; +type ObjectOne = { + objectTwo?: ObjectTwo, +}; +type ObjectTwo = { + objectOne: ObjectOne, +}; + +const objectOne: ObjectOne = {}; +const objectTwo: ObjectTwo = {objectOne}; objectOne.objectTwo = objectTwo; export default function CircularReferences(): React.Node { diff --git a/packages/react-devtools-shell/src/app/InspectableElements/Contexts.js b/packages/react-devtools-shell/src/app/InspectableElements/Contexts.js index da54beed0b21..256cd908518f 100644 --- a/packages/react-devtools-shell/src/app/InspectableElements/Contexts.js +++ b/packages/react-devtools-shell/src/app/InspectableElements/Contexts.js @@ -11,9 +11,11 @@ import * as React from 'react'; import {createContext, Component, useContext, useState} from 'react'; import PropTypes from 'prop-types'; +import type {ReactContext} from 'shared/ReactTypes'; + function someNamedFunction() {} -function formatContextForDisplay(name, value) { +function formatContextForDisplay(name: string, value: any | string) { return (
    • {name}:
      {JSON.stringify(value, null, 2)}
      @@ -34,7 +36,17 @@ const contextData = { }; class LegacyContextProvider extends Component { - static childContextTypes = { + static childContextTypes: { + array: any, + bool: any, + func: any, + null: any, + number: any, + object: any, + string: any, + symbol: any, + undefined: any, + } = { array: PropTypes.array, bool: PropTypes.bool, func: PropTypes.func, @@ -46,17 +58,37 @@ class LegacyContextProvider extends Component { undefined: PropTypes.any, }; - getChildContext() { + getChildContext(): { + array: Array, + bool: boolean, + func: () => void, + null: null, + number: number, + object: {outer: {inner: {...}}}, + string: string, + symbol: symbol, + undefined: void, + } { return contextData; } - render() { + render(): any { return this.props.children; } } class LegacyContextConsumer extends Component { - static contextTypes = { + static contextTypes: { + array: any, + bool: any, + func: any, + null: any, + number: any, + object: any, + string: any, + symbol: any, + undefined: any, + } = { array: PropTypes.array, bool: PropTypes.bool, func: PropTypes.func, @@ -68,26 +100,27 @@ class LegacyContextConsumer extends Component { undefined: PropTypes.any, }; - render() { + render(): any { return formatContextForDisplay('LegacyContextConsumer', this.context); } } class LegacyContextProviderWithUpdates extends Component { - constructor(props) { + constructor(props: any) { super(props); this.state = {type: 'desktop'}; } - getChildContext() { + getChildContext(): {type: any} { return {type: this.state.type}; } + // $FlowFixMe[missing-local-annot] handleChange = event => { this.setState({type: event.target.value}); }; - render() { + render(): any { return ( <> @@ -103,7 +136,8 @@ LegacyContextProviderWithUpdates.childContextTypes = { type: PropTypes.string, }; -function LegacyFunctionalContextConsumer(props, context) { +// $FlowFixMe[missing-local-annot] +function LegacyFunctionalContextConsumer(props: any, context) { return formatContextForDisplay('LegacyFunctionContextConsumer', context.type); } LegacyFunctionalContextConsumer.contextTypes = { @@ -130,9 +164,9 @@ const UndefinedContext = createContext(undefined); UndefinedContext.displayName = 'UndefinedContext'; class ModernContextType extends Component { - static contextType = ModernContext; + static contextType: ReactContext = ModernContext; - render() { + render(): any { return formatContextForDisplay('ModernContextType', this.context); } } @@ -171,7 +205,9 @@ function FunctionalContextConsumerWithContextUpdates() { const {string2, setString2} = useContext(StringContextWithUpdates2); const [state, setState] = useState('state'); + // $FlowFixMe[missing-local-annot] const handleChange = e => setString(e.target.value); + // $FlowFixMe[missing-local-annot] const handleChange2 = e => setString2(e.target.value); return ( @@ -198,7 +234,7 @@ function FunctionalContextConsumerWithContextUpdates() { } class ModernClassContextProviderWithUpdates extends Component { - constructor(props) { + constructor(props: any) { super(props); this.setString = string => { this.setState({string}); @@ -210,7 +246,7 @@ class ModernClassContextProviderWithUpdates extends Component { }; } - render() { + render(): any { return ( @@ -220,10 +256,10 @@ class ModernClassContextProviderWithUpdates extends Component { } class ModernClassContextConsumerWithUpdates extends Component { - render() { + render(): any { return ( - {({string, setString}) => ( + {({string, setString}: {string: string, setString: string => void}) => ( <> {formatContextForDisplay( 'ModernClassContextConsumerWithUpdates', @@ -248,7 +284,9 @@ export default function Contexts(): React.Node { - {value => formatContextForDisplay('ModernContext.Consumer', value)} + {(value: $FlowFixMe) => + formatContextForDisplay('ModernContext.Consumer', value) + } @@ -256,28 +294,44 @@ export default function Contexts(): React.Node { - {value => formatContextForDisplay('ArrayContext.Consumer', value)} + {(value: $FlowFixMe) => + formatContextForDisplay('ArrayContext.Consumer', value) + } - {value => formatContextForDisplay('BoolContext.Consumer', value)} + {(value: $FlowFixMe) => + formatContextForDisplay('BoolContext.Consumer', value) + } - {value => formatContextForDisplay('FuncContext.Consumer', value)} + {(value: $FlowFixMe) => + formatContextForDisplay('FuncContext.Consumer', value) + } - {value => formatContextForDisplay('NumberContext.Consumer', value)} + {(value: $FlowFixMe) => + formatContextForDisplay('NumberContext.Consumer', value) + } - {value => formatContextForDisplay('StringContext.Consumer', value)} + {(value: $FlowFixMe) => + formatContextForDisplay('StringContext.Consumer', value) + } - {value => formatContextForDisplay('SymbolContext.Consumer', value)} + {(value: $FlowFixMe) => + formatContextForDisplay('SymbolContext.Consumer', value) + } - {value => formatContextForDisplay('NullContext.Consumer', value)} + {(value: $FlowFixMe) => + formatContextForDisplay('NullContext.Consumer', value) + } - {value => formatContextForDisplay('UndefinedContext.Consumer', value)} + {(value: $FlowFixMe) => + formatContextForDisplay('UndefinedContext.Consumer', value) + }
diff --git a/packages/react-devtools-shell/src/app/InspectableElements/CustomHooks.js b/packages/react-devtools-shell/src/app/InspectableElements/CustomHooks.js index 66920af0d791..939b2fa20e0d 100644 --- a/packages/react-devtools-shell/src/app/InspectableElements/CustomHooks.js +++ b/packages/react-devtools-shell/src/app/InspectableElements/CustomHooks.js @@ -103,11 +103,11 @@ function FunctionWithHooks(props: any, ref: React$Ref) { const MemoWithHooks = memo(FunctionWithHooks); const ForwardRefWithHooks = forwardRef(FunctionWithHooks); -function wrapWithHoc(Component) { +function wrapWithHoc(Component: (props: any, ref: React$Ref) => any) { function Hoc() { return ; } - // $FlowFixMe + // $FlowFixMe[prop-missing] const displayName = Component.displayName || Component.name; // $FlowFixMe[incompatible-type] found when upgrading Flow Hoc.displayName = `withHoc(${displayName})`; @@ -127,7 +127,7 @@ export default function CustomHooks(): React.Node { } // Below copied from https://usehooks.com/ -function useDebounce(value, delay) { +function useDebounce(value: number, delay: number) { // State and setters for debounced value const [debouncedValue, setDebouncedValue] = useState(value); diff --git a/packages/react-devtools-shell/src/app/InspectableElements/CustomObject.js b/packages/react-devtools-shell/src/app/InspectableElements/CustomObject.js index 9d03fbd71751..f4de359f499f 100644 --- a/packages/react-devtools-shell/src/app/InspectableElements/CustomObject.js +++ b/packages/react-devtools-shell/src/app/InspectableElements/CustomObject.js @@ -11,7 +11,7 @@ import * as React from 'react'; class Custom { _number = 42; - get number() { + get number(): number { return this._number; } } diff --git a/packages/react-devtools-shell/src/app/InspectableElements/SymbolKeys.js b/packages/react-devtools-shell/src/app/InspectableElements/SymbolKeys.js index 5f8d1e0f5d94..0bd1ee258c3a 100644 --- a/packages/react-devtools-shell/src/app/InspectableElements/SymbolKeys.js +++ b/packages/react-devtools-shell/src/app/InspectableElements/SymbolKeys.js @@ -16,6 +16,7 @@ const base = Object.create(Object.prototype, { enumerable: true, configurable: true, }, + // $FlowFixMe[invalid-computed-prop] [Symbol('enumerableSymbolBase')]: { value: 1, writable: true, @@ -28,6 +29,7 @@ const base = Object.create(Object.prototype, { enumerable: false, configurable: true, }, + // $FlowFixMe[invalid-computed-prop] [Symbol('nonEnumerableSymbolBase')]: { value: 1, writable: true, @@ -55,12 +57,14 @@ const data = Object.create(base, { enumerable: true, configurable: true, }, + // $FlowFixMe[invalid-computed-prop] [Symbol('nonEnumerableSymbol')]: { value: 2, writable: true, enumerable: false, configurable: true, }, + // $FlowFixMe[invalid-computed-prop] [Symbol('enumerableSymbol')]: { value: 3, writable: true, diff --git a/packages/react-devtools-shell/src/app/InspectableElements/UnserializableProps.js b/packages/react-devtools-shell/src/app/InspectableElements/UnserializableProps.js index 9d006fb01f86..d04783b6a7e3 100644 --- a/packages/react-devtools-shell/src/app/InspectableElements/UnserializableProps.js +++ b/packages/react-devtools-shell/src/app/InspectableElements/UnserializableProps.js @@ -31,9 +31,15 @@ const immutable = Immutable.fromJS({ xyz: 1, }, }); -// $FlowFixMe const bigInt = BigInt(123); // eslint-disable-line no-undef +class Foo { + flag = false; + object: Object = { + a: {b: {c: {d: 1}}}, + }; +} + export default function UnserializableProps(): React.Node { return ( ); } diff --git a/packages/react-devtools-shell/src/app/ReactNativeWeb/index.js b/packages/react-devtools-shell/src/app/ReactNativeWeb/index.js index 0fd5c24ac481..89049dafafd6 100644 --- a/packages/react-devtools-shell/src/app/ReactNativeWeb/index.js +++ b/packages/react-devtools-shell/src/app/ReactNativeWeb/index.js @@ -9,7 +9,6 @@ import * as React from 'react'; import {Fragment, useState} from 'react'; -// $FlowFixMe import {Button, Text, View} from 'react-native-web'; export default function ReactNativeWeb(): React.Node { diff --git a/packages/react-devtools-shell/src/app/SuspenseTree/index.js b/packages/react-devtools-shell/src/app/SuspenseTree/index.js index 8713d4d0275a..88a56dc10163 100644 --- a/packages/react-devtools-shell/src/app/SuspenseTree/index.js +++ b/packages/react-devtools-shell/src/app/SuspenseTree/index.js @@ -29,6 +29,7 @@ function EmptySuspense() { return ; } +// $FlowFixMe[missing-local-annot] function PrimaryFallbackTest({initialSuspend}) { const [suspend, setSuspend] = useState(initialSuspend); const fallbackStep = useTestSequence('fallback', Fallback1, Fallback2); @@ -51,14 +52,14 @@ function PrimaryFallbackTest({initialSuspend}) { ); } -function useTestSequence(label, T1, T2) { +function useTestSequence(label: string, T1: any => any, T2: any => any) { const [step, setStep] = useState(0); - const next = ( + const next: $FlowFixMe = ( ); - const allSteps = [ + const allSteps: $FlowFixMe = [ {next}, {next} mount diff --git a/packages/react-devtools-shell/src/app/ToDoList/List.js b/packages/react-devtools-shell/src/app/ToDoList/List.js index d5cdbe6c286a..68f0b120ffde 100644 --- a/packages/react-devtools-shell/src/app/ToDoList/List.js +++ b/packages/react-devtools-shell/src/app/ToDoList/List.js @@ -45,7 +45,7 @@ export default function List(props: Props): React.Node { }, [newItemText, items, uid]); const handleKeyPress = useCallback( - event => { + (event: $FlowFixMe) => { if (event.key === 'Enter') { handleClick(); } @@ -54,19 +54,20 @@ export default function List(props: Props): React.Node { ); const handleChange = useCallback( - event => { + (event: $FlowFixMe) => { setNewItemText(event.currentTarget.value); }, [setNewItemText], ); const removeItem = useCallback( - itemToRemove => setItems(items.filter(item => item !== itemToRemove)), + (itemToRemove: $FlowFixMe) => + setItems(items.filter(item => item !== itemToRemove)), [items], ); const toggleItem = useCallback( - itemToToggle => { + (itemToToggle: $FlowFixMe) => { // Dont use indexOf() // because editing props in DevTools creates a new Object. const index = items.findIndex(item => item.id === itemToToggle.id); diff --git a/packages/react-devtools-shell/src/app/console.js b/packages/react-devtools-shell/src/app/console.js index 01aba36e7efa..5fecc27c115a 100644 --- a/packages/react-devtools-shell/src/app/console.js +++ b/packages/react-devtools-shell/src/app/console.js @@ -11,7 +11,8 @@ function ignoreStrings( methodName: string, stringsToIgnore: Array, ): void { - console[methodName] = (...args) => { + // $FlowFixMe[prop-missing] index access only allowed for objects with index keys + console[methodName] = (...args: $ReadOnlyArray) => { const maybeString = args[0]; if (typeof maybeString === 'string') { for (let i = 0; i < stringsToIgnore.length; i++) { diff --git a/packages/react-devtools-shell/src/app/devtools.js b/packages/react-devtools-shell/src/app/devtools.js index 5915275978a4..8f3de74cf67c 100644 --- a/packages/react-devtools-shell/src/app/devtools.js +++ b/packages/react-devtools-shell/src/app/devtools.js @@ -11,7 +11,7 @@ import {initDevTools} from 'react-devtools-shared/src/devtools'; // This is a pretty gross hack to make the runtime loaded named-hooks-code work. // TODO (Webpack 5) Hoepfully we can remove this once we upgrade to Webpack 5. -// $FlowFixMe +// $FlowFixMe[cannot-resolve-name] __webpack_public_path__ = '/dist/'; // eslint-disable-line no-undef const iframe = ((document.getElementById('target'): any): HTMLIFrameElement); @@ -38,7 +38,7 @@ let isTestAppMounted = true; const mountButton = ((document.getElementById( 'mountButton', ): any): HTMLButtonElement); -mountButton.addEventListener('click', function() { +mountButton.addEventListener('click', function () { if (isTestAppMounted) { if (typeof window.unmountTestApp === 'function') { window.unmountTestApp(); @@ -81,7 +81,7 @@ inject('dist/app-index.js', () => { }); }); -function inject(sourcePath, callback) { +function inject(sourcePath: string, callback: () => void) { const script = contentDocument.createElement('script'); script.onload = callback; script.src = sourcePath; diff --git a/packages/react-devtools-shell/src/app/index.js b/packages/react-devtools-shell/src/app/index.js index 3ec70de06715..f77d6c65d44a 100644 --- a/packages/react-devtools-shell/src/app/index.js +++ b/packages/react-devtools-shell/src/app/index.js @@ -33,7 +33,7 @@ ignoreErrors([ ignoreWarnings(['Warning: componentWillReceiveProps has been renamed']); ignoreLogs([]); -const unmountFunctions = []; +const unmountFunctions: Array<() => void | boolean> = []; function createContainer() { const container = document.createElement('div'); @@ -43,7 +43,7 @@ function createContainer() { return container; } -function mountApp(App) { +function mountApp(App: () => React$Node) { const container = createContainer(); const root = createRoot(container); @@ -52,6 +52,7 @@ function mountApp(App) { unmountFunctions.push(() => root.unmount()); } +// $FlowFixMe[missing-local-annot] function mountStrictApp(App) { function StrictRoot() { return createElement(App); @@ -65,7 +66,7 @@ function mountStrictApp(App) { unmountFunctions.push(() => root.unmount()); } -function mountLegacyApp(App) { +function mountLegacyApp(App: () => React$Node) { function LegacyRender() { return createElement(App); } diff --git a/packages/react-devtools-shell/src/e2e-apps/ListApp.js b/packages/react-devtools-shell/src/e2e-apps/ListApp.js index f365686ad139..9cb17188b0ff 100644 --- a/packages/react-devtools-shell/src/e2e-apps/ListApp.js +++ b/packages/react-devtools-shell/src/e2e-apps/ListApp.js @@ -43,6 +43,7 @@ function List() { ); } +// $FlowFixMe[missing-local-annot] function ListItem({label}) { return
  • {label}
  • ; } diff --git a/packages/react-devtools-shell/src/e2e-apps/ListAppLegacy.js b/packages/react-devtools-shell/src/e2e-apps/ListAppLegacy.js index 8d0bb694a968..056bd7b2243b 100644 --- a/packages/react-devtools-shell/src/e2e-apps/ListAppLegacy.js +++ b/packages/react-devtools-shell/src/e2e-apps/ListAppLegacy.js @@ -14,7 +14,7 @@ export default function App(): React.Node { } class List extends React.Component { - constructor(props) { + constructor(props: any) { super(props); this.state = { items: ['one', 'two', 'three'], @@ -28,7 +28,7 @@ class List extends React.Component { } }; - render() { + render(): any { return (
    {label}; } diff --git a/packages/react-devtools-shell/src/e2e-regression/app-legacy.js b/packages/react-devtools-shell/src/e2e-regression/app-legacy.js index abb65dc36a3b..e8bb10fda091 100644 --- a/packages/react-devtools-shell/src/e2e-regression/app-legacy.js +++ b/packages/react-devtools-shell/src/e2e-regression/app-legacy.js @@ -4,12 +4,13 @@ import * as React from 'react'; import * as ReactDOM from 'react-dom'; -import {gte} from 'semver'; import ListApp from '../e2e-apps/ListApp'; import ListAppLegacy from '../e2e-apps/ListAppLegacy'; +import {gte} from 'react-devtools-shared/src/backend/utils'; + const version = process.env.E2E_APP_REACT_VERSION; -function mountApp(App) { +function mountApp(App: () => React$Node) { const container = document.createElement('div'); ((document.body: any): HTMLBodyElement).appendChild(container); diff --git a/packages/react-devtools-shell/src/e2e-regression/app.js b/packages/react-devtools-shell/src/e2e-regression/app.js index d97f1a3f774d..a2b4eb735cce 100644 --- a/packages/react-devtools-shell/src/e2e-regression/app.js +++ b/packages/react-devtools-shell/src/e2e-regression/app.js @@ -7,7 +7,7 @@ import * as ReactDOM from 'react-dom'; import {createRoot} from 'react-dom/client'; import ListApp from '../e2e-apps/ListApp'; -function mountApp(App) { +function mountApp(App: () => React$Node) { const container = document.createElement('div'); ((document.body: any): HTMLBodyElement).appendChild(container); diff --git a/packages/react-devtools-shell/src/e2e-regression/devtools.js b/packages/react-devtools-shell/src/e2e-regression/devtools.js index d8554eb55865..9652e7653996 100644 --- a/packages/react-devtools-shell/src/e2e-regression/devtools.js +++ b/packages/react-devtools-shell/src/e2e-regression/devtools.js @@ -9,7 +9,6 @@ import {initialize as createDevTools} from 'react-devtools-inline/frontend'; // This is a pretty gross hack to make the runtime loaded named-hooks-code work. // TODO (Webpack 5) Hoepfully we can remove this once we upgrade to Webpack 5. -// $FlowFixMer __webpack_public_path__ = '/dist/'; // eslint-disable-line no-undef // TODO (Webpack 5) Hopefully we can remove this prop after the Webpack 5 migration. @@ -33,7 +32,6 @@ function init(appIframe, devtoolsContainer, appSource) { const DevTools = createDevTools(contentWindow); inject(contentDocument, appSource, () => { - // $FlowFixMe Flow doesn't know about createRoot() yet. createRoot(devtoolsContainer).render( { - // $FlowFixMe Flow doesn't know about createRoot() yet. createRoot(devtoolsContainer).render( + + , + ); +} + +mountApp(); diff --git a/packages/react-devtools-shell/src/perf-regression/apps/LargeSubtree.js b/packages/react-devtools-shell/src/perf-regression/apps/LargeSubtree.js new file mode 100644 index 000000000000..3f547a4ca14e --- /dev/null +++ b/packages/react-devtools-shell/src/perf-regression/apps/LargeSubtree.js @@ -0,0 +1,44 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import * as React from 'react'; + +function generateArray(size: number) { + return Array.from({length: size}, () => Math.floor(Math.random() * size)); +} + +const arr = generateArray(50000); + +export default function LargeSubtree(): React.Node { + const [showList, setShowList] = React.useState(false); + const toggleList = () => { + const startTime = performance.now(); + setShowList(!showList); + // requestAnimationFrame should happen after render+commit is done + window.requestAnimationFrame(() => { + const afterRenderTime = performance.now(); + console.log( + `Time spent on ${showList ? 'unmounting' : 'mounting'} the subtree: ${ + afterRenderTime - startTime + }ms`, + ); + }); + }; + return ( +
    +

    Mount/Unmount a large subtree

    +

    Click the button to toggle the state. Open console for results.

    + +
      +
    • dummy item
    • + {showList && arr.map((num, idx) =>
    • {num}
    • )} +
    +
    + ); +} diff --git a/packages/react-devtools-shell/src/perf-regression/apps/index.js b/packages/react-devtools-shell/src/perf-regression/apps/index.js new file mode 100644 index 000000000000..3e51b9a5d7c4 --- /dev/null +++ b/packages/react-devtools-shell/src/perf-regression/apps/index.js @@ -0,0 +1,19 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import * as React from 'react'; +import LargeSubtree from './LargeSubtree'; + +export default function Home(): React.Node { + return ( +
    + +
    + ); +} diff --git a/packages/react-devtools-shell/src/perf-regression/devtools.js b/packages/react-devtools-shell/src/perf-regression/devtools.js new file mode 100644 index 000000000000..424895e41b1c --- /dev/null +++ b/packages/react-devtools-shell/src/perf-regression/devtools.js @@ -0,0 +1,54 @@ +import * as React from 'react'; +import {createRoot} from 'react-dom/client'; +import { + activate as activateBackend, + initialize as initializeBackend, +} from 'react-devtools-inline/backend'; +import {initialize as createDevTools} from 'react-devtools-inline/frontend'; + +// This is a pretty gross hack to make the runtime loaded named-hooks-code work. +// TODO (Webpack 5) Hoepfully we can remove this once we upgrade to Webpack 5. +__webpack_public_path__ = '/dist/'; // eslint-disable-line no-undef + +// TODO (Webpack 5) Hopefully we can remove this prop after the Webpack 5 migration. +function hookNamesModuleLoaderFunction() { + return import('react-devtools-inline/hookNames'); +} + +function inject(contentDocument, sourcePath) { + const script = contentDocument.createElement('script'); + script.src = sourcePath; + + ((contentDocument.body: any): HTMLBodyElement).appendChild(script); +} + +function init( + appSource: string, + appIframe: HTMLIFrameElement, + devtoolsContainer: HTMLElement, + loadDevToolsButton: HTMLButtonElement, +) { + const {contentDocument, contentWindow} = appIframe; + + initializeBackend(contentWindow); + + inject(contentDocument, appSource); + + loadDevToolsButton.addEventListener('click', () => { + const DevTools = createDevTools(contentWindow); + createRoot(devtoolsContainer).render( + , + ); + activateBackend(contentWindow); + }); +} + +init( + 'dist/perf-regression-app.js', + document.getElementById('iframe'), + document.getElementById('devtools'), + document.getElementById('load-devtools'), +); diff --git a/packages/react-devtools-shell/webpack.config.js b/packages/react-devtools-shell/webpack.config.js index 26d27a08a7cf..e1a30f733444 100644 --- a/packages/react-devtools-shell/webpack.config.js +++ b/packages/react-devtools-shell/webpack.config.js @@ -157,6 +157,8 @@ const app = makeConfig( 'multi-devtools': './src/multi/devtools.js', 'multi-right': './src/multi/right.js', 'e2e-regression': './src/e2e-regression/app.js', + 'perf-regression-app': './src/perf-regression/app.js', + 'perf-regression-devtools': './src/perf-regression/devtools.js', }, { react: resolve(builtModulesDir, 'react'), diff --git a/packages/react-devtools-timeline/package.json b/packages/react-devtools-timeline/package.json index 1f86b2540271..0dfacabdd915 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": "4.27.1", + "version": "4.27.4", "license": "MIT", "dependencies": { "@elg/speedscope": "1.9.0-a6f84db", diff --git a/packages/react-devtools-timeline/src/CanvasPage.js b/packages/react-devtools-timeline/src/CanvasPage.js index 8d9334e05ef0..5172b7d5d2b0 100644 --- a/packages/react-devtools-timeline/src/CanvasPage.js +++ b/packages/react-devtools-timeline/src/CanvasPage.js @@ -7,7 +7,7 @@ * @flow */ -import type {Point} from './view-base'; +import type {Interaction, Point} from './view-base'; import type { ReactEventInfo, TimelineData, @@ -199,16 +199,16 @@ function AutoSizedCanvas({ }, [searchIndex, searchRegExp, searchResults, viewState]); const surfaceRef = useRef(new Surface(resetHoveredEvent)); - const userTimingMarksViewRef = useRef(null); - const nativeEventsViewRef = useRef(null); - const schedulingEventsViewRef = useRef(null); - const suspenseEventsViewRef = useRef(null); - const componentMeasuresViewRef = useRef(null); - const reactMeasuresViewRef = useRef(null); - const flamechartViewRef = useRef(null); - const networkMeasuresViewRef = useRef(null); - const snapshotsViewRef = useRef(null); - const thrownErrorsViewRef = useRef(null); + const userTimingMarksViewRef = useRef(null); + const nativeEventsViewRef = useRef(null); + const schedulingEventsViewRef = useRef(null); + const suspenseEventsViewRef = useRef(null); + const componentMeasuresViewRef = useRef(null); + const reactMeasuresViewRef = useRef(null); + const flamechartViewRef = useRef(null); + const networkMeasuresViewRef = useRef(null); + const snapshotsViewRef = useRef(null); + const thrownErrorsViewRef = useRef(null); const {hideMenu: hideContextMenu} = useContext(RegistryContext); @@ -484,7 +484,7 @@ function AutoSizedCanvas({ } }, [width, height]); - const interactor = useCallback(interaction => { + const interactor = useCallback((interaction: Interaction) => { const canvas = canvasRef.current; if (canvas === null) { return; @@ -794,9 +794,9 @@ function AutoSizedCanvas({ copy( - `line ${flamechartStackFrame.locationLine ?? - ''}, column ${flamechartStackFrame.locationColumn ?? - ''}`, + `line ${ + flamechartStackFrame.locationLine ?? '' + }, column ${flamechartStackFrame.locationColumn ?? ''}`, ) } title="Copy location"> diff --git a/packages/react-devtools-timeline/src/EventTooltip.js b/packages/react-devtools-timeline/src/EventTooltip.js index d5e4550a4422..ba3685f646fb 100644 --- a/packages/react-devtools-timeline/src/EventTooltip.js +++ b/packages/react-devtools-timeline/src/EventTooltip.js @@ -15,11 +15,12 @@ import type { ReactComponentMeasure, ReactEventInfo, ReactMeasure, - TimelineData, + ReactMeasureType, SchedulingEvent, Snapshot, SuspenseEvent, ThrownError, + TimelineData, UserTimingMark, } from './types'; @@ -45,7 +46,7 @@ type Props = { width: number, }; -function getReactMeasureLabel(type): string | null { +function getReactMeasureLabel(type: ReactMeasureType): string | null { switch (type) { case 'commit': return 'react commit'; diff --git a/packages/react-devtools-timeline/src/Timeline.js b/packages/react-devtools-timeline/src/Timeline.js index 5ed6a0f7443f..482375a46f92 100644 --- a/packages/react-devtools-timeline/src/Timeline.js +++ b/packages/react-devtools-timeline/src/Timeline.js @@ -33,13 +33,8 @@ import {TimelineSearchContextController} from './TimelineSearchContext'; import styles from './Timeline.css'; export function Timeline(_: {}): React.Node { - const { - file, - inMemoryTimelineData, - isTimelineSupported, - setFile, - viewState, - } = useContext(TimelineContext); + const {file, inMemoryTimelineData, isTimelineSupported, setFile, viewState} = + useContext(TimelineContext); const {didRecordCommits, isProfiling} = useContext(ProfilerContext); const ref = useRef(null); @@ -117,6 +112,7 @@ const ProcessingData = () => (
    ); +// $FlowFixMe[missing-local-annot] const CouldNotLoadProfile = ({error, onFileSelect}) => (
    Could not load profile
    diff --git a/packages/react-devtools-timeline/src/TimelineContext.js b/packages/react-devtools-timeline/src/TimelineContext.js index 1d64d545fb7e..83813eb267a9 100644 --- a/packages/react-devtools-timeline/src/TimelineContext.js +++ b/packages/react-devtools-timeline/src/TimelineContext.js @@ -82,8 +82,10 @@ function TimelineContextController({children}: Props): React.Node { // Recreate view state any time new profiling data is imported. const viewState = useMemo(() => { - const horizontalScrollStateChangeCallbacks: Set = new Set(); - const searchRegExpStateChangeCallbacks: Set = new Set(); + const horizontalScrollStateChangeCallbacks: Set = + new Set(); + const searchRegExpStateChangeCallbacks: Set = + new Set(); const horizontalScrollState = { offset: 0, diff --git a/packages/react-devtools-timeline/src/TimelineSearchContext.js b/packages/react-devtools-timeline/src/TimelineSearchContext.js index 830f98f1c35f..604dbf180607 100644 --- a/packages/react-devtools-timeline/src/TimelineSearchContext.js +++ b/packages/react-devtools-timeline/src/TimelineSearchContext.js @@ -40,7 +40,7 @@ type Action = type Dispatch = (action: Action) => void; -const EMPTY_ARRAY = []; +const EMPTY_ARRAY: Array = []; function reducer(state: State, action: Action): State { let {searchIndex, searchRegExp, searchResults, searchText} = state; diff --git a/packages/react-devtools-timeline/src/TimelineSearchInput.js b/packages/react-devtools-timeline/src/TimelineSearchInput.js index 2f794a229639..c448bded2a3a 100644 --- a/packages/react-devtools-timeline/src/TimelineSearchInput.js +++ b/packages/react-devtools-timeline/src/TimelineSearchInput.js @@ -26,7 +26,8 @@ export default function TimelineSearchInput(props: Props): React.Node { return null; } - const search = text => dispatch({type: 'SET_SEARCH_TEXT', payload: text}); + const search = (text: string) => + dispatch({type: 'SET_SEARCH_TEXT', payload: text}); const goToNextResult = () => dispatch({type: 'GO_TO_NEXT_SEARCH_RESULT'}); const goToPreviousResult = () => dispatch({type: 'GO_TO_PREVIOUS_SEARCH_RESULT'}); diff --git a/packages/react-devtools-timeline/src/content-views/ComponentMeasuresView.js b/packages/react-devtools-timeline/src/content-views/ComponentMeasuresView.js index 085c2ecc3b1a..0e4a5727e20b 100644 --- a/packages/react-devtools-timeline/src/content-views/ComponentMeasuresView.js +++ b/packages/react-devtools-timeline/src/content-views/ComponentMeasuresView.js @@ -92,13 +92,8 @@ export class ComponentMeasuresView extends View { showHoverHighlight: boolean, ): boolean { const {frame} = this; - const { - componentName, - duration, - timestamp, - type, - warning, - } = componentMeasure; + const {componentName, duration, timestamp, type, warning} = + componentMeasure; const xStart = timestampToPosition(timestamp, scaleFactor, frame); const xStop = timestampToPosition(timestamp + duration, scaleFactor, frame); diff --git a/packages/react-devtools-timeline/src/content-views/NativeEventsView.js b/packages/react-devtools-timeline/src/content-views/NativeEventsView.js index ce2cdc652812..5bb88bce797b 100644 --- a/packages/react-devtools-timeline/src/content-views/NativeEventsView.js +++ b/packages/react-devtools-timeline/src/content-views/NativeEventsView.js @@ -65,7 +65,7 @@ export class NativeEventsView extends View { if (!this._depthToNativeEvent.has(depth)) { this._depthToNativeEvent.set(depth, [event]); } else { - // $FlowFixMe This is unnecessary. + // $FlowFixMe[incompatible-use] This is unnecessary. this._depthToNativeEvent.get(depth).push(event); } }); diff --git a/packages/react-devtools-timeline/src/content-views/NetworkMeasuresView.js b/packages/react-devtools-timeline/src/content-views/NetworkMeasuresView.js index 64e790d79bff..f2563852339b 100644 --- a/packages/react-devtools-timeline/src/content-views/NetworkMeasuresView.js +++ b/packages/react-devtools-timeline/src/content-views/NetworkMeasuresView.js @@ -68,7 +68,7 @@ export class NetworkMeasuresView extends View { if (!this._depthToNetworkMeasure.has(depth)) { this._depthToNetworkMeasure.set(depth, [event]); } else { - // $FlowFixMe This is unnecessary. + // $FlowFixMe[incompatible-use] This is unnecessary. this._depthToNetworkMeasure.get(depth).push(event); } }); diff --git a/packages/react-devtools-timeline/src/content-views/ReactMeasuresView.js b/packages/react-devtools-timeline/src/content-views/ReactMeasuresView.js index 27d65c55fa40..ad4cc1f28249 100644 --- a/packages/react-devtools-timeline/src/content-views/ReactMeasuresView.js +++ b/packages/react-devtools-timeline/src/content-views/ReactMeasuresView.js @@ -95,7 +95,7 @@ export class ReactMeasuresView extends View { scaleFactor: number, showGroupHighlight: boolean, showHoverHighlight: boolean, - ) { + ): void { const {frame, visibleArea} = this; const {timestamp, type, duration} = measure; @@ -202,14 +202,9 @@ export class ReactMeasuresView extends View { } } - draw(context: CanvasRenderingContext2D) { - const { - frame, - _hoveredMeasure, - _lanesToRender, - _profilerData, - visibleArea, - } = this; + draw(context: CanvasRenderingContext2D): void { + const {frame, _hoveredMeasure, _lanesToRender, _profilerData, visibleArea} = + this; context.fillStyle = COLORS.PRIORITY_BACKGROUND; context.fillRect( diff --git a/packages/react-devtools-timeline/src/content-views/SnapshotsView.js b/packages/react-devtools-timeline/src/content-views/SnapshotsView.js index 554d2c967131..494cca38c4fc 100644 --- a/packages/react-devtools-timeline/src/content-views/SnapshotsView.js +++ b/packages/react-devtools-timeline/src/content-views/SnapshotsView.js @@ -166,7 +166,7 @@ export class SnapshotsView extends View { imageRect.size.height, ); - // $FlowFixMe Flow doesn't know about the 9 argument variant of drawImage() + // $FlowFixMe[incompatible-call] Flow doesn't know about the 9 argument variant of drawImage() context.drawImage( snapshot.image, diff --git a/packages/react-devtools-timeline/src/content-views/SuspenseEventsView.js b/packages/react-devtools-timeline/src/content-views/SuspenseEventsView.js index 875aa43ea2f8..43df27e8c92b 100644 --- a/packages/react-devtools-timeline/src/content-views/SuspenseEventsView.js +++ b/packages/react-devtools-timeline/src/content-views/SuspenseEventsView.js @@ -72,7 +72,7 @@ export class SuspenseEventsView extends View { if (!this._depthToSuspenseEvent.has(depth)) { this._depthToSuspenseEvent.set(depth, [event]); } else { - // $FlowFixMe This is unnecessary. + // $FlowFixMe[incompatible-use] This is unnecessary. this._depthToSuspenseEvent.get(depth).push(event); } }); diff --git a/packages/react-devtools-timeline/src/content-views/constants.js b/packages/react-devtools-timeline/src/content-views/constants.js index c683cf3cfaef..ec1156605c2f 100644 --- a/packages/react-devtools-timeline/src/content-views/constants.js +++ b/packages/react-devtools-timeline/src/content-views/constants.js @@ -27,18 +27,7 @@ export const TEXT_PADDING = 3; export const SNAPSHOT_SCRUBBER_SIZE = 3; export const INTERVAL_TIMES = [ - 1, - 2, - 5, - 10, - 20, - 50, - 100, - 200, - 500, - 1000, - 2000, - 5000, + 1, 2, 5, 10, 20, 50, 100, 200, 500, 1000, 2000, 5000, ]; export const MIN_INTERVAL_SIZE_PX = 70; diff --git a/packages/react-devtools-timeline/src/content-views/utils/__tests__/colors-test.js b/packages/react-devtools-timeline/src/content-views/utils/__tests__/colors-test.js index ccfb3cee4bd5..517cd3b66fee 100644 --- a/packages/react-devtools-timeline/src/content-views/utils/__tests__/colors-test.js +++ b/packages/react-devtools-timeline/src/content-views/utils/__tests__/colors-test.js @@ -41,7 +41,7 @@ describe(ColorGenerator, () => { describe(ColorGenerator.prototype.colorForID, () => { it('should generate a color for an ID', () => { expect(new ColorGenerator().colorForID('123')).toMatchInlineSnapshot(` - Object { + { "a": 1, "h": 190, "l": 80, diff --git a/packages/react-devtools-timeline/src/content-views/utils/text.js b/packages/react-devtools-timeline/src/content-views/utils/text.js index b0b71639c93e..2305975153c6 100644 --- a/packages/react-devtools-timeline/src/content-views/utils/text.js +++ b/packages/react-devtools-timeline/src/content-views/utils/text.js @@ -12,7 +12,7 @@ import type {Rect} from '../../view-base'; import {rectEqualToRect} from '../../view-base'; import {COLORS, FONT_SIZE, TEXT_PADDING} from '../constants'; -const cachedTextWidths = new Map(); +const cachedTextWidths = new Map(); export function getTextWidth( context: CanvasRenderingContext2D, diff --git a/packages/react-devtools-timeline/src/import-worker/importFile.js b/packages/react-devtools-timeline/src/import-worker/importFile.js index 78a968465bd1..c57f436e6fde 100644 --- a/packages/react-devtools-timeline/src/import-worker/importFile.js +++ b/packages/react-devtools-timeline/src/import-worker/importFile.js @@ -16,8 +16,6 @@ import preprocessData from './preprocessData'; import {readInputData} from './readInputData'; import InvalidProfileError from './InvalidProfileError'; -declare var self: DedicatedWorkerGlobalScope; - export async function importFile(file: File): Promise { try { const readFile = await readInputData(file); diff --git a/packages/react-devtools-timeline/src/import-worker/preprocessData.js b/packages/react-devtools-timeline/src/import-worker/preprocessData.js index 70a57acb957b..82b6a37129bc 100644 --- a/packages/react-devtools-timeline/src/import-worker/preprocessData.js +++ b/packages/react-devtools-timeline/src/import-worker/preprocessData.js @@ -207,7 +207,7 @@ function markWorkCompleted( console.error('Could not find matching measure for type "%s".', type); } - // $FlowFixMe This property should not be writable outside of this function. + // $FlowFixMe[cannot-write] This property should not be writable outside of this function. measure.duration = stopTime - startTime; } @@ -370,7 +370,7 @@ function processScreenshot( fetch(snapshot.imageSource) .then(response => response.blob()) .then(blob => { - // $FlowFixMe createImageBitmap + // $FlowFixMe[cannot-resolve-name] createImageBitmap createImageBitmap(blob).then(bitmap => { snapshot.height = bitmap.height; snapshot.width = bitmap.width; @@ -391,7 +391,7 @@ function processResourceSendRequest( const data = event.args.data; const requestId = data.requestId; - const availableDepths = new Array( + const availableDepths = new Array( state.requestIdToNetworkMeasureMap.size + 1, ).fill(true); state.requestIdToNetworkMeasureMap.forEach(({depth}) => { @@ -552,16 +552,12 @@ function processTimelineEvent( type: 'thrown-error', }); } else if (name.startsWith('--suspense-suspend-')) { - const [ - id, - componentName, - phase, - laneBitmaskString, - promiseName, - ] = name.substr(19).split('-'); + const [id, componentName, phase, laneBitmaskString, promiseName] = name + .substr(19) + .split('-'); const lanes = getLanesFromTransportDecimalBitmask(laneBitmaskString); - const availableDepths = new Array( + const availableDepths = new Array( state.unresolvedSuspenseEvents.size + 1, ).fill(true); state.unresolvedSuspenseEvents.forEach(({depth}) => { @@ -978,14 +974,22 @@ function preprocessFlamechart(rawData: TimelineEvent[]): Flamechart { }); const flamechart: Flamechart = speedscopeFlamechart.getLayers().map(layer => - layer.map(({start, end, node: {frame: {name, file, line, col}}}) => ({ - name, - timestamp: start / 1000, - duration: (end - start) / 1000, - scriptUrl: file, - locationLine: line, - locationColumn: col, - })), + layer.map( + ({ + start, + end, + node: { + frame: {name, file, line, col}, + }, + }) => ({ + name, + timestamp: start / 1000, + duration: (end - start) / 1000, + scriptUrl: file, + locationLine: line, + locationColumn: col, + }), + ), ); return flamechart; @@ -1005,7 +1009,7 @@ export default async function preprocessData( ): Promise { const flamechart = preprocessFlamechart(timeline); - const laneToReactMeasureMap = new Map(); + const laneToReactMeasureMap: Map> = new Map(); for (let lane: ReactLane = 0; lane < REACT_TOTAL_NUM_LANES; lane++) { laneToReactMeasureMap.set(lane, []); } diff --git a/packages/react-devtools-timeline/src/timelineCache.js b/packages/react-devtools-timeline/src/timelineCache.js index 495328d7441e..b6e7e6076e24 100644 --- a/packages/react-devtools-timeline/src/timelineCache.js +++ b/packages/react-devtools-timeline/src/timelineCache.js @@ -55,9 +55,9 @@ export function importFile(file: File): TimelineData | Error { let record = fileNameToProfilerDataMap.get(fileName); if (!record) { - const callbacks = new Set(); + const callbacks = new Set<() => mixed>(); const wakeable: Wakeable = { - then(callback) { + then(callback: () => mixed) { callbacks.add(callback); }, @@ -79,7 +79,8 @@ export function importFile(file: File): TimelineData | Error { importFileWorker(file).then(data => { switch (data.status) { case 'SUCCESS': - const resolvedRecord = ((newRecord: any): ResolvedRecord); + const resolvedRecord = + ((newRecord: any): ResolvedRecord); resolvedRecord.status = Resolved; resolvedRecord.value = data.processedData; break; diff --git a/packages/react-devtools-timeline/src/utils/useSmartTooltip.js b/packages/react-devtools-timeline/src/utils/useSmartTooltip.js index a578e45abc45..73c6ce246d71 100644 --- a/packages/react-devtools-timeline/src/utils/useSmartTooltip.js +++ b/packages/react-devtools-timeline/src/utils/useSmartTooltip.js @@ -44,9 +44,9 @@ export default function useSmartTooltip({ // mouse cursor or finally aligned with the window's top edge. if (mouseY - TOOLTIP_OFFSET_TOP - element.offsetHeight > 0) { // We position the tooltip above the mouse cursor if it fits there. - element.style.top = `${mouseY - - element.offsetHeight - - TOOLTIP_OFFSET_TOP}px`; + element.style.top = `${ + mouseY - element.offsetHeight - TOOLTIP_OFFSET_TOP + }px`; } else { // Otherwise we align the tooltip with the window's top edge. element.style.top = '0px'; @@ -64,9 +64,9 @@ export default function useSmartTooltip({ if (mouseX - TOOLTIP_OFFSET_TOP - element.offsetWidth > 0) { // We position the tooltip at the left of the mouse cursor if it fits // there. - element.style.left = `${mouseX - - element.offsetWidth - - TOOLTIP_OFFSET_TOP}px`; + element.style.left = `${ + mouseX - element.offsetWidth - TOOLTIP_OFFSET_TOP + }px`; } else { // Otherwise, align the tooltip with the window's left edge. element.style.left = '0px'; diff --git a/packages/react-devtools-timeline/src/view-base/Surface.js b/packages/react-devtools-timeline/src/view-base/Surface.js index 67cc5c2b8b70..4708cd95f787 100644 --- a/packages/react-devtools-timeline/src/view-base/Surface.js +++ b/packages/react-devtools-timeline/src/view-base/Surface.js @@ -22,7 +22,11 @@ export type ViewRefs = { }; // hidpi canvas: https://www.html5rocks.com/en/tutorials/canvas/hidpi/ -function configureRetinaCanvas(canvas, height, width) { +function configureRetinaCanvas( + canvas: HTMLCanvasElement, + height: number, + width: number, +) { canvas.width = width * DPR; canvas.height = height * DPR; canvas.style.width = `${width}px`; diff --git a/packages/react-devtools-timeline/src/view-base/utils/__tests__/scrollState-test.js b/packages/react-devtools-timeline/src/view-base/utils/__tests__/scrollState-test.js index 306caffa6076..0241ea3f14a5 100644 --- a/packages/react-devtools-timeline/src/view-base/utils/__tests__/scrollState-test.js +++ b/packages/react-devtools-timeline/src/view-base/utils/__tests__/scrollState-test.js @@ -210,7 +210,7 @@ describe(zoomState, () => { }); expect(zoomedState).toMatchInlineSnapshot(` - Object { + { "length": 200, "offset": -50, } @@ -232,7 +232,7 @@ describe(moveStateToRange, () => { }); expect(movedState).toMatchInlineSnapshot(` - Object { + { "length": 400, "offset": -50, } diff --git a/packages/react-devtools/CHANGELOG.md b/packages/react-devtools/CHANGELOG.md index aefd1dbb7370..3514c7ed7b5b 100644 --- a/packages/react-devtools/CHANGELOG.md +++ b/packages/react-devtools/CHANGELOG.md @@ -4,6 +4,38 @@ --- +### 4.27.4 +March 24, 2023 + +#### Bugfixes +* missing file name in react-devtools package.json for electron ([mondaychen](https://github.com/mondaychen) in [#26469](https://github.com/facebook/react/pull/26469)) + +--- + +### 4.27.3 +March 22, 2023 + +#### Bugfixes +* prevent StyleX plugin from throwing when inspecting CSS ([mondaychen](https://github.com/mondaychen) in [#26364](https://github.com/facebook/react/pull/26364)) +* remove script tag immediately ([mondaychen](https://github.com/mondaychen) in [#26233](https://github.com/facebook/react/pull/26233)) + +#### Others +* upgrade electron to latest version & security improvements ([mondaychen](https://github.com/mondaychen) in [#26337](https://github.com/facebook/react/pull/26337)) +* improve troubleshooting in README ([mondaychen](https://github.com/mondaychen) in [#26235](https://github.com/facebook/react/pull/26235)) +* Remove renderer.js from extension build ([mondaychen](https://github.com/mondaychen) in [#26234](https://github.com/facebook/react/pull/26234)) +* permanently polyfill for rAF in devtools_page ([mondaychen](https://github.com/mondaychen) in [#26193](https://github.com/facebook/react/pull/26193)) + +--- + +### 4.27.2 +February 16, 2023 + +* Replace DevTools `semver` usages with `compare-versions` for smaller bundle size ([markerikson](https://github.com/markerikson) in [#26122](https://github.com/facebook/react/pull/26122)) +* Support highlights for React Native apps in dev tools ([ryancat](https://github.com/ryancat) in [#26060](https://github.com/facebook/react/pull/26060)) +* improve error handling in extension ([mondaychen](https://github.com/mondaychen) in [#26068](https://github.com/facebook/react/pull/26068)) + +--- + ### 4.27.1 December 6, 2022 diff --git a/packages/react-devtools/README.md b/packages/react-devtools/README.md index 60e31025c01a..149ec97f8af5 100644 --- a/packages/react-devtools/README.md +++ b/packages/react-devtools/README.md @@ -105,12 +105,17 @@ Or you could develop with a local HTTP server like [`serve`](https://www.npmjs.c **If your app is inside an iframe, a Chrome extension, React Native, or in another unusual environment**, try [the standalone version instead](https://github.com/facebook/react/tree/main/packages/react-devtools). Chrome apps are currently not inspectable. -**If your Components tab is empty, refer to "Chrome v101 and earlier" section below**, please read the "the issue with Chrome v101 and earlier versions" part below. +**If your Components tab is empty, refer to "The React tab shows no components" section below**. **If you still have issues** please [report them](https://github.com/facebook/react/issues/new?labels=Component:%20Developer%20Tools). Don't forget to specify your OS, browser version, extension version, and the exact instructions to reproduce the issue with a screenshot. -### The Issue with Chrome v101 and earlier -As we migrate to a Chrome Extension Manifest V3, we start to use a new method to hook the DevTools with the inspected page. This new method is more secure, but relies on a new API that's only supported in Chrome v102+. For Chrome v101 or earlier, we use a fallback method, which can cause malfunctions (e.g. empty React component tab) if the JS resources on your page is loaded from cache. Please upgrade to Chrome v102+ to avoid this issue. +### The React tab shows no components + +#### The Issue with Chrome v101 and earlier +As we migrate to a Chrome Extension Manifest V3, we start to use a new method to hook the DevTools with the inspected page. This new method is more secure, but relies on a new API that's only supported in Chrome v102+. For Chrome v101 or earlier, we use a fallback method, which can cause malfunctions (e.g. failure to load React Elements in the Components tab) if the JS resources on your page is loaded from cache. Please upgrade to Chrome v102+ to avoid this issue. + +#### Service Worker malfunction +Go to chrome://extensions. If you see "service worker (inactive)" in the React Developer Tools extension, try disabling and re-enabling the extension. This will restart the service worker. Then go to the page you want to inspect, close the DevTools, and reload the page. Open the DevTools again and the React components tab should be working. ## Local development The standalone DevTools app can be built and tested from source following the instructions below. diff --git a/packages/react-devtools/app.html b/packages/react-devtools/app.html index b60ef3c094b8..7b55e0d37db9 100644 --- a/packages/react-devtools/app.html +++ b/packages/react-devtools/app.html @@ -156,28 +156,11 @@
    '); * While untrusted script content should be made safe before using this api it will * ensure that the script cannot be early terminated or never terminated state */ -function escapeBootstrapScriptContent(scriptText) { +function escapeBootstrapScriptContent(scriptText: string) { if (__DEV__) { checkHtmlStringCoercion(scriptText); } return ('' + scriptText).replace(scriptRegex, scriptReplacer); } const scriptRegex = /(<\/|<)(s)(cript)/gi; -const scriptReplacer = (match, prefix, s, suffix) => - `${prefix}${s === 's' ? '\\u0073' : '\\u0053'}${suffix}`; +const scriptReplacer = ( + match: string, + prefix: string, + s: string, + suffix: string, +) => `${prefix}${s === 's' ? '\\u0073' : '\\u0053'}${suffix}`; export type BootstrapScriptDescriptor = { src: string, @@ -187,7 +219,7 @@ export function createResponseState( : stringToPrecomputedChunk( '"`, + `"
    hello world
    "`, ); }); @@ -204,9 +211,7 @@ describe('ReactDOMFizzServerBrowser', () => { const result = await readResult(stream); expect(result).toContain('Loading'); - expect(errors).toEqual([ - 'The render was aborted by the server without a reason.', - ]); + expect(errors).toEqual(['The operation was aborted.']); }); it('should reject if aborting before the shell is complete', async () => { @@ -227,10 +232,6 @@ describe('ReactDOMFizzServerBrowser', () => { await jest.runAllTimers(); const theReason = new Error('aborted for reasons'); - // @TODO this is a hack to work around lack of support for abortSignal.reason in node - // The abort call itself should set this property but since we are testing in node we - // set it here manually - controller.signal.reason = theReason; controller.abort(theReason); let caughtError = null; @@ -272,22 +273,14 @@ describe('ReactDOMFizzServerBrowser', () => { } catch (error) { caughtError = error; } - expect(caughtError.message).toBe( - 'The render was aborted by the server without a reason.', - ); - expect(errors).toEqual([ - 'The render was aborted by the server without a reason.', - ]); + expect(caughtError.message).toBe('The operation was aborted.'); + expect(errors).toEqual(['The operation was aborted.']); }); it('should reject if passing an already aborted signal', async () => { const errors = []; const controller = new AbortController(); const theReason = new Error('aborted for reasons'); - // @TODO this is a hack to work around lack of support for abortSignal.reason in node - // The abort call itself should set this property but since we are testing in node we - // set it here manually - controller.signal.reason = theReason; controller.abort(theReason); const promise = ReactDOMFizzServer.renderToReadableStream( @@ -435,10 +428,6 @@ describe('ReactDOMFizzServerBrowser', () => { }, }); - // @TODO this is a hack to work around lack of support for abortSignal.reason in node - // The abort call itself should set this property but since we are testing in node we - // set it here manually - controller.signal.reason = 'foobar'; controller.abort('foobar'); expect(errors).toEqual(['foobar', 'foobar']); @@ -476,10 +465,6 @@ describe('ReactDOMFizzServerBrowser', () => { }, }); - // @TODO this is a hack to work around lack of support for abortSignal.reason in node - // The abort call itself should set this property but since we are testing in node we - // set it here manually - controller.signal.reason = new Error('uh oh'); controller.abort(new Error('uh oh')); expect(errors).toEqual(['uh oh', 'uh oh']); diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzServerNode-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzServerNode-test.js index a5b2b5ac417a..11c7043e2b9d 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFizzServerNode-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFizzServerNode-test.js @@ -73,9 +73,16 @@ describe('ReactDOMFizzServerNode', () => { ); pipe(writable); jest.runAllTimers(); - expect(output.result).toMatchInlineSnapshot( - `"hello world"`, - ); + if (gate(flags => flags.enableFloat)) { + // with Float, we emit empty heads if they are elided when rendering + expect(output.result).toMatchInlineSnapshot( + `"hello world"`, + ); + } else { + expect(output.result).toMatchInlineSnapshot( + `"hello world"`, + ); + } }); it('should emit bootstrap script src at the end', () => { @@ -91,7 +98,7 @@ describe('ReactDOMFizzServerNode', () => { pipe(writable); jest.runAllTimers(); expect(output.result).toMatchInlineSnapshot( - `"
    hello world
    "`, + `"
    hello world
    "`, ); }); @@ -628,4 +635,17 @@ describe('ReactDOMFizzServerNode', () => { expect(rendered).toBe(false); expect(isComplete).toBe(true); }); + + it('should encode multibyte characters correctly without nulls (#24985)', () => { + const {writable, output} = getTestWritable(); + const {pipe} = ReactDOMFizzServer.renderToPipeableStream( +
    {Array(700).fill('ののの')}
    , + ); + pipe(writable); + jest.runAllTimers(); + expect(output.result.indexOf('\u0000')).toBe(-1); + expect(output.result).toEqual( + '
    ' + Array(700).fill('ののの').join('') + '
    ', + ); + }); }); diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzShellHydration-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzShellHydration-test.js index 98f0fba9b89f..3c2260d83bd6 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFizzShellHydration-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFizzShellHydration-test.js @@ -5,6 +5,7 @@ * LICENSE file in the root directory of this source tree. * * @emails react-core + * @jest-environment ./scripts/jest/ReactDOMServerIntegrationEnvironment */ let JSDOM; @@ -22,6 +23,7 @@ let buffer = ''; let hasErrored = false; let fatalError = undefined; let textCache; +let assertLog; describe('ReactDOMFizzShellHydration', () => { beforeEach(() => { @@ -30,10 +32,13 @@ describe('ReactDOMFizzShellHydration', () => { React = require('react'); ReactDOMClient = require('react-dom/client'); Scheduler = require('scheduler'); - clientAct = require('jest-react').act; + clientAct = require('internal-test-utils').act; ReactDOMFizzServer = require('react-dom/server'); Stream = require('stream'); + const InternalTestUtils = require('internal-test-utils'); + assertLog = InternalTestUtils.assertLog; + startTransition = React.startTransition; textCache = new Map(); @@ -62,6 +67,10 @@ describe('ReactDOMFizzShellHydration', () => { }); }); + afterEach(() => { + jest.restoreAllMocks(); + }); + async function serverAct(callback) { await callback(); // Await one turn around the event loop. @@ -120,7 +129,7 @@ describe('ReactDOMFizzShellHydration', () => { return record.value; } } else { - Scheduler.unstable_yieldValue(`Suspend! [${text}]`); + Scheduler.log(`Suspend! [${text}]`); const thenable = { pings: [], @@ -144,13 +153,13 @@ describe('ReactDOMFizzShellHydration', () => { } function Text({text}) { - Scheduler.unstable_yieldValue(text); + Scheduler.log(text); return text; } function AsyncText({text}) { readText(text); - Scheduler.unstable_yieldValue(text); + Scheduler.log(text); return text; } @@ -175,7 +184,7 @@ describe('ReactDOMFizzShellHydration', () => { const {pipe} = ReactDOMFizzServer.renderToPipeableStream(); pipe(writable); }); - expect(Scheduler).toHaveYielded(['Shell']); + assertLog(['Shell']); const dehydratedDiv = container.getElementsByTagName('div')[0]; // Clear the cache and start rendering on the client @@ -185,7 +194,7 @@ describe('ReactDOMFizzShellHydration', () => { await clientAct(async () => { ReactDOMClient.hydrateRoot(container, ); }); - expect(Scheduler).toHaveYielded(['Suspend! [Shell]']); + assertLog(['Suspend! [Shell]']); expect(div.current).toBe(null); expect(container.textContent).toBe('Shell'); @@ -193,7 +202,7 @@ describe('ReactDOMFizzShellHydration', () => { await clientAct(async () => { await resolveText('Shell'); }); - expect(Scheduler).toHaveYielded(['Shell']); + assertLog(['Shell']); expect(div.current).toBe(dehydratedDiv); expect(container.textContent).toBe('Shell'); }); @@ -208,12 +217,12 @@ describe('ReactDOMFizzShellHydration', () => { await clientAct(async () => { root.render(); }); - expect(Scheduler).toHaveYielded(['Suspend! [Shell]']); + assertLog(['Suspend! [Shell]']); await clientAct(async () => { await resolveText('Shell'); }); - expect(Scheduler).toHaveYielded(['Shell']); + assertLog(['Shell']); expect(container.textContent).toBe('Shell'); }); @@ -231,7 +240,7 @@ describe('ReactDOMFizzShellHydration', () => { const {pipe} = ReactDOMFizzServer.renderToPipeableStream(); pipe(writable); }); - expect(Scheduler).toHaveYielded(['Initial']); + assertLog(['Initial']); await clientAct(async () => { const root = ReactDOMClient.hydrateRoot(container, ); @@ -241,7 +250,7 @@ describe('ReactDOMFizzShellHydration', () => { root.render(); }); }); - expect(Scheduler).toHaveYielded(['Initial', 'Updated']); + assertLog(['Initial', 'Updated']); expect(container.textContent).toBe('Updated'); }, ); @@ -257,7 +266,7 @@ describe('ReactDOMFizzShellHydration', () => { const {pipe} = ReactDOMFizzServer.renderToPipeableStream(); pipe(writable); }); - expect(Scheduler).toHaveYielded(['Shell']); + assertLog(['Shell']); // Clear the cache and start rendering on the client resetTextCache(); @@ -266,21 +275,43 @@ describe('ReactDOMFizzShellHydration', () => { const root = await clientAct(async () => { return ReactDOMClient.hydrateRoot(container, , { onRecoverableError(error) { - Scheduler.unstable_yieldValue(error.message); + Scheduler.log(error.message); }, }); }); - expect(Scheduler).toHaveYielded(['Suspend! [Shell]']); + assertLog(['Suspend! [Shell]']); expect(container.textContent).toBe('Shell'); await clientAct(async () => { root.render(); }); - expect(Scheduler).toHaveYielded([ + assertLog([ 'New screen', 'This root received an early update, before anything was able ' + 'hydrate. Switched the entire root to client rendering.', ]); expect(container.textContent).toBe('New screen'); }); + + test('TODO: A large component stack causes SSR to stack overflow', async () => { + spyOnDevAndProd(console, 'error').mockImplementation(() => {}); + + function NestedComponent({depth}: {depth: number}) { + if (depth <= 0) { + return ; + } + return ; + } + + // Server render + await serverAct(async () => { + ReactDOMFizzServer.renderToPipeableStream( + , + ); + }); + expect(console.error).toHaveBeenCalledTimes(1); + expect(console.error.mock.calls[0][0].toString()).toBe( + 'RangeError: Maximum call stack size exceeded', + ); + }); }); diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzStatic-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzStatic-test.js index 025a3144601a..def4261daef3 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFizzStatic-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFizzStatic-test.js @@ -5,6 +5,7 @@ * LICENSE file in the root directory of this source tree. * * @emails react-core + * @jest-environment ./scripts/jest/ReactDOMServerIntegrationEnvironment */ 'use strict'; diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzStaticBrowser-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzStaticBrowser-test.js index d55b11154f28..dc15ddc58aab 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFizzStaticBrowser-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFizzStaticBrowser-test.js @@ -10,7 +10,8 @@ 'use strict'; // Polyfills for test environment -global.ReadableStream = require('web-streams-polyfill/ponyfill/es6').ReadableStream; +global.ReadableStream = + require('web-streams-polyfill/ponyfill/es6').ReadableStream; global.TextEncoder = require('util').TextEncoder; let React; @@ -63,9 +64,15 @@ describe('ReactDOMFizzStaticBrowser', () => { , ); const prelude = await readContent(result.prelude); - expect(prelude).toMatchInlineSnapshot( - `"hello world"`, - ); + if (gate(flags => flags.enableFloat)) { + expect(prelude).toMatchInlineSnapshot( + `"hello world"`, + ); + } else { + expect(prelude).toMatchInlineSnapshot( + `"hello world"`, + ); + } }); // @gate experimental @@ -77,7 +84,7 @@ describe('ReactDOMFizzStaticBrowser', () => { }); const prelude = await readContent(result.prelude); expect(prelude).toMatchInlineSnapshot( - `"
    hello world
    "`, + `"
    hello world
    "`, ); }); @@ -207,9 +214,7 @@ describe('ReactDOMFizzStaticBrowser', () => { const prelude = await readContent(result.prelude); expect(prelude).toContain('Loading'); - expect(errors).toEqual([ - 'The render was aborted by the server without a reason.', - ]); + expect(errors).toEqual(['The operation was aborted.']); }); // @gate experimental @@ -231,10 +236,6 @@ describe('ReactDOMFizzStaticBrowser', () => { await jest.runAllTimers(); const theReason = new Error('aborted for reasons'); - // @TODO this is a hack to work around lack of support for abortSignal.reason in node - // The abort call itself should set this property but since we are testing in node we - // set it here manually - controller.signal.reason = theReason; controller.abort(theReason); let caughtError = null; @@ -277,12 +278,8 @@ describe('ReactDOMFizzStaticBrowser', () => { } catch (error) { caughtError = error; } - expect(caughtError.message).toBe( - 'The render was aborted by the server without a reason.', - ); - expect(errors).toEqual([ - 'The render was aborted by the server without a reason.', - ]); + expect(caughtError.message).toBe('The operation was aborted.'); + expect(errors).toEqual(['The operation was aborted.']); }); // @gate experimental @@ -290,10 +287,6 @@ describe('ReactDOMFizzStaticBrowser', () => { const errors = []; const controller = new AbortController(); const theReason = new Error('aborted for reasons'); - // @TODO this is a hack to work around lack of support for abortSignal.reason in node - // The abort call itself should set this property but since we are testing in node we - // set it here manually - controller.signal.reason = theReason; controller.abort(theReason); const promise = ReactDOMFizzStatic.prerender( @@ -355,10 +348,6 @@ describe('ReactDOMFizzStaticBrowser', () => { }, }); - // @TODO this is a hack to work around lack of support for abortSignal.reason in node - // The abort call itself should set this property but since we are testing in node we - // set it here manually - controller.signal.reason = 'foobar'; controller.abort('foobar'); await resultPromise; @@ -399,10 +388,6 @@ describe('ReactDOMFizzStaticBrowser', () => { }, }); - // @TODO this is a hack to work around lack of support for abortSignal.reason in node - // The abort call itself should set this property but since we are testing in node we - // set it here manually - controller.signal.reason = new Error('uh oh'); controller.abort(new Error('uh oh')); await resultPromise; diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzStaticNode-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzStaticNode-test.js index 64b14ef79359..c17d08fb033a 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFizzStaticNode-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFizzStaticNode-test.js @@ -5,11 +5,9 @@ * LICENSE file in the root directory of this source tree. * * @emails react-core + * @jest-environment node */ -// TODO: This should actually run in `@jest-environment node` but we currently -// run an old jest that doesn't support AbortController so we use DOM for now. - 'use strict'; let React; @@ -65,9 +63,15 @@ describe('ReactDOMFizzStaticNode', () => { , ); const prelude = await readContent(result.prelude); - expect(prelude).toMatchInlineSnapshot( - `"hello world"`, - ); + if (gate(flags => flags.enableFloat)) { + expect(prelude).toMatchInlineSnapshot( + `"hello world"`, + ); + } else { + expect(prelude).toMatchInlineSnapshot( + `"hello world"`, + ); + } }); // @gate experimental @@ -82,7 +86,7 @@ describe('ReactDOMFizzStaticNode', () => { ); const prelude = await readContent(result.prelude); expect(prelude).toMatchInlineSnapshot( - `"
    hello world
    "`, + `"
    hello world
    "`, ); }); @@ -212,9 +216,7 @@ describe('ReactDOMFizzStaticNode', () => { const prelude = await readContent(result.prelude); expect(prelude).toContain('Loading'); - expect(errors).toEqual([ - 'The render was aborted by the server without a reason.', - ]); + expect(errors).toEqual(['This operation was aborted']); }); // @gate experimental @@ -236,10 +238,6 @@ describe('ReactDOMFizzStaticNode', () => { await jest.runAllTimers(); const theReason = new Error('aborted for reasons'); - // @TODO this is a hack to work around lack of support for abortSignal.reason in node - // The abort call itself should set this property but since we are testing in node we - // set it here manually - controller.signal.reason = theReason; controller.abort(theReason); let caughtError = null; @@ -282,12 +280,8 @@ describe('ReactDOMFizzStaticNode', () => { } catch (error) { caughtError = error; } - expect(caughtError.message).toBe( - 'The render was aborted by the server without a reason.', - ); - expect(errors).toEqual([ - 'The render was aborted by the server without a reason.', - ]); + expect(caughtError.message).toBe('This operation was aborted'); + expect(errors).toEqual(['This operation was aborted']); }); // @gate experimental @@ -295,10 +289,6 @@ describe('ReactDOMFizzStaticNode', () => { const errors = []; const controller = new AbortController(); const theReason = new Error('aborted for reasons'); - // @TODO this is a hack to work around lack of support for abortSignal.reason in node - // The abort call itself should set this property but since we are testing in node we - // set it here manually - controller.signal.reason = theReason; controller.abort(theReason); const promise = ReactDOMFizzStatic.prerenderToNodeStreams( @@ -362,10 +352,6 @@ describe('ReactDOMFizzStaticNode', () => { await jest.runAllTimers(); - // @TODO this is a hack to work around lack of support for abortSignal.reason in node - // The abort call itself should set this property but since we are testing in node we - // set it here manually - controller.signal.reason = 'foobar'; controller.abort('foobar'); await resultPromise; @@ -408,10 +394,6 @@ describe('ReactDOMFizzStaticNode', () => { await jest.runAllTimers(); - // @TODO this is a hack to work around lack of support for abortSignal.reason in node - // The abort call itself should set this property but since we are testing in node we - // set it here manually - controller.signal.reason = new Error('uh oh'); controller.abort(new Error('uh oh')); await resultPromise; diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzSuppressHydrationWarning-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzSuppressHydrationWarning-test.js index e2b0fbb86f6f..3300297b77ee 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFizzSuppressHydrationWarning-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFizzSuppressHydrationWarning-test.js @@ -5,6 +5,7 @@ * LICENSE file in the root directory of this source tree. * * @emails react-core + * @jest-environment ./scripts/jest/ReactDOMServerIntegrationEnvironment */ 'use strict'; @@ -21,6 +22,7 @@ let container; let buffer = ''; let hasErrored = false; let fatalError = undefined; +let waitForAll; describe('ReactDOMFizzServerHydrationWarning', () => { beforeEach(() => { @@ -32,6 +34,9 @@ describe('ReactDOMFizzServerHydrationWarning', () => { ReactDOMFizzServer = require('react-dom/server'); Stream = require('stream'); + const InternalTestUtils = require('internal-test-utils'); + waitForAll = InternalTestUtils.waitForAll; + // Test Environment const jsdom = new JSDOM( '
    ', @@ -125,6 +130,47 @@ describe('ReactDOMFizzServerHydrationWarning', () => { : children; } + // @gate enableClientRenderFallbackOnTextMismatch + it('suppresses but does not fix text mismatches with suppressHydrationWarning', async () => { + function App({isClient}) { + return ( +
    + + {isClient ? 'Client Text' : 'Server Text'} + + {isClient ? 2 : 1} +
    + ); + } + await act(() => { + const {pipe} = ReactDOMFizzServer.renderToPipeableStream( + , + ); + pipe(writable); + }); + expect(getVisibleChildren(container)).toEqual( +
    + Server Text + 1 +
    , + ); + ReactDOMClient.hydrateRoot(container, , { + onRecoverableError(error) { + // Don't miss a hydration error. There should be none. + Scheduler.log(error.message); + }, + }); + await waitForAll([]); + // The text mismatch should be *silently* fixed. Even in production. + expect(getVisibleChildren(container)).toEqual( +
    + Server Text + 1 +
    , + ); + }); + + // @gate !enableClientRenderFallbackOnTextMismatch it('suppresses and fixes text mismatches with suppressHydrationWarning', async () => { function App({isClient}) { return ( @@ -136,7 +182,7 @@ describe('ReactDOMFizzServerHydrationWarning', () => {
    ); } - await act(async () => { + await act(() => { const {pipe} = ReactDOMFizzServer.renderToPipeableStream( , ); @@ -151,10 +197,10 @@ describe('ReactDOMFizzServerHydrationWarning', () => { ReactDOMClient.hydrateRoot(container, , { onRecoverableError(error) { // Don't miss a hydration error. There should be none. - Scheduler.unstable_yieldValue(error.message); + Scheduler.log(error.message); }, }); - expect(Scheduler).toFlushAndYield([]); + await waitForAll([]); // The text mismatch should be *silently* fixed. Even in production. expect(getVisibleChildren(container)).toEqual(
    @@ -164,6 +210,49 @@ describe('ReactDOMFizzServerHydrationWarning', () => { ); }); + // @gate enableClientRenderFallbackOnTextMismatch + it('suppresses but does not fix multiple text node mismatches with suppressHydrationWarning', async () => { + function App({isClient}) { + return ( +
    + + {isClient ? 'Client1' : 'Server1'} + {isClient ? 'Client2' : 'Server2'} + +
    + ); + } + await act(() => { + const {pipe} = ReactDOMFizzServer.renderToPipeableStream( + , + ); + pipe(writable); + }); + expect(getVisibleChildren(container)).toEqual( +
    + + {'Server1'} + {'Server2'} + +
    , + ); + ReactDOMClient.hydrateRoot(container, , { + onRecoverableError(error) { + Scheduler.log(error.message); + }, + }); + await waitForAll([]); + expect(getVisibleChildren(container)).toEqual( +
    + + {'Server1'} + {'Server2'} + +
    , + ); + }); + + // @gate !enableClientRenderFallbackOnTextMismatch it('suppresses and fixes multiple text node mismatches with suppressHydrationWarning', async () => { function App({isClient}) { return ( @@ -175,7 +264,7 @@ describe('ReactDOMFizzServerHydrationWarning', () => {
    ); } - await act(async () => { + await act(() => { const {pipe} = ReactDOMFizzServer.renderToPipeableStream( , ); @@ -191,10 +280,10 @@ describe('ReactDOMFizzServerHydrationWarning', () => { ); ReactDOMClient.hydrateRoot(container, , { onRecoverableError(error) { - Scheduler.unstable_yieldValue(error.message); + Scheduler.log(error.message); }, }); - expect(Scheduler).toFlushAndYield([]); + await waitForAll([]); expect(getVisibleChildren(container)).toEqual(
    @@ -215,7 +304,7 @@ describe('ReactDOMFizzServerHydrationWarning', () => {
    ); } - await act(async () => { + await act(() => { const {pipe} = ReactDOMFizzServer.renderToPipeableStream( , ); @@ -232,11 +321,11 @@ describe('ReactDOMFizzServerHydrationWarning', () => { ); ReactDOMClient.hydrateRoot(container, , { onRecoverableError(error) { - Scheduler.unstable_yieldValue(error.message); + Scheduler.log(error.message); }, }); - expect(() => { - expect(Scheduler).toFlushAndYield([ + await expect(async () => { + await waitForAll([ 'Hydration failed because the initial UI does not match what was rendered on the server.', 'There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.', ]); @@ -256,6 +345,48 @@ describe('ReactDOMFizzServerHydrationWarning', () => { ); }); + // @gate enableClientRenderFallbackOnTextMismatch + it('suppresses but does not fix client-only single text node mismatches with suppressHydrationWarning', async () => { + function App({text}) { + return ( +
    + {text} +
    + ); + } + await act(() => { + const {pipe} = ReactDOMFizzServer.renderToPipeableStream( + , + ); + pipe(writable); + }); + expect(getVisibleChildren(container)).toEqual( +
    + +
    , + ); + const root = ReactDOMClient.hydrateRoot(container, , { + onRecoverableError(error) { + Scheduler.log(error.message); + }, + }); + await waitForAll([]); + expect(getVisibleChildren(container)).toEqual( +
    + +
    , + ); + // An update fixes it though. + root.render(); + await waitForAll([]); + expect(getVisibleChildren(container)).toEqual( +
    + Client 2 +
    , + ); + }); + + // @gate !enableClientRenderFallbackOnTextMismatch it('suppresses and fixes client-only single text node mismatches with suppressHydrationWarning', async () => { function App({isClient}) { return ( @@ -266,7 +397,7 @@ describe('ReactDOMFizzServerHydrationWarning', () => {
    ); } - await act(async () => { + await act(() => { const {pipe} = ReactDOMFizzServer.renderToPipeableStream( , ); @@ -279,10 +410,10 @@ describe('ReactDOMFizzServerHydrationWarning', () => { ); ReactDOMClient.hydrateRoot(container, , { onRecoverableError(error) { - Scheduler.unstable_yieldValue(error.message); + Scheduler.log(error.message); }, }); - expect(Scheduler).toFlushAndYield([]); + await waitForAll([]); expect(getVisibleChildren(container)).toEqual(
    {'Client'} @@ -302,7 +433,7 @@ describe('ReactDOMFizzServerHydrationWarning', () => {
    ); } - await act(async () => { + await act(() => { const {pipe} = ReactDOMFizzServer.renderToPipeableStream( , ); @@ -315,11 +446,11 @@ describe('ReactDOMFizzServerHydrationWarning', () => { ); ReactDOMClient.hydrateRoot(container, , { onRecoverableError(error) { - Scheduler.unstable_yieldValue(error.message); + Scheduler.log(error.message); }, }); - expect(() => { - expect(Scheduler).toFlushAndYield([ + await expect(async () => { + await waitForAll([ 'Hydration failed because the initial UI does not match what was rendered on the server.', 'There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.', ]); @@ -348,7 +479,7 @@ describe('ReactDOMFizzServerHydrationWarning', () => {
    ); } - await act(async () => { + await act(() => { const {pipe} = ReactDOMFizzServer.renderToPipeableStream( , ); @@ -363,11 +494,11 @@ describe('ReactDOMFizzServerHydrationWarning', () => { ); ReactDOMClient.hydrateRoot(container, , { onRecoverableError(error) { - Scheduler.unstable_yieldValue(error.message); + Scheduler.log(error.message); }, }); - expect(() => { - expect(Scheduler).toFlushAndYield([ + await expect(async () => { + await waitForAll([ 'Hydration failed because the initial UI does not match what was rendered on the server.', 'There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.', ]); @@ -399,7 +530,7 @@ describe('ReactDOMFizzServerHydrationWarning', () => {
    ); } - await act(async () => { + await act(() => { const {pipe} = ReactDOMFizzServer.renderToPipeableStream( , ); @@ -414,11 +545,11 @@ describe('ReactDOMFizzServerHydrationWarning', () => { ); ReactDOMClient.hydrateRoot(container, , { onRecoverableError(error) { - Scheduler.unstable_yieldValue(error.message); + Scheduler.log(error.message); }, }); - expect(() => { - expect(Scheduler).toFlushAndYield([ + await expect(async () => { + await waitForAll([ 'Hydration failed because the initial UI does not match what was rendered on the server.', 'There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.', ]); @@ -448,7 +579,7 @@ describe('ReactDOMFizzServerHydrationWarning', () => {
    ); } - await act(async () => { + await act(() => { const {pipe} = ReactDOMFizzServer.renderToPipeableStream( , ); @@ -463,12 +594,11 @@ describe('ReactDOMFizzServerHydrationWarning', () => { ); ReactDOMClient.hydrateRoot(container, , { onRecoverableError(error) { - Scheduler.unstable_yieldValue(error.message); + Scheduler.log(error.message); }, }); - expect(() => { - expect(Scheduler).toFlushAndYield([ - 'Hydration failed because the initial UI does not match what was rendered on the server.', + await expect(async () => { + await waitForAll([ 'Hydration failed because the initial UI does not match what was rendered on the server.', 'There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.', ]); @@ -490,7 +620,7 @@ describe('ReactDOMFizzServerHydrationWarning', () => { ); }); - it('suppresses and does not fix attribute mismatches with suppressHydrationWarning', async () => { + it('suppresses but does not fix attribute mismatches with suppressHydrationWarning', async () => { function App({isClient}) { return (
    @@ -504,7 +634,7 @@ describe('ReactDOMFizzServerHydrationWarning', () => {
    ); } - await act(async () => { + await act(() => { const {pipe} = ReactDOMFizzServer.renderToPipeableStream( , ); @@ -517,10 +647,10 @@ describe('ReactDOMFizzServerHydrationWarning', () => { ); ReactDOMClient.hydrateRoot(container, , { onRecoverableError(error) { - Scheduler.unstable_yieldValue(error.message); + Scheduler.log(error.message); }, }); - expect(Scheduler).toFlushAndYield([]); + await waitForAll([]); expect(getVisibleChildren(container)).toEqual(
    @@ -541,7 +671,7 @@ describe('ReactDOMFizzServerHydrationWarning', () => {
    ); } - await act(async () => { + await act(() => { const {pipe} = ReactDOMFizzServer.renderToPipeableStream( , ); @@ -554,10 +684,10 @@ describe('ReactDOMFizzServerHydrationWarning', () => { ); ReactDOMClient.hydrateRoot(container, , { onRecoverableError(error) { - Scheduler.unstable_yieldValue(error.message); + Scheduler.log(error.message); }, }); - expect(Scheduler).toFlushAndYield([]); + await waitForAll([]); expect(getVisibleChildren(container)).toEqual(

    Server HTML

    @@ -574,7 +704,7 @@ describe('ReactDOMFizzServerHydrationWarning', () => {
    ); } - await act(async () => { + await act(() => { const {pipe} = ReactDOMFizzServer.renderToPipeableStream( , ); @@ -587,11 +717,11 @@ describe('ReactDOMFizzServerHydrationWarning', () => { ); ReactDOMClient.hydrateRoot(container, , { onRecoverableError(error) { - Scheduler.unstable_yieldValue(error.message); + Scheduler.log(error.message); }, }); - expect(() => { - expect(Scheduler).toFlushAndYield([ + await expect(async () => { + await waitForAll([ 'Hydration failed because the initial UI does not match what was rendered on the server.', 'There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.', ]); @@ -619,7 +749,7 @@ describe('ReactDOMFizzServerHydrationWarning', () => {
    ); } - await act(async () => { + await act(() => { const {pipe} = ReactDOMFizzServer.renderToPipeableStream( , ); @@ -633,11 +763,11 @@ describe('ReactDOMFizzServerHydrationWarning', () => { ); ReactDOMClient.hydrateRoot(container, , { onRecoverableError(error) { - Scheduler.unstable_yieldValue(error.message); + Scheduler.log(error.message); }, }); - expect(() => { - expect(Scheduler).toFlushAndYield([ + await expect(async () => { + await waitForAll([ 'Hydration failed because the initial UI does not match what was rendered on the server.', 'There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.', ]); diff --git a/packages/react-dom/src/__tests__/ReactDOMFloat-test.js b/packages/react-dom/src/__tests__/ReactDOMFloat-test.js index b216700671cc..d362e1b94a4a 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFloat-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFloat-test.js @@ -5,6 +5,7 @@ * LICENSE file in the root directory of this source tree. * * @emails react-core + * @jest-environment ./scripts/jest/ReactDOMServerIntegrationEnvironment */ 'use strict'; @@ -16,13 +17,14 @@ import { let JSDOM; let Stream; -let Scheduler; let React; let ReactDOM; let ReactDOMClient; let ReactDOMFizzServer; let Suspense; let textCache; +let loadCache; +let window; let document; let writable; const CSPnonce = null; @@ -31,29 +33,49 @@ let buffer = ''; let hasErrored = false; let fatalError = undefined; let renderOptions; +let waitForAll; +let waitForThrow; +let assertLog; +let Scheduler; + +function resetJSDOM(markup) { + // Test Environment + const jsdom = new JSDOM(markup, { + runScripts: 'dangerously', + }); + // We mock matchMedia. for simplicity it only matches 'all' or '' and misses everything else + Object.defineProperty(jsdom.window, 'matchMedia', { + writable: true, + value: jest.fn().mockImplementation(query => ({ + matches: query === 'all' || query === '', + media: query, + })), + }); + window = jsdom.window; + document = jsdom.window.document; +} describe('ReactDOMFloat', () => { beforeEach(() => { jest.resetModules(); JSDOM = require('jsdom').JSDOM; - Scheduler = require('scheduler'); React = require('react'); ReactDOM = require('react-dom'); ReactDOMClient = require('react-dom/client'); ReactDOMFizzServer = require('react-dom/server'); Stream = require('stream'); Suspense = React.Suspense; + Scheduler = require('scheduler/unstable_mock'); + + const InternalTestUtils = require('internal-test-utils'); + waitForAll = InternalTestUtils.waitForAll; + waitForThrow = InternalTestUtils.waitForThrow; + assertLog = InternalTestUtils.assertLog; textCache = new Map(); + loadCache = new Set(); - // Test Environment - const jsdom = new JSDOM( - '
    ', - { - runScripts: 'dangerously', - }, - ); - document = jsdom.window.document; + resetJSDOM('
    '); container = document.getElementById('container'); buffer = ''; @@ -76,21 +98,6 @@ describe('ReactDOMFloat', () => { } }); - function normalizeCodeLocInfo(str) { - return ( - typeof str === 'string' && - str.replace(/\n +(?:at|in) ([\S]+)[^\n]*/g, function(m, name) { - return '\n in ' + name + ' (at **)'; - }) - ); - } - - function componentStack(components) { - return components - .map(component => `\n in ${component} (at **)`) - .join(''); - } - async function act(callback) { await callback(); // Await one turn around the event loop. @@ -137,15 +144,11 @@ describe('ReactDOMFloat', () => { // We also want to execute any scripts that are embedded. // We assume that we have now received a proper fragment of HTML. const bufferedContent = buffer; - // Test Environment - const jsdom = new JSDOM(bufferedContent, { - runScripts: 'dangerously', - }); - document = jsdom.window.document; + resetJSDOM(bufferedContent); container = document; buffer = ''; await withLoadingReadyState(async () => { - await replaceScriptsAndMove(jsdom.window, null, document.documentElement); + await replaceScriptsAndMove(window, null, document.documentElement); }, document); } @@ -196,6 +199,11 @@ describe('ReactDOMFloat', () => { : children; } + function BlockedOn({value, children}) { + readText(value); + return children; + } + function resolveText(text) { const record = textCache.get(text); if (record === undefined) { @@ -257,6 +265,54 @@ describe('ReactDOMFloat', () => { ); } + function loadPreloads(hrefs) { + const event = new window.Event('load'); + const nodes = document.querySelectorAll('link[rel="preload"]'); + resolveLoadables(hrefs, nodes, event, href => + Scheduler.log('load preload: ' + href), + ); + } + + function errorPreloads(hrefs) { + const event = new window.Event('error'); + const nodes = document.querySelectorAll('link[rel="preload"]'); + resolveLoadables(hrefs, nodes, event, href => + Scheduler.log('error preload: ' + href), + ); + } + + function loadStylesheets(hrefs) { + const event = new window.Event('load'); + const nodes = document.querySelectorAll('link[rel="stylesheet"]'); + resolveLoadables(hrefs, nodes, event, href => + Scheduler.log('load stylesheet: ' + href), + ); + } + + function errorStylesheets(hrefs) { + const event = new window.Event('error'); + const nodes = document.querySelectorAll('link[rel="stylesheet"]'); + resolveLoadables(hrefs, nodes, event, href => { + Scheduler.log('error stylesheet: ' + href); + }); + } + + function resolveLoadables(hrefs, nodes, event, onLoad) { + const hrefSet = hrefs ? new Set(hrefs) : null; + for (let i = 0; i < nodes.length; i++) { + const node = nodes[i]; + if (loadCache.has(node)) { + continue; + } + const href = node.getAttribute('href'); + if (!hrefSet || hrefSet.has(href)) { + loadCache.add(node); + onLoad(href); + node.dispatchEvent(event); + } + } + } + // @gate enableFloat it('can render resources before singletons', async () => { const root = ReactDOMClient.createRoot(document); @@ -272,11 +328,11 @@ describe('ReactDOMFloat', () => { , ); try { - expect(Scheduler).toFlushWithoutYielding(); + await waitForAll([]); } catch (e) { // for DOMExceptions that happen when expecting this test to fail we need // to clear the scheduler first otherwise the expected failure will fail - expect(Scheduler).toFlushWithoutYielding(); + await waitForAll([]); throw e; } expect(getMeaningfulChildren(document)).toEqual( @@ -290,19 +346,6 @@ describe('ReactDOMFloat', () => { ); }); - function renderSafelyAndExpect(root, children) { - root.render(children); - return expect(() => { - try { - expect(Scheduler).toFlushWithoutYielding(); - } catch (e) { - try { - expect(Scheduler).toFlushWithoutYielding(); - } catch (f) {} - } - }); - } - // @gate enableFloat it('can hydrate non Resources in head when Resources are also inserted there', async () => { await actIntoEmptyDocument(() => { @@ -326,11 +369,12 @@ describe('ReactDOMFloat', () => { expect(getMeaningfulChildren(document)).toEqual( - foo + + foo , @@ -352,16 +396,16 @@ describe('ReactDOMFloat', () => { foo , ); - expect(Scheduler).toFlushWithoutYielding(); + await waitForAll([]); expect(getMeaningfulChildren(document)).toEqual( - foo + foobar', + 'foobar', '', ]); }); @@ -642,3233 +699,4255 @@ describe('ReactDOMFloat', () => { ).toEqual(['']); }); - describe('HostResource', () => { - // @gate enableFloat - it('warns when you update props to an invalid type', async () => { - const root = ReactDOMClient.createRoot(container); - root.render( -
    - - -
    , - ); - expect(Scheduler).toFlushWithoutYielding(); - root.render( -
    - {}} href="bar" /> - {}} /> -
    , - ); - expect(() => { - expect(Scheduler).toFlushWithoutYielding(); - }).toErrorDev([ - 'Warning: A previously rendered as a Resource with href "foo" with rel ""stylesheet"" but was updated with an invalid rel: something with type "function". When a link does not have a valid rel prop it is not represented in the DOM. If this is intentional, instead do not render the anymore.', - 'Warning: A previously rendered as a Resource with href "bar" but was updated with an invalid href prop: something with type "function". When a link does not have a valid href prop it is not represented in the DOM. If this is intentional, instead do not render the anymore.', - ]); - expect(getMeaningfulChildren(document)).toEqual( + // @gate enableFloat + it('can avoid inserting a late stylesheet if it already rendered on the client', async () => { + await actIntoEmptyDocument(() => { + renderToPipeableStream( - - - - - - -
    -
    -
    + + + + foo + + + + + + bar + + , - ); + ).pipe(writable); }); - }); - describe('ReactDOM.preload', () => { - // @gate enableFloat - it('inserts a preload resource into the stream when called during server rendering', async () => { - function Component() { - ReactDOM.preload('foo', {as: 'style'}); - return 'foo'; - } - await actIntoEmptyDocument(() => { - const {pipe} = renderToPipeableStream( - - - - - - , - ); - pipe(writable); - }); - expect(getMeaningfulChildren(document)).toEqual( - - - - - foo - , - ); - }); + expect(getMeaningfulChildren(document)).toEqual( + + + + {'loading foo...'} + {'loading bar...'} + + , + ); - // @gate enableFloat - it('inserts a preload resource into the document during render when called during client rendering', async () => { - function Component() { - ReactDOM.preload('foo', {as: 'style'}); - return 'foo'; - } - const root = ReactDOMClient.createRoot(container); - root.render(); - expect(Scheduler).toFlushWithoutYielding(); - expect(getMeaningfulChildren(document)).toEqual( - - - - - -
    foo
    - - , + ReactDOMClient.hydrateRoot( + document, + + + + + + foo + + + + bar + + + , + ); + await waitForAll([]); + expect(getMeaningfulChildren(document)).toEqual( + + + + + + + {'loading foo...'} + {'loading bar...'} + + , + ); + + await act(() => { + resolveText('bar'); + }); + await act(() => { + const sheets = document.querySelectorAll( + 'link[rel="stylesheet"][data-precedence]', ); + const event = document.createEvent('Event'); + event.initEvent('load', true, true); + for (let i = 0; i < sheets.length; i++) { + sheets[i].dispatchEvent(event); + } }); + expect(getMeaningfulChildren(document)).toEqual( + + + + + + + + {'loading foo...'} + {'bar'} + + + , + ); - // @gate enableFloat - it('inserts a preload resource when called in a layout effect', async () => { - function App() { - React.useLayoutEffect(() => { - ReactDOM.preload('foo', {as: 'style'}); - }, []); - return 'foobar'; + await act(() => { + resolveText('foo'); + }); + await act(() => { + const sheets = document.querySelectorAll( + 'link[rel="stylesheet"][data-precedence]', + ); + const event = document.createEvent('Event'); + event.initEvent('load', true, true); + for (let i = 0; i < sheets.length; i++) { + sheets[i].dispatchEvent(event); } - const root = ReactDOMClient.createRoot(container); - root.render(); - expect(Scheduler).toFlushWithoutYielding(); - - expect(getMeaningfulChildren(document)).toEqual( - - - - - -
    foobar
    - - , - ); }); + expect(getMeaningfulChildren(document)).toEqual( + + + + + + + + {'foo'} + {'bar'} + + + + , + ); + }); - // @gate enableFloat - it('inserts a preload resource when called in a passive effect', async () => { - function App() { - React.useEffect(() => { - ReactDOM.preload('foo', {as: 'style'}); - }, []); - return 'foobar'; - } - const root = ReactDOMClient.createRoot(container); - root.render(); - expect(Scheduler).toFlushWithoutYielding(); + // @gate enableFloat + it('can hoist and + + + + + + + + + + + + + + + + + + + + + + + + + + + + + , - ); + ).pipe(writable); }); - // @gate enableFloat - it('inserts a preload resource when called in module scope if a root has already been created', async () => { - // The requirement that a root be created has to do with bootstrapping the dispatcher. - // We are intentionally avoiding setting it to the default via import due to cycles and - // we are trying to avoid doing a mutable initailation in module scope. - ReactDOM.preload('foo', {as: 'style'}); - ReactDOMClient.createRoot(container); - ReactDOM.preload('bar', {as: 'style'}); - // We need to use global.document because preload falls back - // to the window.document global when no other documents have been used - // The way the JSDOM runtim is created for these tests the local document - // global does not point to the global.document - expect(getMeaningfulChildren(global.document)).toEqual( - - - - - - , - ); + expect(getMeaningfulChildren(document)).toEqual( + + + + + + + + + , + ); + + await act(() => { + resolveText('block'); }); - // @gate enableFloat - it('supports script preloads', async () => { - function ServerApp() { - ReactDOM.preload('foo', {as: 'script', integrity: 'foo hash'}); - ReactDOM.preload('bar', { - as: 'script', - crossOrigin: 'use-credentials', - integrity: 'bar hash', - }); - return ( - - - - hi - - foo - - ); - } - function ClientApp() { - ReactDOM.preload('foo', {as: 'script', integrity: 'foo hash'}); - ReactDOM.preload('qux', {as: 'script'}); - return ( - - - hi - - foo - - - ); - } + expect(getMeaningfulChildren(document)).toEqual( + + + + + + + + + + + + + + + + + + , + ); - await actIntoEmptyDocument(() => { - const {pipe} = renderToPipeableStream(); - pipe(writable); - }); - expect(getMeaningfulChildren(document)).toEqual( - - - - - - hi - - foo - , - ); + await act(() => { + resolveText('block2'); + }); - ReactDOMClient.hydrateRoot(document, ); - expect(Scheduler).toFlushWithoutYielding(); + expect(getMeaningfulChildren(document)).toEqual( + + + + + + + + + + + + + + + + + + + + + + + + + , + ); - expect(getMeaningfulChildren(document)).toEqual( - - - - - - hi - - - - foo - , - ); + await act(() => { + resolveText('block again'); }); - }); - describe('ReactDOM.preinit as style', () => { - // @gate enableFloat - it('creates a style Resource when called during server rendering before first flush', async () => { - function Component() { - ReactDOM.preinit('foo', {as: 'style'}); - return 'foo'; - } - await actIntoEmptyDocument(() => { - const {pipe} = renderToPipeableStream( - - - - - - , - ); - pipe(writable); - }); - expect(getMeaningfulChildren(document)).toEqual( - - - - - foo - , - ); - }); + expect(getMeaningfulChildren(document)).toEqual( + + + + + + + + + + + + + + + + + + + + + + + + + , + ); - // @gate enableFloat - it('creates a preload Resource when called during server rendering after first flush', async () => { - function BlockedOn({text, children}) { - readText(text); - return children; - } - function Component() { - ReactDOM.preinit('foo', {as: 'style', precedence: 'foo'}); - return 'foo'; - } - await actIntoEmptyDocument(() => { - const {pipe} = renderToPipeableStream( - - - - - - - - - - , - ); - pipe(writable); - }); - await act(() => { - resolveText('unblock'); - }); - expect(getMeaningfulChildren(document)).toEqual( + ReactDOMClient.hydrateRoot( + document, + + + + + + + + + + , + ); + await waitForAll([]); + + expect(getMeaningfulChildren(document)).toEqual( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + , + ); + }); + + // @gate enableFloat + it('client renders a boundary if a style Resource dependency fails to load', async () => { + function App() { + return ( - foo - + + + + + Hello + + - , + ); + } + await actIntoEmptyDocument(() => { + const {pipe} = renderToPipeableStream(); + pipe(writable); }); - // @gate enableFloat - it('inserts a style Resource into the document during render when called during client rendering', async () => { - function Component() { - ReactDOM.preinit('foo', {as: 'style', precedence: 'foo'}); - return 'foo'; - } - const root = ReactDOMClient.createRoot(container); - root.render(); - expect(Scheduler).toFlushWithoutYielding(); - expect(getMeaningfulChildren(document)).toEqual( - - - - - -
    foo
    - - , - ); + await act(() => { + resolveText('unblock'); }); - // @gate enableFloat - it('inserts a preload resource into the document when called in an insertion effect, layout effect, or passive effect', async () => { - function App() { - React.useEffect(() => { - ReactDOM.preinit('passive', {as: 'style', precedence: 'default'}); - }, []); - React.useLayoutEffect(() => { - ReactDOM.preinit('layout', {as: 'style', precedence: 'default'}); - }); - React.useInsertionEffect(() => { - ReactDOM.preinit('insertion', {as: 'style', precedence: 'default'}); - }); - return 'foobar'; - } - const root = ReactDOMClient.createRoot(container); - root.render(); - expect(Scheduler).toFlushWithoutYielding(); - - expect(getMeaningfulChildren(document)).toEqual( - - - - - - - -
    foobar
    - - , - ); - }); + expect(getMeaningfulChildren(document)).toEqual( + + + + + + + loading... + + + + , + ); - // @gate enableFloat - it('inserts a preload resource when called in module scope', async () => { - // The requirement that a root be created has to do with bootstrapping the dispatcher. - // We are intentionally avoiding setting it to the default via import due to cycles and - // we are trying to avoid doing a mutable initailation in module scope. - ReactDOM.preinit('foo', {as: 'style'}); - ReactDOMClient.hydrateRoot(container, null); - ReactDOM.preinit('bar', {as: 'style'}); - // We need to use global.document because preload falls back - // to the window.document global when no other documents have been used - // The way the JSDOM runtim is created for these tests the local document - // global does not point to the global.document - expect(getMeaningfulChildren(global.document)).toEqual( - - - - - - , - ); - }); - }); + errorStylesheets(['bar']); + assertLog(['error stylesheet: bar']); - describe('ReactDOM.preinit as script', () => { - // @gate enableFloat - it('can preinit a script', async () => { - function App({srcs}) { - srcs.forEach(src => ReactDOM.preinit(src, {as: 'script'})); - return ( - - - title - - foo - - ); - } - await actIntoEmptyDocument(() => { - const {pipe} = renderToPipeableStream( - , - ); - pipe(writable); - }); - expect(getMeaningfulChildren(document)).toEqual( - - - "`, + `"
    hello world
    "`, ); }); diff --git a/packages/react-server-dom-relay/src/__tests__/ReactFlightDOMRelay-test.internal.js b/packages/react-server-dom-relay/src/__tests__/ReactFlightDOMRelay-test.internal.js index 14690f7f913e..a79346676d02 100644 --- a/packages/react-server-dom-relay/src/__tests__/ReactFlightDOMRelay-test.internal.js +++ b/packages/react-server-dom-relay/src/__tests__/ReactFlightDOMRelay-test.internal.js @@ -19,7 +19,7 @@ describe('ReactFlightDOMRelay', () => { beforeEach(() => { jest.resetModules(); - act = require('jest-react').act; + act = require('internal-test-utils').act; React = require('react'); ReactDOMClient = require('react-dom/client'); ReactDOMFlightRelayServer = require('react-server-dom-relay/server'); @@ -85,7 +85,7 @@ describe('ReactFlightDOMRelay', () => { }); }); - it('can render a Client Component using a module reference and render there', () => { + it('can render a Client Component using a module reference and render there', async () => { function UserClient(props) { return ( @@ -110,7 +110,7 @@ describe('ReactFlightDOMRelay', () => { const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); - act(() => { + await act(() => { root.render(modelClient.greeting); }); @@ -226,7 +226,7 @@ describe('ReactFlightDOMRelay', () => { Foo.prototype = Object.create(Bar.prototype); // This is enumerable which some polyfills do. Foo.prototype.constructor = Foo; - Foo.prototype.method = function() {}; + Foo.prototype.method = function () {}; expect(() => { const transport = []; diff --git a/packages/react-native-renderer/src/legacy-events/PropagationPhases.js b/packages/react-server-dom-webpack/client.browser.js similarity index 78% rename from packages/react-native-renderer/src/legacy-events/PropagationPhases.js rename to packages/react-server-dom-webpack/client.browser.js index 6672ab7687ae..7d26c2771e50 100644 --- a/packages/react-native-renderer/src/legacy-events/PropagationPhases.js +++ b/packages/react-server-dom-webpack/client.browser.js @@ -7,4 +7,4 @@ * @flow */ -export type PropagationPhases = 'bubbled' | 'captured'; +export * from './src/ReactFlightDOMClientBrowser'; diff --git a/packages/react-server-dom-webpack/client.edge.js b/packages/react-server-dom-webpack/client.edge.js new file mode 100644 index 000000000000..fadceeaf8443 --- /dev/null +++ b/packages/react-server-dom-webpack/client.edge.js @@ -0,0 +1,10 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +export * from './src/ReactFlightDOMClientEdge'; diff --git a/packages/react-server-dom-webpack/client.js b/packages/react-server-dom-webpack/client.js index 9b9c654fb580..2dad5bb51387 100644 --- a/packages/react-server-dom-webpack/client.js +++ b/packages/react-server-dom-webpack/client.js @@ -7,4 +7,4 @@ * @flow */ -export * from './src/ReactFlightDOMClient'; +export * from './client.browser'; diff --git a/packages/react-server-dom-webpack/client.node.js b/packages/react-server-dom-webpack/client.node.js new file mode 100644 index 000000000000..4f435353a20f --- /dev/null +++ b/packages/react-server-dom-webpack/client.node.js @@ -0,0 +1,10 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +export * from './src/ReactFlightDOMClientNode'; diff --git a/packages/react-server-dom-webpack/client.node.unbundled.js b/packages/react-server-dom-webpack/client.node.unbundled.js new file mode 100644 index 000000000000..4f435353a20f --- /dev/null +++ b/packages/react-server-dom-webpack/client.node.unbundled.js @@ -0,0 +1,10 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +export * from './src/ReactFlightDOMClientNode'; diff --git a/packages/react-server-dom-webpack/esm/react-server-dom-webpack-node-loader.js b/packages/react-server-dom-webpack/esm/react-server-dom-webpack-node-loader.production.min.js similarity index 100% rename from packages/react-server-dom-webpack/esm/react-server-dom-webpack-node-loader.js rename to packages/react-server-dom-webpack/esm/react-server-dom-webpack-node-loader.production.min.js diff --git a/packages/react-server-dom-webpack/npm/client.browser.js b/packages/react-server-dom-webpack/npm/client.browser.js new file mode 100644 index 000000000000..9a80cea84cbb --- /dev/null +++ b/packages/react-server-dom-webpack/npm/client.browser.js @@ -0,0 +1,7 @@ +'use strict'; + +if (process.env.NODE_ENV === 'production') { + module.exports = require('./cjs/react-server-dom-webpack-client.browser.production.min.js'); +} else { + module.exports = require('./cjs/react-server-dom-webpack-client.browser.development.js'); +} diff --git a/packages/react-server-dom-webpack/npm/client.edge.js b/packages/react-server-dom-webpack/npm/client.edge.js new file mode 100644 index 000000000000..661caffa755b --- /dev/null +++ b/packages/react-server-dom-webpack/npm/client.edge.js @@ -0,0 +1,7 @@ +'use strict'; + +if (process.env.NODE_ENV === 'production') { + module.exports = require('./cjs/react-server-dom-webpack-client.edge.production.min.js'); +} else { + module.exports = require('./cjs/react-server-dom-webpack-client.edge.development.js'); +} diff --git a/packages/react-server-dom-webpack/npm/client.js b/packages/react-server-dom-webpack/npm/client.js index b8e9a99ec5e8..89d93a7a7920 100644 --- a/packages/react-server-dom-webpack/npm/client.js +++ b/packages/react-server-dom-webpack/npm/client.js @@ -1,7 +1,3 @@ 'use strict'; -if (process.env.NODE_ENV === 'production') { - module.exports = require('./cjs/react-server-dom-webpack-client.production.min.js'); -} else { - module.exports = require('./cjs/react-server-dom-webpack-client.development.js'); -} +module.exports = require('./client.browser'); diff --git a/packages/react-server-dom-webpack/npm/client.node.js b/packages/react-server-dom-webpack/npm/client.node.js new file mode 100644 index 000000000000..cb7dbcf4c34f --- /dev/null +++ b/packages/react-server-dom-webpack/npm/client.node.js @@ -0,0 +1,7 @@ +'use strict'; + +if (process.env.NODE_ENV === 'production') { + module.exports = require('./cjs/react-server-dom-webpack-client.node.production.min.js'); +} else { + module.exports = require('./cjs/react-server-dom-webpack-client.node.development.js'); +} diff --git a/packages/react-server-dom-webpack/npm/client.node.unbundled.js b/packages/react-server-dom-webpack/npm/client.node.unbundled.js new file mode 100644 index 000000000000..17fe876fd388 --- /dev/null +++ b/packages/react-server-dom-webpack/npm/client.node.unbundled.js @@ -0,0 +1,7 @@ +'use strict'; + +if (process.env.NODE_ENV === 'production') { + module.exports = require('./cjs/react-server-dom-webpack-client.node.unbundled.production.min.js'); +} else { + module.exports = require('./cjs/react-server-dom-webpack-client.node.unbundled.development.js'); +} diff --git a/packages/react-server-dom-webpack/npm/server.edge.js b/packages/react-server-dom-webpack/npm/server.edge.js new file mode 100644 index 000000000000..d061fe624e78 --- /dev/null +++ b/packages/react-server-dom-webpack/npm/server.edge.js @@ -0,0 +1,7 @@ +'use strict'; + +if (process.env.NODE_ENV === 'production') { + module.exports = require('./cjs/react-server-dom-webpack-server.edge.production.min.js'); +} else { + module.exports = require('./cjs/react-server-dom-webpack-server.edge.development.js'); +} diff --git a/packages/react-server-dom-webpack/npm/server.node.unbundled.js b/packages/react-server-dom-webpack/npm/server.node.unbundled.js new file mode 100644 index 000000000000..5f75de80942a --- /dev/null +++ b/packages/react-server-dom-webpack/npm/server.node.unbundled.js @@ -0,0 +1,7 @@ +'use strict'; + +if (process.env.NODE_ENV === 'production') { + module.exports = require('./cjs/react-server-dom-webpack-server.node.unbundled.production.min.js'); +} else { + module.exports = require('./cjs/react-server-dom-webpack-server.node.unbundled.development.js'); +} diff --git a/packages/react-server-dom-webpack/package.json b/packages/react-server-dom-webpack/package.json index f68394b92fc4..4075fdec420d 100644 --- a/packages/react-server-dom-webpack/package.json +++ b/packages/react-server-dom-webpack/package.json @@ -14,9 +14,15 @@ "index.js", "plugin.js", "client.js", + "client.browser.js", + "client.edge.js", + "client.node.js", + "client.node.unbundled.js", "server.js", "server.browser.js", + "server.edge.js", "server.node.js", + "server.node.unbundled.js", "node-register.js", "cjs/", "umd/", @@ -25,19 +31,42 @@ "exports": { ".": "./index.js", "./plugin": "./plugin.js", - "./client": "./client.js", + "./client": { + "workerd": "./client.edge.js", + "edge-light": "./client.edge.js", + "deno": "./client.edge.js", + "worker": "./client.edge.js", + "node": { + "webpack": "./client.node.js", + "default": "./client.node.unbundled.js" + }, + "browser": "./client.browser.js", + "default": "./client.browser.js" + }, + "./client.browser": "./client.browser.js", + "./client.edge": "./client.edge.js", + "./client.node": "./client.node.js", + "./client.node.unbundled": "./client.node.unbundled.js", "./server": { "react-server": { - "node": "./server.node.js", + "workerd": "./server.edge.js", + "edge-light": "./server.edge.js", + "deno": "./server.browser.js", + "node": { + "webpack": "./server.node.js", + "default": "./server.node.unbundled.js" + }, "browser": "./server.browser.js" }, "default": "./server.js" }, - "./server.node": "./server.node.js", "./server.browser": "./server.browser.js", - "./node-loader": "./esm/react-server-dom-webpack-node-loader.js", + "./server.edge": "./server.edge.js", + "./server.node": "./server.node.js", + "./server.node.unbundled": "./server.node.unbundled.js", + "./node-loader": "./esm/react-server-dom-webpack-node-loader.production.min.js", "./node-register": "./node-register.js", - "./src/*": "./src/*", + "./src/*": "./src/*.js", "./package.json": "./package.json" }, "main": "index.js", @@ -55,7 +84,7 @@ "webpack": "^5.59.0" }, "dependencies": { - "acorn": "^6.2.1", + "acorn-loose": "^8.3.0", "neo-async": "^2.6.1", "loose-envify": "^1.1.0" }, diff --git a/packages/react-server-dom-webpack/server.edge.js b/packages/react-server-dom-webpack/server.edge.js new file mode 100644 index 000000000000..98f975cb4706 --- /dev/null +++ b/packages/react-server-dom-webpack/server.edge.js @@ -0,0 +1,10 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +export * from './src/ReactFlightDOMServerEdge'; diff --git a/packages/react-server-dom-webpack/server.node.unbundled.js b/packages/react-server-dom-webpack/server.node.unbundled.js new file mode 100644 index 000000000000..7726b9bb929d --- /dev/null +++ b/packages/react-server-dom-webpack/server.node.unbundled.js @@ -0,0 +1,10 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +export * from './src/ReactFlightDOMServerNode'; diff --git a/packages/react-server-dom-webpack/src/ReactFlightClientNodeBundlerConfig.js b/packages/react-server-dom-webpack/src/ReactFlightClientNodeBundlerConfig.js new file mode 100644 index 000000000000..b3bd02c05411 --- /dev/null +++ b/packages/react-server-dom-webpack/src/ReactFlightClientNodeBundlerConfig.js @@ -0,0 +1,109 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import type { + Thenable, + FulfilledThenable, + RejectedThenable, +} from 'shared/ReactTypes'; + +export type SSRManifest = { + [clientId: string]: { + [clientExportName: string]: ClientReference, + }, +}; + +export type ServerManifest = void; + +export type ServerReferenceId = string; + +export opaque type ClientReferenceMetadata = { + id: string, + chunks: Array, + name: string, +}; + +// eslint-disable-next-line no-unused-vars +export opaque type ClientReference = { + specifier: string, + name: string, +}; + +export function resolveClientReference( + bundlerConfig: SSRManifest, + metadata: ClientReferenceMetadata, +): ClientReference { + const resolvedModuleData = bundlerConfig[metadata.id][metadata.name]; + return resolvedModuleData; +} + +export function resolveServerReference( + bundlerConfig: ServerManifest, + id: ServerReferenceId, +): ClientReference { + const idx = id.lastIndexOf('#'); + const specifier = id.substr(0, idx); + const name = id.substr(idx + 1); + return {specifier, name}; +} + +const asyncModuleCache: Map> = new Map(); + +export function preloadModule( + metadata: ClientReference, +): null | Thenable { + const existingPromise = asyncModuleCache.get(metadata.specifier); + if (existingPromise) { + if (existingPromise.status === 'fulfilled') { + return null; + } + return existingPromise; + } else { + // $FlowFixMe[unsupported-syntax] + const modulePromise: Thenable = import(metadata.specifier); + modulePromise.then( + value => { + const fulfilledThenable: FulfilledThenable = + (modulePromise: any); + fulfilledThenable.status = 'fulfilled'; + fulfilledThenable.value = value; + }, + reason => { + const rejectedThenable: RejectedThenable = (modulePromise: any); + rejectedThenable.status = 'rejected'; + rejectedThenable.reason = reason; + }, + ); + asyncModuleCache.set(metadata.specifier, modulePromise); + return modulePromise; + } +} + +export function requireModule(metadata: ClientReference): T { + let moduleExports; + // We assume that preloadModule has been called before, which + // should have added something to the module cache. + const promise: any = asyncModuleCache.get(metadata.specifier); + if (promise.status === 'fulfilled') { + moduleExports = promise.value; + } else { + throw promise.reason; + } + if (metadata.name === '*') { + // This is a placeholder value that represents that the caller imported this + // as a CommonJS module as is. + return moduleExports; + } + if (metadata.name === '') { + // This is a placeholder value that represents that the caller accessed the + // default property of this if it was an ESM interop module. + return moduleExports.default; + } + return moduleExports[metadata.name]; +} diff --git a/packages/react-server-dom-webpack/src/ReactFlightClientWebpackBundlerConfig.js b/packages/react-server-dom-webpack/src/ReactFlightClientWebpackBundlerConfig.js index abb472f744d2..85aaab79a905 100644 --- a/packages/react-server-dom-webpack/src/ReactFlightClientWebpackBundlerConfig.js +++ b/packages/react-server-dom-webpack/src/ReactFlightClientWebpackBundlerConfig.js @@ -13,15 +13,19 @@ import type { RejectedThenable, } from 'shared/ReactTypes'; -export type WebpackSSRMap = { +export type SSRManifest = null | { [clientId: string]: { - [clientExportName: string]: ModuleMetaData, + [clientExportName: string]: ClientReferenceMetadata, }, }; -export type BundlerConfig = null | WebpackSSRMap; +export type ServerManifest = { + [id: string]: ClientReference, +}; + +export type ServerReferenceId = string; -export opaque type ModuleMetaData = { +export opaque type ClientReferenceMetadata = { id: string, chunks: Array, name: string, @@ -29,15 +33,15 @@ export opaque type ModuleMetaData = { }; // eslint-disable-next-line no-unused-vars -export opaque type ModuleReference = ModuleMetaData; +export opaque type ClientReference = ClientReferenceMetadata; -export function resolveModuleReference( - bundlerConfig: BundlerConfig, - moduleData: ModuleMetaData, -): ModuleReference { +export function resolveClientReference( + bundlerConfig: SSRManifest, + metadata: ClientReferenceMetadata, +): ClientReference { if (bundlerConfig) { - const resolvedModuleData = bundlerConfig[moduleData.id][moduleData.name]; - if (moduleData.async) { + const resolvedModuleData = bundlerConfig[metadata.id][metadata.name]; + if (metadata.async) { return { id: resolvedModuleData.id, chunks: resolvedModuleData.chunks, @@ -48,7 +52,15 @@ export function resolveModuleReference( return resolvedModuleData; } } - return moduleData; + return metadata; +} + +export function resolveServerReference( + bundlerConfig: ServerManifest, + id: ServerReferenceId, +): ClientReference { + // This needs to return async: true if it's an async module. + return bundlerConfig[id]; } // The chunk cache contains all the chunks we've preloaded so far. @@ -64,9 +76,9 @@ function ignoreReject() { // Start preloading the modules since we might need them soon. // This function doesn't suspend. export function preloadModule( - moduleData: ModuleReference, + metadata: ClientReference, ): null | Thenable { - const chunks = moduleData.chunks; + const chunks = metadata.chunks; const promises = []; for (let i = 0; i < chunks.length; i++) { const chunkId = chunks[i]; @@ -82,8 +94,8 @@ export function preloadModule( promises.push(entry); } } - if (moduleData.async) { - const existingPromise = asyncModuleCache.get(moduleData.id); + if (metadata.async) { + const existingPromise = asyncModuleCache.get(metadata.id); if (existingPromise) { if (existingPromise.status === 'fulfilled') { return null; @@ -91,21 +103,23 @@ export function preloadModule( return existingPromise; } else { const modulePromise: Thenable = Promise.all(promises).then(() => { - return __webpack_require__(moduleData.id); + return __webpack_require__(metadata.id); }); modulePromise.then( value => { - const fulfilledThenable: FulfilledThenable = (modulePromise: any); + const fulfilledThenable: FulfilledThenable = + (modulePromise: any); fulfilledThenable.status = 'fulfilled'; fulfilledThenable.value = value; }, reason => { - const rejectedThenable: RejectedThenable = (modulePromise: any); + const rejectedThenable: RejectedThenable = + (modulePromise: any); rejectedThenable.status = 'rejected'; rejectedThenable.reason = reason; }, ); - asyncModuleCache.set(moduleData.id, modulePromise); + asyncModuleCache.set(metadata.id, modulePromise); return modulePromise; } } else if (promises.length > 0) { @@ -117,29 +131,29 @@ export function preloadModule( // Actually require the module or suspend if it's not yet ready. // Increase priority if necessary. -export function requireModule(moduleData: ModuleReference): T { +export function requireModule(metadata: ClientReference): T { let moduleExports; - if (moduleData.async) { + if (metadata.async) { // We assume that preloadModule has been called before, which // should have added something to the module cache. - const promise: any = asyncModuleCache.get(moduleData.id); + const promise: any = asyncModuleCache.get(metadata.id); if (promise.status === 'fulfilled') { moduleExports = promise.value; } else { throw promise.reason; } } else { - moduleExports = __webpack_require__(moduleData.id); + moduleExports = __webpack_require__(metadata.id); } - if (moduleData.name === '*') { + if (metadata.name === '*') { // This is a placeholder value that represents that the caller imported this // as a CommonJS module as is. return moduleExports; } - if (moduleData.name === '') { + if (metadata.name === '') { // This is a placeholder value that represents that the caller accessed the // default property of this if it was an ESM interop module. return moduleExports.__esModule ? moduleExports.default : moduleExports; } - return moduleExports[moduleData.name]; + return moduleExports[metadata.name]; } diff --git a/packages/react-server-dom-webpack/src/ReactFlightDOMClient.js b/packages/react-server-dom-webpack/src/ReactFlightDOMClientBrowser.js similarity index 64% rename from packages/react-server-dom-webpack/src/ReactFlightDOMClient.js rename to packages/react-server-dom-webpack/src/ReactFlightDOMClientBrowser.js index 13fbb0e32881..537b96187a00 100644 --- a/packages/react-server-dom-webpack/src/ReactFlightDOMClient.js +++ b/packages/react-server-dom-webpack/src/ReactFlightDOMClientBrowser.js @@ -11,7 +11,7 @@ import type {Thenable} from 'shared/ReactTypes.js'; import type {Response as FlightResponse} from 'react-client/src/ReactFlightClientStream'; -import type {BundlerConfig} from './ReactFlightClientWebpackBundlerConfig'; +import type {ReactServerValue} from 'react-client/src/ReactFlightReplyClient'; import { createResponse, @@ -22,43 +22,53 @@ import { close, } from 'react-client/src/ReactFlightClientStream'; +import {processReply} from 'react-client/src/ReactFlightReplyClient'; + +type CallServerCallback = (string, args: A) => Promise; + export type Options = { - moduleMap?: BundlerConfig, + callServer?: CallServerCallback, }; +function createResponseFromOptions(options: void | Options) { + return createResponse( + null, + options && options.callServer ? options.callServer : undefined, + ); +} + function startReadingFromStream( response: FlightResponse, stream: ReadableStream, ): void { const reader = stream.getReader(); - function progress({done, value}) { + function progress({ + done, + value, + }: { + done: boolean, + value: ?any, + ... + }): void | Promise { if (done) { close(response); return; } const buffer: Uint8Array = (value: any); processBinaryChunk(response, buffer); - return reader - .read() - .then(progress) - .catch(error); + return reader.read().then(progress).catch(error); } - function error(e) { + function error(e: any) { reportGlobalError(response, e); } - reader - .read() - .then(progress) - .catch(error); + reader.read().then(progress).catch(error); } function createFromReadableStream( stream: ReadableStream, options?: Options, ): Thenable { - const response: FlightResponse = createResponse( - options && options.moduleMap ? options.moduleMap : null, - ); + const response: FlightResponse = createResponseFromOptions(options); startReadingFromStream(response, stream); return getRoot(response); } @@ -67,14 +77,12 @@ function createFromFetch( promiseForResponse: Promise, options?: Options, ): Thenable { - const response: FlightResponse = createResponse( - options && options.moduleMap ? options.moduleMap : null, - ); + const response: FlightResponse = createResponseFromOptions(options); promiseForResponse.then( - function(r) { + function (r) { startReadingFromStream(response, (r.body: any)); }, - function(e) { + function (e) { reportGlobalError(response, e); }, ); @@ -85,9 +93,7 @@ function createFromXHR( request: XMLHttpRequest, options?: Options, ): Thenable { - const response: FlightResponse = createResponse( - options && options.moduleMap ? options.moduleMap : null, - ); + const response: FlightResponse = createResponseFromOptions(options); let processedLength = 0; function progress(e: ProgressEvent): void { const chunk = request.responseText; @@ -109,4 +115,14 @@ function createFromXHR( return getRoot(response); } -export {createFromXHR, createFromFetch, createFromReadableStream}; +function encodeReply( + value: ReactServerValue, +): Promise< + string | URLSearchParams | FormData, +> /* We don't use URLSearchParams yet but maybe */ { + return new Promise((resolve, reject) => { + processReply(value, resolve, reject); + }); +} + +export {createFromXHR, createFromFetch, createFromReadableStream, encodeReply}; diff --git a/packages/react-server-dom-webpack/src/ReactFlightDOMClientEdge.js b/packages/react-server-dom-webpack/src/ReactFlightDOMClientEdge.js new file mode 100644 index 000000000000..9a68b21f6c63 --- /dev/null +++ b/packages/react-server-dom-webpack/src/ReactFlightDOMClientEdge.js @@ -0,0 +1,95 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import type {Thenable} from 'shared/ReactTypes.js'; + +import type {Response as FlightResponse} from 'react-client/src/ReactFlightClientStream'; + +import type {SSRManifest} from './ReactFlightClientWebpackBundlerConfig'; + +import { + createResponse, + getRoot, + reportGlobalError, + processBinaryChunk, + close, +} from 'react-client/src/ReactFlightClientStream'; + +function noServerCall() { + throw new Error( + 'Server Functions cannot be called during initial render. ' + + 'This would create a fetch waterfall. Try to use a Server Component ' + + 'to pass data to Client Components instead.', + ); +} + +export type Options = { + moduleMap?: $NonMaybeType, +}; + +function createResponseFromOptions(options: void | Options) { + return createResponse( + options && options.moduleMap ? options.moduleMap : null, + noServerCall, + ); +} + +function startReadingFromStream( + response: FlightResponse, + stream: ReadableStream, +): void { + const reader = stream.getReader(); + function progress({ + done, + value, + }: { + done: boolean, + value: ?any, + ... + }): void | Promise { + if (done) { + close(response); + return; + } + const buffer: Uint8Array = (value: any); + processBinaryChunk(response, buffer); + return reader.read().then(progress).catch(error); + } + function error(e: any) { + reportGlobalError(response, e); + } + reader.read().then(progress).catch(error); +} + +function createFromReadableStream( + stream: ReadableStream, + options?: Options, +): Thenable { + const response: FlightResponse = createResponseFromOptions(options); + startReadingFromStream(response, stream); + return getRoot(response); +} + +function createFromFetch( + promiseForResponse: Promise, + options?: Options, +): Thenable { + const response: FlightResponse = createResponseFromOptions(options); + promiseForResponse.then( + function (r) { + startReadingFromStream(response, (r.body: any)); + }, + function (e) { + reportGlobalError(response, e); + }, + ); + return getRoot(response); +} + +export {createFromFetch, createFromReadableStream}; diff --git a/packages/react-server-dom-webpack/src/ReactFlightDOMClientNode.js b/packages/react-server-dom-webpack/src/ReactFlightDOMClientNode.js new file mode 100644 index 000000000000..09502a7bf547 --- /dev/null +++ b/packages/react-server-dom-webpack/src/ReactFlightDOMClientNode.js @@ -0,0 +1,54 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import type {Thenable} from 'shared/ReactTypes.js'; + +import type {Response} from 'react-client/src/ReactFlightClientStream'; + +import type {SSRManifest} from 'react-client/src/ReactFlightClientHostConfig'; + +import type {Readable} from 'stream'; + +import { + createResponse, + getRoot, + reportGlobalError, + processBinaryChunk, + close, +} from 'react-client/src/ReactFlightClientStream'; +import {processStringChunk} from '../../react-client/src/ReactFlightClientStream'; + +function noServerCall() { + throw new Error( + 'Server Functions cannot be called during initial render. ' + + 'This would create a fetch waterfall. Try to use a Server Component ' + + 'to pass data to Client Components instead.', + ); +} + +function createFromNodeStream( + stream: Readable, + moduleMap: $NonMaybeType, +): Thenable { + const response: Response = createResponse(moduleMap, noServerCall); + stream.on('data', chunk => { + if (typeof chunk === 'string') { + processStringChunk(response, chunk, 0); + } else { + processBinaryChunk(response, chunk); + } + }); + stream.on('error', error => { + reportGlobalError(response, error); + }); + stream.on('end', () => close(response)); + return getRoot(response); +} + +export {createFromNodeStream}; diff --git a/packages/react-server-dom-webpack/src/ReactFlightDOMServerBrowser.js b/packages/react-server-dom-webpack/src/ReactFlightDOMServerBrowser.js index be2343121e2a..5884f18cb30c 100644 --- a/packages/react-server-dom-webpack/src/ReactFlightDOMServerBrowser.js +++ b/packages/react-server-dom-webpack/src/ReactFlightDOMServerBrowser.js @@ -7,9 +7,10 @@ * @flow */ -import type {ReactModel} from 'react-server/src/ReactFlightServer'; -import type {ServerContextJSONValue} from 'shared/ReactTypes'; -import type {BundlerConfig} from './ReactFlightServerWebpackBundlerConfig'; +import type {ReactClientValue} from 'react-server/src/ReactFlightServer'; +import type {ServerContextJSONValue, Thenable} from 'shared/ReactTypes'; +import type {ClientManifest} from './ReactFlightServerWebpackBundlerConfig'; +import type {ServerManifest} from 'react-client/src/ReactFlightClientHostConfig'; import { createRequest, @@ -18,6 +19,14 @@ import { abort, } from 'react-server/src/ReactFlightServer'; +import { + createResponse, + close, + resolveField, + resolveFile, + getRoot, +} from 'react-server/src/ReactFlightReplyServer'; + type Options = { identifierPrefix?: string, signal?: AbortSignal, @@ -26,8 +35,8 @@ type Options = { }; function renderToReadableStream( - model: ReactModel, - webpackMap: BundlerConfig, + model: ReactClientValue, + webpackMap: ClientManifest, options?: Options, ): ReadableStream { const request = createRequest( @@ -60,10 +69,32 @@ function renderToReadableStream( }, cancel: (reason): ?Promise => {}, }, - // $FlowFixMe size() methods are not allowed on byte streams. + // $FlowFixMe[prop-missing] size() methods are not allowed on byte streams. {highWaterMark: 0}, ); return stream; } -export {renderToReadableStream}; +function decodeReply( + body: string | FormData, + webpackMap: ServerManifest, +): Thenable { + const response = createResponse(webpackMap); + if (typeof body === 'string') { + resolveField(response, 0, body); + } else { + // $FlowFixMe[prop-missing] Flow doesn't know that forEach exists. + body.forEach((value: string | File, key: string) => { + const id = +key; + if (typeof value === 'string') { + resolveField(response, id, value); + } else { + resolveFile(response, id, value); + } + }); + } + close(response); + return getRoot(response); +} + +export {renderToReadableStream, decodeReply}; diff --git a/packages/react-server-dom-webpack/src/ReactFlightDOMServerEdge.js b/packages/react-server-dom-webpack/src/ReactFlightDOMServerEdge.js new file mode 100644 index 000000000000..5884f18cb30c --- /dev/null +++ b/packages/react-server-dom-webpack/src/ReactFlightDOMServerEdge.js @@ -0,0 +1,100 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import type {ReactClientValue} from 'react-server/src/ReactFlightServer'; +import type {ServerContextJSONValue, Thenable} from 'shared/ReactTypes'; +import type {ClientManifest} from './ReactFlightServerWebpackBundlerConfig'; +import type {ServerManifest} from 'react-client/src/ReactFlightClientHostConfig'; + +import { + createRequest, + startWork, + startFlowing, + abort, +} from 'react-server/src/ReactFlightServer'; + +import { + createResponse, + close, + resolveField, + resolveFile, + getRoot, +} from 'react-server/src/ReactFlightReplyServer'; + +type Options = { + identifierPrefix?: string, + signal?: AbortSignal, + context?: Array<[string, ServerContextJSONValue]>, + onError?: (error: mixed) => void, +}; + +function renderToReadableStream( + model: ReactClientValue, + webpackMap: ClientManifest, + options?: Options, +): ReadableStream { + const request = createRequest( + model, + webpackMap, + options ? options.onError : undefined, + options ? options.context : undefined, + options ? options.identifierPrefix : undefined, + ); + if (options && options.signal) { + const signal = options.signal; + if (signal.aborted) { + abort(request, (signal: any).reason); + } else { + const listener = () => { + abort(request, (signal: any).reason); + signal.removeEventListener('abort', listener); + }; + signal.addEventListener('abort', listener); + } + } + const stream = new ReadableStream( + { + type: 'bytes', + start: (controller): ?Promise => { + startWork(request); + }, + pull: (controller): ?Promise => { + startFlowing(request, controller); + }, + cancel: (reason): ?Promise => {}, + }, + // $FlowFixMe[prop-missing] size() methods are not allowed on byte streams. + {highWaterMark: 0}, + ); + return stream; +} + +function decodeReply( + body: string | FormData, + webpackMap: ServerManifest, +): Thenable { + const response = createResponse(webpackMap); + if (typeof body === 'string') { + resolveField(response, 0, body); + } else { + // $FlowFixMe[prop-missing] Flow doesn't know that forEach exists. + body.forEach((value: string | File, key: string) => { + const id = +key; + if (typeof value === 'string') { + resolveField(response, id, value); + } else { + resolveFile(response, id, value); + } + }); + } + close(response); + return getRoot(response); +} + +export {renderToReadableStream, decodeReply}; diff --git a/packages/react-server-dom-webpack/src/ReactFlightDOMServerNode.js b/packages/react-server-dom-webpack/src/ReactFlightDOMServerNode.js index 57dc2101ca8d..f2653dca98c4 100644 --- a/packages/react-server-dom-webpack/src/ReactFlightDOMServerNode.js +++ b/packages/react-server-dom-webpack/src/ReactFlightDOMServerNode.js @@ -7,11 +7,16 @@ * @flow */ -import type {ReactModel} from 'react-server/src/ReactFlightServer'; +import type { + Request, + ReactClientValue, +} from 'react-server/src/ReactFlightServer'; import type {Destination} from 'react-server/src/ReactServerStreamConfigNode'; -import type {BundlerConfig} from './ReactFlightServerWebpackBundlerConfig'; +import type {ClientManifest} from './ReactFlightServerWebpackBundlerConfig'; +import type {ServerManifest} from 'react-client/src/ReactFlightClientHostConfig'; +import type {Busboy} from 'busboy'; import type {Writable} from 'stream'; -import type {ServerContextJSONValue} from 'shared/ReactTypes'; +import type {ServerContextJSONValue, Thenable} from 'shared/ReactTypes'; import { createRequest, @@ -20,7 +25,19 @@ import { abort, } from 'react-server/src/ReactFlightServer'; -function createDrainHandler(destination: Destination, request) { +import { + createResponse, + reportGlobalError, + close, + resolveField, + resolveFile, + resolveFileInfo, + resolveFileChunk, + resolveFileComplete, + getRoot, +} from 'react-server/src/ReactFlightReplyServer'; + +function createDrainHandler(destination: Destination, request: Request) { return () => startFlowing(request, destination); } @@ -36,8 +53,8 @@ type PipeableStream = { }; function renderToPipeableStream( - model: ReactModel, - webpackMap: BundlerConfig, + model: ReactClientValue, + webpackMap: ClientManifest, options?: Options, ): PipeableStream { const request = createRequest( @@ -67,4 +84,61 @@ function renderToPipeableStream( }; } -export {renderToPipeableStream}; +function decodeReplyFromBusboy( + busboyStream: Busboy, + webpackMap: ServerManifest, +): Thenable { + const response = createResponse(webpackMap); + busboyStream.on('field', (name, value) => { + const id = +name; + resolveField(response, id, value); + }); + busboyStream.on('file', (name, value, {filename, encoding, mimeType}) => { + if (encoding.toLowerCase() === 'base64') { + throw new Error( + "React doesn't accept base64 encoded file uploads because we don't expect " + + "form data passed from a browser to ever encode data that way. If that's " + + 'the wrong assumption, we can easily fix it.', + ); + } + const id = +name; + const file = resolveFileInfo(response, id, filename, mimeType); + value.on('data', chunk => { + resolveFileChunk(response, file, chunk); + }); + value.on('end', () => { + resolveFileComplete(response, file); + }); + }); + busboyStream.on('finish', () => { + close(response); + }); + busboyStream.on('error', err => { + reportGlobalError(response, err); + }); + return getRoot(response); +} + +function decodeReply( + body: string | FormData, + webpackMap: ServerManifest, +): Thenable { + const response = createResponse(webpackMap); + if (typeof body === 'string') { + resolveField(response, 0, body); + } else { + // $FlowFixMe[prop-missing] Flow doesn't know that forEach exists. + body.forEach((value: string | File, key: string) => { + const id = +key; + if (typeof value === 'string') { + resolveField(response, id, value); + } else { + resolveFile(response, id, value); + } + }); + } + close(response); + return getRoot(response); +} + +export {renderToPipeableStream, decodeReplyFromBusboy, decodeReply}; diff --git a/packages/react-server-dom-webpack/src/ReactFlightServerWebpackBundlerConfig.js b/packages/react-server-dom-webpack/src/ReactFlightServerWebpackBundlerConfig.js index 8951a3cce88c..444f2c0b2a15 100644 --- a/packages/react-server-dom-webpack/src/ReactFlightServerWebpackBundlerConfig.js +++ b/packages/react-server-dom-webpack/src/ReactFlightServerWebpackBundlerConfig.js @@ -7,53 +7,59 @@ * @flow */ -type WebpackMap = { - [filepath: string]: { - [name: string]: ModuleMetaData, - }, +import type {ReactClientValue} from 'react-server/src/ReactFlightServer'; + +export type ClientManifest = { + [id: string]: ClientReferenceMetadata, +}; + +export type ServerReference = T & { + $$typeof: symbol, + $$id: string, + $$bound: null | Array, }; -export type BundlerConfig = WebpackMap; +export type ServerReferenceId = string; // eslint-disable-next-line no-unused-vars -export type ModuleReference = { +export type ClientReference = { $$typeof: symbol, - filepath: string, - name: string, - async: boolean, + $$id: string, + $$async: boolean, }; -export type ModuleMetaData = { +export type ClientReferenceMetadata = { id: string, chunks: Array, name: string, async: boolean, }; -export type ModuleKey = string; +export type ClientReferenceKey = string; -const MODULE_TAG = Symbol.for('react.module.reference'); +const CLIENT_REFERENCE_TAG = Symbol.for('react.client.reference'); +const SERVER_REFERENCE_TAG = Symbol.for('react.server.reference'); -export function getModuleKey(reference: ModuleReference): ModuleKey { - return ( - reference.filepath + - '#' + - reference.name + - (reference.async ? '#async' : '') - ); +export function getClientReferenceKey( + reference: ClientReference, +): ClientReferenceKey { + return reference.$$async ? reference.$$id + '#async' : reference.$$id; } -export function isModuleReference(reference: Object): boolean { - return reference.$$typeof === MODULE_TAG; +export function isClientReference(reference: Object): boolean { + return reference.$$typeof === CLIENT_REFERENCE_TAG; } -export function resolveModuleMetaData( - config: BundlerConfig, - moduleReference: ModuleReference, -): ModuleMetaData { - const resolvedModuleData = - config[moduleReference.filepath][moduleReference.name]; - if (moduleReference.async) { +export function isServerReference(reference: Object): boolean { + return reference.$$typeof === SERVER_REFERENCE_TAG; +} + +export function resolveClientReferenceMetadata( + config: ClientManifest, + clientReference: ClientReference, +): ClientReferenceMetadata { + const resolvedModuleData = config[clientReference.$$id]; + if (clientReference.$$async) { return { id: resolvedModuleData.id, chunks: resolvedModuleData.chunks, @@ -64,3 +70,17 @@ export function resolveModuleMetaData( return resolvedModuleData; } } + +export function getServerReferenceId( + config: ClientManifest, + serverReference: ServerReference, +): ServerReferenceId { + return serverReference.$$id; +} + +export function getServerReferenceBoundArguments( + config: ClientManifest, + serverReference: ServerReference, +): null | Array { + return serverReference.$$bound; +} diff --git a/packages/react-server-dom-webpack/src/ReactFlightWebpackNodeLoader.js b/packages/react-server-dom-webpack/src/ReactFlightWebpackNodeLoader.js index ff04bd97e48b..1e03a453474c 100644 --- a/packages/react-server-dom-webpack/src/ReactFlightWebpackNodeLoader.js +++ b/packages/react-server-dom-webpack/src/ReactFlightWebpackNodeLoader.js @@ -7,7 +7,7 @@ * @flow */ -import * as acorn from 'acorn'; +import * as acorn from 'acorn-loose'; type ResolveContext = { conditions: Array, @@ -41,6 +41,18 @@ type TransformSourceFunction = ( TransformSourceFunction, ) => Promise<{source: Source}>; +type LoadContext = { + conditions: Array, + format: string | null | void, + importAssertions: Object, +}; + +type LoadFunction = ( + string, + LoadContext, + LoadFunction, +) => Promise<{format: string, shortCircuit?: boolean, source: Source}>; + type Source = string | ArrayBuffer | Uint8Array; let warnedAboutConditionsFlag = false; @@ -70,24 +82,7 @@ export async function resolve( ); } } - const resolved = await defaultResolve(specifier, context, defaultResolve); - if (resolved.url.endsWith('.server.js')) { - const parentURL = context.parentURL; - if (parentURL && !parentURL.endsWith('.server.js')) { - let reason; - if (specifier.endsWith('.server.js')) { - reason = `"${specifier}"`; - } else { - reason = `"${specifier}" (which expands to "${resolved.url}")`; - } - throw new Error( - `Cannot import ${reason} from "${parentURL}". ` + - 'By react-server convention, .server.js files can only be imported from other .server.js files. ' + - 'That way nobody accidentally sends these to the client by indirectly importing it.', - ); - } - } - return resolved; + return await defaultResolve(specifier, context, defaultResolve); } export async function getSource( @@ -100,7 +95,106 @@ export async function getSource( return defaultGetSource(url, context, defaultGetSource); } -function addExportNames(names, node) { +function addLocalExportedNames(names: Map, node: any) { + switch (node.type) { + case 'Identifier': + names.set(node.name, node.name); + return; + case 'ObjectPattern': + for (let i = 0; i < node.properties.length; i++) + addLocalExportedNames(names, node.properties[i]); + return; + case 'ArrayPattern': + for (let i = 0; i < node.elements.length; i++) { + const element = node.elements[i]; + if (element) addLocalExportedNames(names, element); + } + return; + case 'Property': + addLocalExportedNames(names, node.value); + return; + case 'AssignmentPattern': + addLocalExportedNames(names, node.left); + return; + case 'RestElement': + addLocalExportedNames(names, node.argument); + return; + case 'ParenthesizedExpression': + addLocalExportedNames(names, node.expression); + return; + } +} + +function transformServerModule( + source: string, + body: any, + url: string, + loader: LoadFunction, +): string { + // If the same local name is exported more than once, we only need one of the names. + const localNames: Map = new Map(); + const localTypes: Map = new Map(); + + for (let i = 0; i < body.length; i++) { + const node = body[i]; + switch (node.type) { + case 'ExportAllDeclaration': + // If export * is used, the other file needs to explicitly opt into "use server" too. + break; + case 'ExportDefaultDeclaration': + if (node.declaration.type === 'Identifier') { + localNames.set(node.declaration.name, 'default'); + } else if (node.declaration.type === 'FunctionDeclaration') { + if (node.declaration.id) { + localNames.set(node.declaration.id.name, 'default'); + localTypes.set(node.declaration.id.name, 'function'); + } else { + // TODO: This needs to be rewritten inline because it doesn't have a local name. + } + } + continue; + case 'ExportNamedDeclaration': + if (node.declaration) { + if (node.declaration.type === 'VariableDeclaration') { + const declarations = node.declaration.declarations; + for (let j = 0; j < declarations.length; j++) { + addLocalExportedNames(localNames, declarations[j].id); + } + } else { + const name = node.declaration.id.name; + localNames.set(name, name); + if (node.declaration.type === 'FunctionDeclaration') { + localTypes.set(name, 'function'); + } + } + } + if (node.specifiers) { + const specifiers = node.specifiers; + for (let j = 0; j < specifiers.length; j++) { + const specifier = specifiers[j]; + localNames.set(specifier.local.name, specifier.exported.name); + } + } + continue; + } + } + + let newSrc = source + '\n\n;'; + localNames.forEach(function (exported, local) { + if (localTypes.get(local) !== 'function') { + // We first check if the export is a function and if so annotate it. + newSrc += 'if (typeof ' + local + ' === "function") '; + } + newSrc += 'Object.defineProperties(' + local + ',{'; + newSrc += '$$typeof: {value: Symbol.for("react.server.reference")},'; + newSrc += '$$id: {value: ' + JSON.stringify(url + '#' + exported) + '},'; + newSrc += '$$bound: { value: null }'; + newSrc += '});\n'; + }); + return newSrc; +} + +function addExportNames(names: Array, node: any) { switch (node.type) { case 'Identifier': names.push(node.name); @@ -148,38 +242,12 @@ function resolveClientImport( return stashedResolve(specifier, {conditions, parentURL}, stashedResolve); } -async function loadClientImport( - url: string, - defaultTransformSource: TransformSourceFunction, -): Promise<{source: Source}> { - if (stashedGetSource === null) { - throw new Error( - 'Expected getSource to have been called before transformSource', - ); - } - // TODO: Validate that this is another module by calling getFormat. - const {source} = await stashedGetSource( - url, - {format: 'module'}, - stashedGetSource, - ); - return defaultTransformSource( - source, - {format: 'module', url}, - defaultTransformSource, - ); -} - async function parseExportNamesInto( - transformedSource: string, + body: any, names: Array, parentURL: string, - defaultTransformSource, + loader: LoadFunction, ): Promise { - const {body} = acorn.parse(transformedSource, { - ecmaVersion: '2019', - sourceType: 'module', - }); for (let i = 0; i < body.length; i++) { const node = body[i]; switch (node.type) { @@ -189,11 +257,26 @@ async function parseExportNamesInto( continue; } else { const {url} = await resolveClientImport(node.source.value, parentURL); - const {source} = await loadClientImport(url, defaultTransformSource); + const {source} = await loader( + url, + {format: 'module', conditions: [], importAssertions: {}}, + loader, + ); if (typeof source !== 'string') { throw new Error('Expected the transformed source to be a string.'); } - parseExportNamesInto(source, names, url, defaultTransformSource); + let childBody; + try { + childBody = acorn.parse(source, { + ecmaVersion: '2024', + sourceType: 'module', + }).body; + } catch (x) { + // eslint-disable-next-line react-internal/no-production-logging + console.error('Error parsing %s %s', url, x.message); + continue; + } + await parseExportNamesInto(childBody, names, url, loader); continue; } case 'ExportDefaultDeclaration': @@ -221,6 +304,132 @@ async function parseExportNamesInto( } } +async function transformClientModule( + body: any, + url: string, + loader: LoadFunction, +): Promise { + const names: Array = []; + + await parseExportNamesInto(body, names, url, loader); + + let newSrc = + "const CLIENT_REFERENCE = Symbol.for('react.client.reference');\n"; + for (let i = 0; i < names.length; i++) { + const name = names[i]; + if (name === 'default') { + newSrc += 'export default '; + newSrc += 'Object.defineProperties(function() {'; + newSrc += + 'throw new Error(' + + JSON.stringify( + `Attempted to call the default export of ${url} from the server` + + `but it's on the client. It's not possible to invoke a client function from ` + + `the server, it can only be rendered as a Component or passed to props of a` + + `Client Component.`, + ) + + ');'; + } else { + newSrc += 'export const ' + name + ' = '; + newSrc += 'Object.defineProperties(function() {'; + newSrc += + 'throw new Error(' + + JSON.stringify( + `Attempted to call ${name}() from the server but ${name} is on the client. ` + + `It's not possible to invoke a client function from the server, it can ` + + `only be rendered as a Component or passed to props of a Client Component.`, + ) + + ');'; + } + newSrc += '},{'; + newSrc += '$$typeof: {value: CLIENT_REFERENCE},'; + newSrc += '$$id: {value: ' + JSON.stringify(url + '#' + name) + '}'; + newSrc += '});\n'; + } + return newSrc; +} + +async function loadClientImport( + url: string, + defaultTransformSource: TransformSourceFunction, +): Promise<{format: string, shortCircuit?: boolean, source: Source}> { + if (stashedGetSource === null) { + throw new Error( + 'Expected getSource to have been called before transformSource', + ); + } + // TODO: Validate that this is another module by calling getFormat. + const {source} = await stashedGetSource( + url, + {format: 'module'}, + stashedGetSource, + ); + const result = await defaultTransformSource( + source, + {format: 'module', url}, + defaultTransformSource, + ); + return {format: 'module', source: result.source}; +} + +async function transformModuleIfNeeded( + source: string, + url: string, + loader: LoadFunction, +): Promise { + // Do a quick check for the exact string. If it doesn't exist, don't + // bother parsing. + if ( + source.indexOf('use client') === -1 && + source.indexOf('use server') === -1 + ) { + return source; + } + + let body; + try { + body = acorn.parse(source, { + ecmaVersion: '2024', + sourceType: 'module', + }).body; + } catch (x) { + // eslint-disable-next-line react-internal/no-production-logging + console.error('Error parsing %s %s', url, x.message); + return source; + } + + let useClient = false; + let useServer = false; + for (let i = 0; i < body.length; i++) { + const node = body[i]; + if (node.type !== 'ExpressionStatement' || !node.directive) { + break; + } + if (node.directive === 'use client') { + useClient = true; + } + if (node.directive === 'use server') { + useServer = true; + } + } + + if (!useClient && !useServer) { + return source; + } + + if (useClient && useServer) { + throw new Error( + 'Cannot have both "use client" and "use server" directives in the same file.', + ); + } + + if (useClient) { + return transformClientModule(body, url, loader); + } + + return transformServerModule(source, body, url, loader); +} + export async function transformSource( source: Source, context: TransformSourceContext, @@ -231,37 +440,39 @@ export async function transformSource( context, defaultTransformSource, ); - if (context.format === 'module' && context.url.endsWith('.client.js')) { + if (context.format === 'module') { const transformedSource = transformed.source; if (typeof transformedSource !== 'string') { throw new Error('Expected source to have been transformed to a string.'); } - - const names = []; - await parseExportNamesInto( + const newSrc = await transformModuleIfNeeded( transformedSource, - names, context.url, - defaultTransformSource, + (url: string, ctx: LoadContext, defaultLoad: LoadFunction) => { + return loadClientImport(url, defaultTransformSource); + }, ); - - let newSrc = - "const MODULE_REFERENCE = Symbol.for('react.module.reference');\n"; - for (let i = 0; i < names.length; i++) { - const name = names[i]; - if (name === 'default') { - newSrc += 'export default '; - } else { - newSrc += 'export const ' + name + ' = '; - } - newSrc += '{ $$typeof: MODULE_REFERENCE, filepath: '; - newSrc += JSON.stringify(context.url); - newSrc += ', name: '; - newSrc += JSON.stringify(name); - newSrc += '};\n'; - } - return {source: newSrc}; } return transformed; } + +export async function load( + url: string, + context: LoadContext, + defaultLoad: LoadFunction, +): Promise<{format: string, shortCircuit?: boolean, source: Source}> { + const result = await defaultLoad(url, context, defaultLoad); + if (result.format === 'module') { + if (typeof result.source !== 'string') { + throw new Error('Expected source to have been loaded into a string.'); + } + const newSrc = await transformModuleIfNeeded( + result.source, + url, + defaultLoad, + ); + return {format: 'module', source: newSrc}; + } + return result; +} diff --git a/packages/react-server-dom-webpack/src/ReactFlightWebpackNodeRegister.js b/packages/react-server-dom-webpack/src/ReactFlightWebpackNodeRegister.js index 3076a196badd..3b4dec28aced 100644 --- a/packages/react-server-dom-webpack/src/ReactFlightWebpackNodeRegister.js +++ b/packages/react-server-dom-webpack/src/ReactFlightWebpackNodeRegister.js @@ -7,128 +7,304 @@ * @flow */ +const acorn = require('acorn-loose'); + const url = require('url'); const Module = require('module'); module.exports = function register() { - const MODULE_REFERENCE = Symbol.for('react.module.reference'); + const CLIENT_REFERENCE = Symbol.for('react.client.reference'); + const SERVER_REFERENCE = Symbol.for('react.server.reference'); const PROMISE_PROTOTYPE = Promise.prototype; - const proxyHandlers = { - get: function(target, name, receiver) { + // Patch bind on the server to ensure that this creates another + // bound server reference with the additional arguments. + const originalBind = Function.prototype.bind; + /*eslint-disable no-extend-native */ + Function.prototype.bind = (function bind(this: any, self: any) { + // $FlowFixMe[unsupported-syntax] + const newFn = originalBind.apply(this, arguments); + if (this.$$typeof === SERVER_REFERENCE) { + // $FlowFixMe[method-unbinding] + const args = Array.prototype.slice.call(arguments, 1); + newFn.$$typeof = SERVER_REFERENCE; + newFn.$$id = this.$$id; + newFn.$$bound = this.$$bound ? this.$$bound.concat(args) : args; + } + return newFn; + }: any); + + const deepProxyHandlers = { + get: function (target: Function, name: string, receiver: Proxy) { switch (name) { // These names are read by the Flight runtime if you end up using the exports object. case '$$typeof': // These names are a little too common. We should probably have a way to // have the Flight runtime extract the inner target instead. return target.$$typeof; - case 'filepath': - return target.filepath; + case '$$id': + return target.$$id; + case '$$async': + return target.$$async; case 'name': return target.name; - case 'async': - return target.async; + case 'displayName': + return undefined; // We need to special case this because createElement reads it if we pass this // reference. case 'defaultProps': return undefined; + // Avoid this attempting to be serialized. + case 'toJSON': + return undefined; + case Symbol.toPrimitive: + // $FlowFixMe[prop-missing] + return Object.prototype[Symbol.toPrimitive]; + case 'Provider': + throw new Error( + `Cannot render a Client Context Provider on the Server. ` + + `Instead, you can export a Client Component wrapper ` + + `that itself renders a Client Context Provider.`, + ); + } + // eslint-disable-next-line react-internal/safe-string-coercion + const expression = String(target.name) + '.' + String(name); + throw new Error( + `Cannot access ${expression} on the server. ` + + 'You cannot dot into a client module from a server component. ' + + 'You can only pass the imported name through.', + ); + }, + set: function () { + throw new Error('Cannot assign to a client module from a server module.'); + }, + }; + + const proxyHandlers = { + get: function ( + target: Function, + name: string, + receiver: Proxy, + ): $FlowFixMe { + switch (name) { + // These names are read by the Flight runtime if you end up using the exports object. + case '$$typeof': + return target.$$typeof; + case '$$id': + return target.$$id; + case '$$async': + return target.$$async; + case 'name': + return target.name; + // We need to special case this because createElement reads it if we pass this + // reference. + case 'defaultProps': + return undefined; + // Avoid this attempting to be serialized. + case 'toJSON': + return undefined; + case Symbol.toPrimitive: + // $FlowFixMe[prop-missing] + return Object.prototype[Symbol.toPrimitive]; case '__esModule': // Something is conditionally checking which export to use. We'll pretend to be // an ESM compat module but then we'll check again on the client. - target.default = { - $$typeof: MODULE_REFERENCE, - filepath: target.filepath, - // This a placeholder value that tells the client to conditionally use the - // whole object or just the default export. - name: '', - async: target.async, - }; + const moduleId = target.$$id; + target.default = Object.defineProperties( + (function () { + throw new Error( + `Attempted to call the default export of ${moduleId} from the server ` + + `but it's on the client. It's not possible to invoke a client function from ` + + `the server, it can only be rendered as a Component or passed to props of a ` + + `Client Component.`, + ); + }: any), + { + $$typeof: {value: CLIENT_REFERENCE}, + // This a placeholder value that tells the client to conditionally use the + // whole object or just the default export. + $$id: {value: target.$$id + '#'}, + $$async: {value: target.$$async}, + }, + ); return true; case 'then': - if (!target.async) { + if (target.then) { + // Use a cached value + return target.then; + } + if (!target.$$async) { // If this module is expected to return a Promise (such as an AsyncModule) then // we should resolve that with a client reference that unwraps the Promise on // the client. - const then = function then(resolve, reject) { - const moduleReference: {[string]: any, ...} = { - $$typeof: MODULE_REFERENCE, - filepath: target.filepath, - name: '*', // Represents the whole object instead of a particular import. - async: true, - }; - return Promise.resolve( - // $FlowFixMe[incompatible-call] found when upgrading Flow - resolve(new Proxy(moduleReference, proxyHandlers)), - ); - }; - // If this is not used as a Promise but is treated as a reference to a `.then` - // export then we should treat it as a reference to that name. - then.$$typeof = MODULE_REFERENCE; - then.filepath = target.filepath; - // then.name is conveniently already "then" which is the export name we need. - // This will break if it's minified though. + + const clientReference = Object.defineProperties(({}: any), { + $$typeof: {value: CLIENT_REFERENCE}, + $$id: {value: target.$$id}, + $$async: {value: true}, + }); + const proxy = new Proxy(clientReference, proxyHandlers); + + // Treat this as a resolved Promise for React's use() + target.status = 'fulfilled'; + target.value = proxy; + + const then = (target.then = Object.defineProperties( + (function then(resolve, reject: any) { + // Expose to React. + return Promise.resolve(resolve(proxy)); + }: any), + // If this is not used as a Promise but is treated as a reference to a `.then` + // export then we should treat it as a reference to that name. + { + $$typeof: {value: CLIENT_REFERENCE}, + $$id: {value: target.$$id + '#then'}, + $$async: {value: false}, + }, + )); return then; + } else { + // Since typeof .then === 'function' is a feature test we'd continue recursing + // indefinitely if we return a function. Instead, we return an object reference + // if we check further. + return undefined; } } let cachedReference = target[name]; if (!cachedReference) { - cachedReference = target[name] = { - $$typeof: MODULE_REFERENCE, - filepath: target.filepath, - name: name, - async: target.async, - }; + const reference = Object.defineProperties( + (function () { + throw new Error( + // eslint-disable-next-line react-internal/safe-string-coercion + `Attempted to call ${String(name)}() from the server but ${String( + name, + )} is on the client. ` + + `It's not possible to invoke a client function from the server, it can ` + + `only be rendered as a Component or passed to props of a Client Component.`, + ); + }: any), + { + name: {value: name}, + $$typeof: {value: CLIENT_REFERENCE}, + $$id: {value: target.$$id + '#' + name}, + $$async: {value: target.$$async}, + }, + ); + cachedReference = target[name] = new Proxy( + reference, + deepProxyHandlers, + ); } return cachedReference; }, - getPrototypeOf(target) { + getPrototypeOf(target: Function): Object { // Pretend to be a Promise in case anyone asks. return PROMISE_PROTOTYPE; }, - set: function() { + set: function (): empty { throw new Error('Cannot assign to a client module from a server module.'); }, }; // $FlowFixMe[prop-missing] found when upgrading Flow - Module._extensions['.client.js'] = function(module, path) { - const moduleId = url.pathToFileURL(path).href; - const moduleReference: {[string]: any, ...} = { - $$typeof: MODULE_REFERENCE, - filepath: moduleId, - name: '*', // Represents the whole object instead of a particular import. - async: false, - }; - // $FlowFixMe[incompatible-call] found when upgrading Flow - module.exports = new Proxy(moduleReference, proxyHandlers); - }; + const originalCompile = Module.prototype._compile; // $FlowFixMe[prop-missing] found when upgrading Flow - const originalResolveFilename = Module._resolveFilename; + Module.prototype._compile = function ( + this: any, + content: string, + filename: string, + ): void { + // Do a quick check for the exact string. If it doesn't exist, don't + // bother parsing. + if ( + content.indexOf('use client') === -1 && + content.indexOf('use server') === -1 + ) { + return originalCompile.apply(this, arguments); + } - // $FlowFixMe[prop-missing] found when upgrading Flow - Module._resolveFilename = function(request, parent, isMain, options) { - const resolved = originalResolveFilename.apply(this, arguments); - if (resolved.endsWith('.server.js')) { - if ( - parent && - parent.filename && - !parent.filename.endsWith('.server.js') - ) { - let reason; - if (request.endsWith('.server.js')) { - reason = `"${request}"`; - } else { - reason = `"${request}" (which expands to "${resolved}")`; + let body; + try { + body = acorn.parse(content, { + ecmaVersion: '2024', + sourceType: 'source', + }).body; + } catch (x) { + // eslint-disable-next-line react-internal/no-production-logging + console.error('Error parsing %s %s', url, x.message); + return originalCompile.apply(this, arguments); + } + + let useClient = false; + let useServer = false; + for (let i = 0; i < body.length; i++) { + const node = body[i]; + if (node.type !== 'ExpressionStatement' || !node.directive) { + break; + } + if (node.directive === 'use client') { + useClient = true; + } + if (node.directive === 'use server') { + useServer = true; + } + } + + if (!useClient && !useServer) { + return originalCompile.apply(this, arguments); + } + + if (useClient && useServer) { + throw new Error( + 'Cannot have both "use client" and "use server" directives in the same file.', + ); + } + + if (useClient) { + const moduleId: string = (url.pathToFileURL(filename).href: any); + const clientReference = Object.defineProperties(({}: any), { + $$typeof: {value: CLIENT_REFERENCE}, + // Represents the whole Module object instead of a particular import. + $$id: {value: moduleId}, + $$async: {value: false}, + }); + // $FlowFixMe[incompatible-call] found when upgrading Flow + this.exports = new Proxy(clientReference, proxyHandlers); + } + + if (useServer) { + originalCompile.apply(this, arguments); + + const moduleId: string = (url.pathToFileURL(filename).href: any); + + const exports = this.exports; + + // This module is imported server to server, but opts in to exposing functions by + // reference. If there are any functions in the export. + if (typeof exports === 'function') { + // The module exports a function directly, + Object.defineProperties((exports: any), { + $$typeof: {value: SERVER_REFERENCE}, + // Represents the whole Module object instead of a particular import. + $$id: {value: moduleId}, + $$bound: {value: null}, + }); + } else { + const keys = Object.keys(exports); + for (let i = 0; i < keys.length; i++) { + const key = keys[i]; + const value = exports[keys[i]]; + if (typeof value === 'function') { + Object.defineProperties((value: any), { + $$typeof: {value: SERVER_REFERENCE}, + $$id: {value: moduleId + '#' + key}, + $$bound: {value: null}, + }); + } } - throw new Error( - `Cannot import ${reason} from "${parent.filename}". ` + - 'By react-server convention, .server.js files can only be imported from other .server.js files. ' + - 'That way nobody accidentally sends these to the client by indirectly importing it.', - ); } } - return resolved; }; }; diff --git a/packages/react-server-dom-webpack/src/ReactFlightWebpackPlugin.js b/packages/react-server-dom-webpack/src/ReactFlightWebpackPlugin.js index 9182597c2b85..298bd4d9b725 100644 --- a/packages/react-server-dom-webpack/src/ReactFlightWebpackPlugin.js +++ b/packages/react-server-dom-webpack/src/ReactFlightWebpackPlugin.js @@ -9,8 +9,8 @@ import {join} from 'path'; import {pathToFileURL} from 'url'; - import asyncLib from 'neo-async'; +import * as acorn from 'acorn-loose'; import ModuleDependency from 'webpack/lib/dependencies/ModuleDependency'; import NullDependency from 'webpack/lib/dependencies/NullDependency'; @@ -40,7 +40,7 @@ class ClientReferenceDependency extends ModuleDependency { // without the client runtime so it's the first time in the loading sequence // you might want them. const clientImportName = 'react-server-dom-webpack/client'; -const clientFileName = require.resolve('../client'); +const clientFileName = require.resolve('../client.browser.js'); type ClientReferenceSearchPath = { directory: string, @@ -55,7 +55,8 @@ type Options = { isServer: boolean, clientReferences?: ClientReferencePath | $ReadOnlyArray, chunkName?: string, - manifestFilename?: string, + clientManifestFilename?: string, + ssrManifestFilename?: string, }; const PLUGIN_NAME = 'React Server Plugin'; @@ -63,7 +64,8 @@ const PLUGIN_NAME = 'React Server Plugin'; export default class ReactFlightWebpackPlugin { clientReferences: $ReadOnlyArray; chunkName: string; - manifestFilename: string; + clientManifestFilename: string; + ssrManifestFilename: string; constructor(options: Options) { if (!options || typeof options.isServer !== 'boolean') { @@ -79,7 +81,7 @@ export default class ReactFlightWebpackPlugin { { directory: '.', recursive: true, - include: /\.client\.(js|ts|jsx|tsx)$/, + include: /\.(js|ts|jsx|tsx)$/, }, ]; } else if ( @@ -99,8 +101,10 @@ export default class ReactFlightWebpackPlugin { } else { this.chunkName = 'client[index]'; } - this.manifestFilename = - options.manifestFilename || 'react-client-manifest.json'; + this.clientManifestFilename = + options.clientManifestFilename || 'react-client-manifest.json'; + this.ssrManifestFilename = + options.ssrManifestFilename || 'react-ssr-manifest.json'; } apply(compiler: any) { @@ -113,13 +117,15 @@ export default class ReactFlightWebpackPlugin { PLUGIN_NAME, ({contextModuleFactory}, callback) => { const contextResolver = compiler.resolverFactory.get('context', {}); + const normalResolver = compiler.resolverFactory.get('normal'); _this.resolveAllClientFiles( compiler.context, contextResolver, + normalResolver, compiler.inputFileSystem, contextModuleFactory, - function(err, resolvedClientRefs) { + function (err, resolvedClientRefs) { if (err) { callback(err); return; @@ -144,6 +150,7 @@ export default class ReactFlightWebpackPlugin { new NullDependency.Template(), ); + // $FlowFixMe[missing-local-annot] const handler = parser => { // We need to add all client references as dependency of something in the graph so // Webpack knows which entries need to know about the relevant chunks and include the @@ -204,28 +211,39 @@ export default class ReactFlightWebpackPlugin { name: PLUGIN_NAME, stage: Compilation.PROCESS_ASSETS_STAGE_REPORT, }, - function() { + function () { if (clientFileNameFound === false) { compilation.warnings.push( new WebpackError( - `Client runtime at ${clientImportName} was not found. React Server Components module map file ${_this.manifestFilename} was not created.`, + `Client runtime at ${clientImportName} was not found. React Server Components module map file ${_this.clientManifestFilename} was not created.`, ), ); return; } - const json = {}; - compilation.chunkGroups.forEach(function(chunkGroup) { - const chunkIds = chunkGroup.chunks.map(function(c) { + const resolvedClientFiles = new Set( + (resolvedClientReferences || []).map(ref => ref.request), + ); + + const clientManifest: { + [string]: {chunks: $FlowFixMe, id: string, name: string}, + } = {}; + const ssrManifest: { + [string]: { + [string]: {specifier: string, name: string}, + }, + } = {}; + compilation.chunkGroups.forEach(function (chunkGroup) { + const chunkIds = chunkGroup.chunks.map(function (c) { return c.id; }); - function recordModule(id, module) { + // $FlowFixMe[missing-local-annot] + function recordModule(id: $FlowFixMe, module) { // TODO: Hook into deps instead of the target module. // That way we know by the type of dep whether to include. // It also resolves conflicts when the same module is in multiple chunks. - - if (!/\.client\.(js|ts)x?$/.test(module.resource)) { + if (!resolvedClientFiles.has(module.resource)) { return; } @@ -233,33 +251,55 @@ export default class ReactFlightWebpackPlugin { .getExportsInfo(module) .getProvidedExports(); - const moduleExports = {}; - ['', '*'] - .concat( - Array.isArray(moduleProvidedExports) - ? moduleProvidedExports - : [], - ) - .forEach(function(name) { - moduleExports[name] = { - id, - chunks: chunkIds, - name: name, - }; - }); const href = pathToFileURL(module.resource).href; if (href !== undefined) { - json[href] = moduleExports; + const ssrExports: { + [string]: {specifier: string, name: string}, + } = {}; + + clientManifest[href] = { + id, + chunks: chunkIds, + name: '*', + }; + ssrExports['*'] = { + specifier: href, + name: '*', + }; + clientManifest[href + '#'] = { + id, + chunks: chunkIds, + name: '', + }; + ssrExports[''] = { + specifier: href, + name: '', + }; + + if (Array.isArray(moduleProvidedExports)) { + moduleProvidedExports.forEach(function (name) { + clientManifest[href + '#' + name] = { + id, + chunks: chunkIds, + name: name, + }; + ssrExports[name] = { + specifier: href, + name: name, + }; + }); + } + + ssrManifest[id] = ssrExports; } } - chunkGroup.chunks.forEach(function(chunk) { - const chunkModules = compilation.chunkGraph.getChunkModulesIterable( - chunk, - ); + chunkGroup.chunks.forEach(function (chunk) { + const chunkModules = + compilation.chunkGraph.getChunkModulesIterable(chunk); - Array.from(chunkModules).forEach(function(module) { + Array.from(chunkModules).forEach(function (module) { const moduleId = compilation.chunkGraph.getModuleId(module); recordModule(moduleId, module); @@ -273,10 +313,15 @@ export default class ReactFlightWebpackPlugin { }); }); - const output = JSON.stringify(json, null, 2); + const clientOutput = JSON.stringify(clientManifest, null, 2); compilation.emitAsset( - _this.manifestFilename, - new sources.RawSource(output, false), + _this.clientManifestFilename, + new sources.RawSource(clientOutput, false), + ); + const ssrOutput = JSON.stringify(ssrManifest, null, 2); + compilation.emitAsset( + _this.ssrManifestFilename, + new sources.RawSource(ssrOutput, false), ); }, ); @@ -288,6 +333,7 @@ export default class ReactFlightWebpackPlugin { resolveAllClientFiles( context: string, contextResolver: any, + normalResolver: any, fs: any, contextModuleFactory: any, callback: ( @@ -295,6 +341,31 @@ export default class ReactFlightWebpackPlugin { result?: $ReadOnlyArray, ) => void, ) { + function hasUseClientDirective(source: string): boolean { + if (source.indexOf('use client') === -1) { + return false; + } + let body; + try { + body = acorn.parse(source, { + ecmaVersion: '2024', + sourceType: 'module', + }).body; + } catch (x) { + return false; + } + for (let i = 0; i < body.length; i++) { + const node = body[i]; + if (node.type !== 'ExpressionStatement' || !node.directive) { + break; + } + if (node.directive === 'use client') { + return true; + } + } + return false; + } + asyncLib.map( this.clientReferences, ( @@ -308,7 +379,8 @@ export default class ReactFlightWebpackPlugin { cb(null, [new ClientReferenceDependency(clientReferencePath)]); return; } - const clientReferenceSearch: ClientReferenceSearchPath = clientReferencePath; + const clientReferenceSearch: ClientReferenceSearchPath = + clientReferencePath; contextResolver.resolve( {}, context, @@ -332,6 +404,7 @@ export default class ReactFlightWebpackPlugin { options, (err2: null | Error, deps: Array) => { if (err2) return cb(err2); + const clientRefDeps = deps.map(dep => { // use userRequest instead of request. request always end with undefined which is wrong const request = join(resolvedDirectory, dep.userRequest); @@ -339,7 +412,38 @@ export default class ReactFlightWebpackPlugin { clientRefDep.userRequest = dep.userRequest; return clientRefDep; }); - cb(null, clientRefDeps); + + asyncLib.filter( + clientRefDeps, + ( + clientRefDep: ClientReferenceDependency, + filterCb: (err: null | Error, truthValue: boolean) => void, + ) => { + normalResolver.resolve( + {}, + context, + clientRefDep.request, + {}, + (err3: null | Error, resolvedPath: mixed) => { + if (err3 || typeof resolvedPath !== 'string') { + return filterCb(null, false); + } + fs.readFile( + resolvedPath, + 'utf-8', + (err4: null | Error, content: string) => { + if (err4 || typeof content !== 'string') { + return filterCb(null, false); + } + const useClient = hasUseClientDirective(content); + filterCb(null, useClient); + }, + ); + }, + ); + }, + cb, + ); }, ); }, @@ -350,7 +454,7 @@ export default class ReactFlightWebpackPlugin { result: $ReadOnlyArray<$ReadOnlyArray>, ): void => { if (err) return callback(err); - const flat = []; + const flat: Array = []; for (let i = 0; i < result.length; i++) { // $FlowFixMe[method-unbinding] flat.push.apply(flat, result[i]); diff --git a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js index 524227cacd76..b2a25dec436d 100644 --- a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js +++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js @@ -10,7 +10,8 @@ 'use strict'; // Polyfills for test environment -global.ReadableStream = require('web-streams-polyfill/ponyfill/es6').ReadableStream; +global.ReadableStream = + require('web-streams-polyfill/ponyfill/es6').ReadableStream; global.TextDecoder = require('util').TextDecoder; // Don't wait before processing work on the server. @@ -25,15 +26,15 @@ let webpackMap; let Stream; let React; let ReactDOMClient; -let ReactServerDOMWriter; -let ReactServerDOMReader; +let ReactServerDOMServer; +let ReactServerDOMClient; let Suspense; let ErrorBoundary; describe('ReactFlightDOM', () => { beforeEach(() => { jest.resetModules(); - act = require('jest-react').act; + act = require('internal-test-utils').act; const WebpackMock = require('./utils/WebpackMock'); clientExports = WebpackMock.clientExports; clientModuleError = WebpackMock.clientModuleError; @@ -44,8 +45,8 @@ describe('ReactFlightDOM', () => { use = React.use; Suspense = React.Suspense; ReactDOMClient = require('react-dom/client'); - ReactServerDOMWriter = require('react-server-dom-webpack/server.node'); - ReactServerDOMReader = require('react-server-dom-webpack/client'); + ReactServerDOMClient = require('react-server-dom-webpack/client'); + ReactServerDOMServer = require('react-server-dom-webpack/server.node.unbundled'); ErrorBoundary = class extends React.Component { state = {hasError: false, error: null}; @@ -108,12 +109,12 @@ describe('ReactFlightDOM', () => { } const {writable, readable} = getTestStream(); - const {pipe} = ReactServerDOMWriter.renderToPipeableStream( + const {pipe} = ReactServerDOMServer.renderToPipeableStream( , webpackMap, ); pipe(writable); - const response = ReactServerDOMReader.createFromReadableStream(readable); + const response = ReactServerDOMClient.createFromReadableStream(readable); const model = await response; expect(model).toEqual({ html: ( @@ -158,16 +159,16 @@ describe('ReactFlightDOM', () => { } const {writable, readable} = getTestStream(); - const {pipe} = ReactServerDOMWriter.renderToPipeableStream( + const {pipe} = ReactServerDOMServer.renderToPipeableStream( , webpackMap, ); pipe(writable); - const response = ReactServerDOMReader.createFromReadableStream(readable); + const response = ReactServerDOMClient.createFromReadableStream(readable); const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); - await act(async () => { + await act(() => { root.render(); }); expect(container.innerHTML).toBe( @@ -195,16 +196,16 @@ describe('ReactFlightDOM', () => { } const {writable, readable} = getTestStream(); - const {pipe} = ReactServerDOMWriter.renderToPipeableStream( + const {pipe} = ReactServerDOMServer.renderToPipeableStream( , webpackMap, ); pipe(writable); - const response = ReactServerDOMReader.createFromReadableStream(readable); + const response = ReactServerDOMClient.createFromReadableStream(readable); const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); - await act(async () => { + await act(() => { root.render(); }); expect(container.innerHTML).toBe('

    $1

    '); @@ -230,21 +231,109 @@ describe('ReactFlightDOM', () => { } const {writable, readable} = getTestStream(); - const {pipe} = ReactServerDOMWriter.renderToPipeableStream( + const {pipe} = ReactServerDOMServer.renderToPipeableStream( , webpackMap, ); pipe(writable); - const response = ReactServerDOMReader.createFromReadableStream(readable); + const response = ReactServerDOMClient.createFromReadableStream(readable); const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); - await act(async () => { + await act(() => { root.render(); }); expect(container.innerHTML).toBe('

    @div

    '); }); + // @gate enableUseHook + it('should be able to esm compat test module references', async () => { + const ESMCompatModule = { + __esModule: true, + default: function ({greeting}) { + return greeting + ' World'; + }, + hi: 'Hello', + }; + + function Print({response}) { + return

    {use(response)}

    ; + } + + function App({response}) { + return ( + Loading...}> + + + ); + } + + function interopWebpack(obj) { + // Basically what Webpack's ESM interop feature testing does. + if (typeof obj === 'object' && obj.__esModule) { + return obj; + } + return Object.assign({default: obj}, obj); + } + + const {default: Component, hi} = interopWebpack( + clientExports(ESMCompatModule), + ); + + const {writable, readable} = getTestStream(); + const {pipe} = ReactServerDOMServer.renderToPipeableStream( + , + webpackMap, + ); + pipe(writable); + const response = ReactServerDOMClient.createFromReadableStream(readable); + + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render(); + }); + expect(container.innerHTML).toBe('

    Hello World

    '); + }); + + // @gate enableUseHook + it('should be able to render a named component export', async () => { + const Module = { + Component: function ({greeting}) { + return greeting + ' World'; + }, + }; + + function Print({response}) { + return

    {use(response)}

    ; + } + + function App({response}) { + return ( + Loading...}> + + + ); + } + + const {Component} = clientExports(Module); + + const {writable, readable} = getTestStream(); + const {pipe} = ReactServerDOMServer.renderToPipeableStream( + , + webpackMap, + ); + pipe(writable); + const response = ReactServerDOMClient.createFromReadableStream(readable); + + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render(); + }); + expect(container.innerHTML).toBe('

    Hello World

    '); + }); + // @gate enableUseHook it('should unwrap async module references', async () => { const AsyncModule = Promise.resolve(function AsyncModule({text}) { @@ -271,21 +360,60 @@ describe('ReactFlightDOM', () => { const AsyncModuleRef2 = await clientExports(AsyncModule2); const {writable, readable} = getTestStream(); - const {pipe} = ReactServerDOMWriter.renderToPipeableStream( + const {pipe} = ReactServerDOMServer.renderToPipeableStream( , webpackMap, ); pipe(writable); - const response = ReactServerDOMReader.createFromReadableStream(readable); + const response = ReactServerDOMClient.createFromReadableStream(readable); const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); - await act(async () => { + await act(() => { root.render(); }); expect(container.innerHTML).toBe('

    Async: Module

    '); }); + // @gate enableUseHook + it('should unwrap async module references using use', async () => { + const AsyncModule = Promise.resolve('Async Text'); + + function Print({response}) { + return use(response); + } + + function App({response}) { + return ( + Loading...}> + + + ); + } + + const AsyncModuleRef = clientExports(AsyncModule); + + function ServerComponent() { + const text = use(AsyncModuleRef); + return

    {text}

    ; + } + + const {writable, readable} = getTestStream(); + const {pipe} = ReactServerDOMServer.renderToPipeableStream( + , + webpackMap, + ); + pipe(writable); + const response = ReactServerDOMClient.createFromReadableStream(readable); + + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render(); + }); + expect(container.innerHTML).toBe('

    Async Text

    '); + }); + // @gate enableUseHook it('should be able to import a name called "then"', async () => { const thenExports = { @@ -309,21 +437,57 @@ describe('ReactFlightDOM', () => { const ThenRef = clientExports(thenExports).then; const {writable, readable} = getTestStream(); - const {pipe} = ReactServerDOMWriter.renderToPipeableStream( + const {pipe} = ReactServerDOMServer.renderToPipeableStream( , webpackMap, ); pipe(writable); - const response = ReactServerDOMReader.createFromReadableStream(readable); + const response = ReactServerDOMClient.createFromReadableStream(readable); const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); - await act(async () => { + await act(() => { root.render(); }); expect(container.innerHTML).toBe('

    and then

    '); }); + it('throws when accessing a member below the client exports', () => { + const ClientModule = clientExports({ + Component: {deep: 'thing'}, + }); + function dotting() { + return ClientModule.Component.deep; + } + expect(dotting).toThrowError( + 'Cannot access Component.deep on the server. ' + + 'You cannot dot into a client module from a server component. ' + + 'You can only pass the imported name through.', + ); + }); + + it('does not throw when React inspects any deep props', () => { + const ClientModule = clientExports({ + Component: function () {}, + }); + ; + }); + + it('throws when accessing a Context.Provider below the client exports', () => { + const Context = React.createContext(); + const ClientModule = clientExports({ + Context, + }); + function dotting() { + return ClientModule.Context.Provider; + } + expect(dotting).toThrowError( + `Cannot render a Client Context Provider on the Server. ` + + `Instead, you can export a Client Component wrapper ` + + `that itself renders a Client Context Provider.`, + ); + }); + // @gate enableUseHook it('should progressively reveal server components', async () => { let reportedErrors = []; @@ -428,7 +592,7 @@ describe('ReactFlightDOM', () => { } const {writable, readable} = getTestStream(); - const {pipe} = ReactServerDOMWriter.renderToPipeableStream( + const {pipe} = ReactServerDOMServer.renderToPipeableStream( model, webpackMap, { @@ -439,11 +603,11 @@ describe('ReactFlightDOM', () => { }, ); pipe(writable); - const response = ReactServerDOMReader.createFromReadableStream(readable); + const response = ReactServerDOMClient.createFromReadableStream(readable); const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); - await act(async () => { + await act(() => { root.render( (loading)

    }> @@ -453,17 +617,17 @@ describe('ReactFlightDOM', () => { expect(container.innerHTML).toBe('

    (loading)

    '); // This isn't enough to show anything. - await act(async () => { + await act(() => { resolveFriends(); }); expect(container.innerHTML).toBe('

    (loading)

    '); // We can now show the details. Sidebar and posts are still loading. - await act(async () => { + await act(() => { resolveName(); }); // Advance time enough to trigger a nested fallback. - await act(async () => { + await act(() => { jest.advanceTimersByTime(500); }); expect(container.innerHTML).toBe( @@ -551,15 +715,15 @@ describe('ReactFlightDOM', () => { const root = ReactDOMClient.createRoot(container); const stream1 = getTestStream(); - const {pipe} = ReactServerDOMWriter.renderToPipeableStream( + const {pipe} = ReactServerDOMServer.renderToPipeableStream( , webpackMap, ); pipe(stream1.writable); - const response1 = ReactServerDOMReader.createFromReadableStream( + const response1 = ReactServerDOMClient.createFromReadableStream( stream1.readable, ); - await act(async () => { + await act(() => { root.render( (loading)

    }> @@ -579,15 +743,15 @@ describe('ReactFlightDOM', () => { inputB.value = 'goodbye'; const stream2 = getTestStream(); - const {pipe: pipe2} = ReactServerDOMWriter.renderToPipeableStream( + const {pipe: pipe2} = ReactServerDOMServer.renderToPipeableStream( , webpackMap, ); pipe2(stream2.writable); - const response2 = ReactServerDOMReader.createFromReadableStream( + const response2 = ReactServerDOMClient.createFromReadableStream( stream2.readable, ); - await act(async () => { + await act(() => { root.render( (loading)

    }> @@ -612,7 +776,7 @@ describe('ReactFlightDOM', () => { const reportedErrors = []; const {writable, readable} = getTestStream(); - const {pipe, abort} = ReactServerDOMWriter.renderToPipeableStream( + const {pipe, abort} = ReactServerDOMServer.renderToPipeableStream(
    , @@ -626,7 +790,7 @@ describe('ReactFlightDOM', () => { }, ); pipe(writable); - const response = ReactServerDOMReader.createFromReadableStream(readable); + const response = ReactServerDOMClient.createFromReadableStream(readable); const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); @@ -635,7 +799,7 @@ describe('ReactFlightDOM', () => { return use(res); } - await act(async () => { + await act(() => { root.render( ( @@ -652,7 +816,7 @@ describe('ReactFlightDOM', () => { }); expect(container.innerHTML).toBe('

    (loading)

    '); - await act(async () => { + await act(() => { abort('for reasons'); }); if (__DEV__) { @@ -670,14 +834,14 @@ describe('ReactFlightDOM', () => { it('should be able to recover from a direct reference erroring client-side', async () => { const reportedErrors = []; - const ClientComponent = clientExports(function({prop}) { + const ClientComponent = clientExports(function ({prop}) { return 'This should never render'; }); const ClientReference = clientModuleError(new Error('module init error')); const {writable, readable} = getTestStream(); - const {pipe} = ReactServerDOMWriter.renderToPipeableStream( + const {pipe} = ReactServerDOMServer.renderToPipeableStream(
    , @@ -689,7 +853,7 @@ describe('ReactFlightDOM', () => { }, ); pipe(writable); - const response = ReactServerDOMReader.createFromReadableStream(readable); + const response = ReactServerDOMClient.createFromReadableStream(readable); const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); @@ -698,7 +862,7 @@ describe('ReactFlightDOM', () => { return use(res); } - await act(async () => { + await act(() => { root.render(

    {e.message}

    }> (loading)

    }> @@ -716,7 +880,7 @@ describe('ReactFlightDOM', () => { it('should be able to recover from a direct reference erroring client-side async', async () => { const reportedErrors = []; - const ClientComponent = clientExports(function({prop}) { + const ClientComponent = clientExports(function ({prop}) { return 'This should never render'; }); @@ -728,7 +892,7 @@ describe('ReactFlightDOM', () => { ); const {writable, readable} = getTestStream(); - const {pipe} = ReactServerDOMWriter.renderToPipeableStream( + const {pipe} = ReactServerDOMServer.renderToPipeableStream(
    , @@ -740,7 +904,7 @@ describe('ReactFlightDOM', () => { }, ); pipe(writable); - const response = ReactServerDOMReader.createFromReadableStream(readable); + const response = ReactServerDOMClient.createFromReadableStream(readable); const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); @@ -749,7 +913,7 @@ describe('ReactFlightDOM', () => { return use(res); } - await act(async () => { + await act(() => { root.render(

    {e.message}

    }> (loading)

    }> @@ -761,7 +925,7 @@ describe('ReactFlightDOM', () => { expect(container.innerHTML).toBe('

    (loading)

    '); - await act(async () => { + await act(() => { rejectPromise(new Error('async module init error')); }); @@ -774,7 +938,7 @@ describe('ReactFlightDOM', () => { it('should be able to recover from a direct reference erroring server-side', async () => { const reportedErrors = []; - const ClientComponent = clientExports(function({prop}) { + const ClientComponent = clientExports(function ({prop}) { return 'This should never render'; }); @@ -788,7 +952,7 @@ describe('ReactFlightDOM', () => { } const {writable, readable} = getTestStream(); - const {pipe} = ReactServerDOMWriter.renderToPipeableStream( + const {pipe} = ReactServerDOMServer.renderToPipeableStream(
    , @@ -802,7 +966,7 @@ describe('ReactFlightDOM', () => { ); pipe(writable); - const response = ReactServerDOMReader.createFromReadableStream(readable); + const response = ReactServerDOMClient.createFromReadableStream(readable); const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); @@ -811,7 +975,7 @@ describe('ReactFlightDOM', () => { return use(res); } - await act(async () => { + await act(() => { root.render( ( @@ -836,4 +1000,118 @@ describe('ReactFlightDOM', () => { expect(reportedErrors).toEqual(['bug in the bundler']); }); + + // @gate enableUseHook + it('should pass a Promise through props and be able use() it on the client', async () => { + async function getData() { + return 'async hello'; + } + + function Component({data}) { + const text = use(data); + return

    {text}

    ; + } + + const ClientComponent = clientExports(Component); + + function ServerComponent() { + const data = getData(); // no await here + return ; + } + + function Print({response}) { + return use(response); + } + + function App({response}) { + return ( + Loading...}> + + + ); + } + + const {writable, readable} = getTestStream(); + const {pipe} = ReactServerDOMServer.renderToPipeableStream( + , + webpackMap, + ); + pipe(writable); + const response = ReactServerDOMClient.createFromReadableStream(readable); + + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render(); + }); + expect(container.innerHTML).toBe('

    async hello

    '); + }); + + // @gate enableUseHook + it('should throw on the client if a passed promise eventually rejects', async () => { + const reportedErrors = []; + const theError = new Error('Server throw'); + + async function getData() { + throw theError; + } + + function Component({data}) { + const text = use(data); + return

    {text}

    ; + } + + const ClientComponent = clientExports(Component); + + function ServerComponent() { + const data = getData(); // no await here + return ; + } + + function Await({response}) { + return use(response); + } + + function App({response}) { + return ( + Loading...}> + ( +

    + {__DEV__ ? e.message + ' + ' : null} + {e.digest} +

    + )}> + +
    +
    + ); + } + + const {writable, readable} = getTestStream(); + const {pipe} = ReactServerDOMServer.renderToPipeableStream( + , + webpackMap, + { + onError(x) { + reportedErrors.push(x); + return __DEV__ ? 'a dev digest' : `digest("${x.message}")`; + }, + }, + ); + pipe(writable); + const response = ReactServerDOMClient.createFromReadableStream(readable); + + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render(); + }); + expect(container.innerHTML).toBe( + __DEV__ + ? '

    Server throw + a dev digest

    ' + : '

    digest("Server throw")

    ', + ); + expect(reportedErrors).toEqual([theError]); + }); }); diff --git a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js index 05b7dd63d6de..d53a9df2e3eb 100644 --- a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js +++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js @@ -10,51 +10,40 @@ 'use strict'; // Polyfills for test environment -global.ReadableStream = require('web-streams-polyfill/ponyfill/es6').ReadableStream; +global.ReadableStream = + require('web-streams-polyfill/ponyfill/es6').ReadableStream; global.TextEncoder = require('util').TextEncoder; global.TextDecoder = require('util').TextDecoder; let clientExports; +let serverExports; let webpackMap; -let webpackModules; +let webpackServerMap; let act; let React; let ReactDOMClient; -let ReactDOMServer; -let ReactServerDOMWriter; -let ReactServerDOMReader; +let ReactServerDOMServer; +let ReactServerDOMClient; let Suspense; let use; describe('ReactFlightDOMBrowser', () => { beforeEach(() => { jest.resetModules(); - act = require('jest-react').act; + act = require('internal-test-utils').act; const WebpackMock = require('./utils/WebpackMock'); clientExports = WebpackMock.clientExports; + serverExports = WebpackMock.serverExports; webpackMap = WebpackMock.webpackMap; - webpackModules = WebpackMock.webpackModules; + webpackServerMap = WebpackMock.webpackServerMap; React = require('react'); ReactDOMClient = require('react-dom/client'); - ReactDOMServer = require('react-dom/server.browser'); - ReactServerDOMWriter = require('react-server-dom-webpack/server.browser'); - ReactServerDOMReader = require('react-server-dom-webpack/client'); + ReactServerDOMServer = require('react-server-dom-webpack/server.browser'); + ReactServerDOMClient = require('react-server-dom-webpack/client'); Suspense = React.Suspense; use = React.use; }); - async function readResult(stream) { - const reader = stream.getReader(); - let result = ''; - while (true) { - const {done, value} = await reader.read(); - if (done) { - return result; - } - result += Buffer.from(value).toString('utf8'); - } - } - function makeDelayedText(Model) { let error, _resolve, _reject; let promise = new Promise((resolve, reject) => { @@ -85,6 +74,21 @@ describe('ReactFlightDOMBrowser', () => { throw theInfinitePromise; } + function requireServerRef(ref) { + const metaData = webpackServerMap[ref]; + const mod = __webpack_require__(metaData.id); + if (metaData.name === '*') { + return mod; + } + return mod[metaData.name]; + } + + async function callServer(actionId, body) { + const fn = requireServerRef(actionId); + const args = await ReactServerDOMServer.decodeReply(body, webpackServerMap); + return fn.apply(null, args); + } + it('should resolve HTML using W3C streams', async () => { function Text({children}) { return {children}; @@ -105,8 +109,8 @@ describe('ReactFlightDOMBrowser', () => { return model; } - const stream = ReactServerDOMWriter.renderToReadableStream(); - const response = ReactServerDOMReader.createFromReadableStream(stream); + const stream = ReactServerDOMServer.renderToReadableStream(); + const response = ReactServerDOMClient.createFromReadableStream(stream); const model = await response; expect(model).toEqual({ html: ( @@ -138,8 +142,8 @@ describe('ReactFlightDOMBrowser', () => { return model; } - const stream = ReactServerDOMWriter.renderToReadableStream(); - const response = ReactServerDOMReader.createFromReadableStream(stream); + const stream = ReactServerDOMServer.renderToReadableStream(); + const response = ReactServerDOMClient.createFromReadableStream(stream); const model = await response; expect(model).toEqual({ html: ( @@ -261,7 +265,7 @@ describe('ReactFlightDOMBrowser', () => { return use(response).rootContent; } - const stream = ReactServerDOMWriter.renderToReadableStream( + const stream = ReactServerDOMServer.renderToReadableStream( model, webpackMap, { @@ -271,11 +275,11 @@ describe('ReactFlightDOMBrowser', () => { }, }, ); - const response = ReactServerDOMReader.createFromReadableStream(stream); + const response = ReactServerDOMClient.createFromReadableStream(stream); const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); - await act(async () => { + await act(() => { root.render( (loading)

    }> @@ -285,13 +289,13 @@ describe('ReactFlightDOMBrowser', () => { expect(container.innerHTML).toBe('

    (loading)

    '); // This isn't enough to show anything. - await act(async () => { + await act(() => { resolveFriends(); }); expect(container.innerHTML).toBe('

    (loading)

    '); // We can now show the details. Sidebar and posts are still loading. - await act(async () => { + await act(() => { resolveName(); }); // Advance time enough to trigger a nested fallback. @@ -307,7 +311,7 @@ describe('ReactFlightDOMBrowser', () => { const theError = new Error('Game over'); // Let's *fail* loading games. - await act(async () => { + await act(() => { rejectGames(theError); }); @@ -326,7 +330,7 @@ describe('ReactFlightDOMBrowser', () => { reportedErrors = []; // We can now show the sidebar. - await act(async () => { + await act(() => { resolvePhotos(); }); expect(container.innerHTML).toBe( @@ -337,7 +341,7 @@ describe('ReactFlightDOMBrowser', () => { ); // Show everything. - await act(async () => { + await act(() => { resolvePosts(); }); expect(container.innerHTML).toBe( @@ -400,7 +404,7 @@ describe('ReactFlightDOMBrowser', () => { rootContent: , }; - const stream = ReactServerDOMWriter.renderToReadableStream( + const stream = ReactServerDOMServer.renderToReadableStream( model, webpackMap, ); @@ -425,7 +429,7 @@ describe('ReactFlightDOMBrowser', () => { // Advance time enough to trigger a nested fallback. jest.advanceTimersByTime(500); - await act(async () => {}); + await act(() => {}); expect(flightResponse).toContain('(loading everything)'); expect(flightResponse).toContain('(loading sidebar)'); @@ -433,25 +437,25 @@ describe('ReactFlightDOMBrowser', () => { expect(flightResponse).not.toContain(':friends:'); expect(flightResponse).not.toContain(':name:'); - await act(async () => { + await act(() => { resolveFriends(); }); expect(flightResponse).toContain(':friends:'); - await act(async () => { + await act(() => { resolveName(); }); expect(flightResponse).toContain(':name:'); - await act(async () => { + await act(() => { resolvePhotos(); }); expect(flightResponse).toContain(':photos:'); - await act(async () => { + await act(() => { resolvePosts(); }); @@ -461,52 +465,6 @@ describe('ReactFlightDOMBrowser', () => { expect(isDone).toBeTruthy(); }); - // @gate enableUseHook - it('should allow an alternative module mapping to be used for SSR', async () => { - function ClientComponent() { - return Client Component; - } - // The Client build may not have the same IDs as the Server bundles for the same - // component. - const ClientComponentOnTheClient = clientExports(ClientComponent); - const ClientComponentOnTheServer = clientExports(ClientComponent); - - // In the SSR bundle this module won't exist. We simulate this by deleting it. - const clientId = webpackMap[ClientComponentOnTheClient.filepath]['*'].id; - delete webpackModules[clientId]; - - // Instead, we have to provide a translation from the client meta data to the SSR - // meta data. - const ssrMetaData = webpackMap[ClientComponentOnTheServer.filepath]['*']; - const translationMap = { - [clientId]: { - '*': ssrMetaData, - }, - }; - - function App() { - return ; - } - - const stream = ReactServerDOMWriter.renderToReadableStream( - , - webpackMap, - ); - const response = ReactServerDOMReader.createFromReadableStream(stream, { - moduleMap: translationMap, - }); - - function ClientRoot() { - return use(response); - } - - const ssrStream = await ReactDOMServer.renderToReadableStream( - , - ); - const result = await readResult(ssrStream); - expect(result).toEqual('Client Component'); - }); - // @gate enableUseHook it('should be able to complete after aborting and throw the reason client-side', async () => { const reportedErrors = []; @@ -546,7 +504,7 @@ describe('ReactFlightDOMBrowser', () => { } const controller = new AbortController(); - const stream = ReactServerDOMWriter.renderToReadableStream( + const stream = ReactServerDOMServer.renderToReadableStream(
    , @@ -560,7 +518,7 @@ describe('ReactFlightDOMBrowser', () => { }, }, ); - const response = ReactServerDOMReader.createFromReadableStream(stream); + const response = ReactServerDOMClient.createFromReadableStream(stream); const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); @@ -569,7 +527,7 @@ describe('ReactFlightDOMBrowser', () => { return use(res); } - await act(async () => { + await act(() => { root.render( (loading)

    }> @@ -580,11 +538,7 @@ describe('ReactFlightDOMBrowser', () => { }); expect(container.innerHTML).toBe('

    (loading)

    '); - await act(async () => { - // @TODO this is a hack to work around lack of support for abortSignal.reason in node - // The abort call itself should set this property but since we are testing in node we - // set it here manually - controller.signal.reason = 'for reasons'; + await act(() => { controller.abort('for reasons'); }); const expectedValue = __DEV__ @@ -605,8 +559,8 @@ describe('ReactFlightDOMBrowser', () => { ); } - const stream = ReactServerDOMWriter.renderToReadableStream(); - const response = ReactServerDOMReader.createFromReadableStream(stream); + const stream = ReactServerDOMServer.renderToReadableStream(); + const response = ReactServerDOMClient.createFromReadableStream(stream); function Client() { return use(response); @@ -614,7 +568,7 @@ describe('ReactFlightDOMBrowser', () => { const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); - await act(async () => { + await act(() => { root.render( @@ -639,8 +593,8 @@ describe('ReactFlightDOMBrowser', () => { ); } - const stream = ReactServerDOMWriter.renderToReadableStream(); - const response = ReactServerDOMReader.createFromReadableStream(stream); + const stream = ReactServerDOMServer.renderToReadableStream(); + const response = ReactServerDOMClient.createFromReadableStream(stream); function Client() { return use(response); @@ -648,7 +602,7 @@ describe('ReactFlightDOMBrowser', () => { const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); - await act(async () => { + await act(() => { // Client uses a different renderer. // We reset _currentRenderer here to not trigger a warning about multiple // renderers concurrently using this context @@ -670,8 +624,8 @@ describe('ReactFlightDOMBrowser', () => { ); } - const stream = ReactServerDOMWriter.renderToReadableStream(); - const response = ReactServerDOMReader.createFromReadableStream(stream); + const stream = ReactServerDOMServer.renderToReadableStream(); + const response = ReactServerDOMClient.createFromReadableStream(stream); function Client() { return use(response); @@ -679,7 +633,7 @@ describe('ReactFlightDOMBrowser', () => { const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); - await act(async () => { + await act(() => { root.render( @@ -704,7 +658,7 @@ describe('ReactFlightDOMBrowser', () => { } const reportedErrors = []; - const stream = ReactServerDOMWriter.renderToReadableStream( + const stream = ReactServerDOMServer.renderToReadableStream( , webpackMap, { @@ -714,7 +668,7 @@ describe('ReactFlightDOMBrowser', () => { }, }, ); - const response = ReactServerDOMReader.createFromReadableStream(stream); + const response = ReactServerDOMClient.createFromReadableStream(stream); class ErrorBoundary extends React.Component { state = {error: null}; @@ -737,7 +691,7 @@ describe('ReactFlightDOMBrowser', () => { const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); - await act(async () => { + await act(() => { root.render( @@ -764,8 +718,8 @@ describe('ReactFlightDOMBrowser', () => { return use(thenable); } - const stream = ReactServerDOMWriter.renderToReadableStream(); - const response = ReactServerDOMReader.createFromReadableStream(stream); + const stream = ReactServerDOMServer.renderToReadableStream(); + const response = ReactServerDOMClient.createFromReadableStream(stream); function Client() { return use(response); @@ -773,7 +727,7 @@ describe('ReactFlightDOMBrowser', () => { const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); - await act(async () => { + await act(() => { root.render(); }); expect(container.innerHTML).toBe('Hi'); @@ -800,8 +754,8 @@ describe('ReactFlightDOMBrowser', () => { // Because the thenable resolves synchronously, we should be able to finish // rendering synchronously, with no fallback. - const stream = ReactServerDOMWriter.renderToReadableStream(); - const response = ReactServerDOMReader.createFromReadableStream(stream); + const stream = ReactServerDOMServer.renderToReadableStream(); + const response = ReactServerDOMClient.createFromReadableStream(stream); function Client() { return use(response); @@ -809,9 +763,170 @@ describe('ReactFlightDOMBrowser', () => { const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); - await act(async () => { + await act(() => { root.render(); }); expect(container.innerHTML).toBe('Hi'); }); + + it('can pass a higher order function by reference from server to client', async () => { + let actionProxy; + + function Client({action}) { + actionProxy = action; + return 'Click Me'; + } + + function greet(transform, text) { + return 'Hello ' + transform(text); + } + + function upper(text) { + return text.toUpperCase(); + } + + const ServerModuleA = serverExports({ + greet, + }); + const ServerModuleB = serverExports({ + upper, + }); + const ClientRef = clientExports(Client); + + const boundFn = ServerModuleA.greet.bind(null, ServerModuleB.upper); + + const stream = ReactServerDOMServer.renderToReadableStream( + , + webpackMap, + ); + + const response = ReactServerDOMClient.createFromReadableStream(stream, { + async callServer(ref, args) { + const body = await ReactServerDOMClient.encodeReply(args); + return callServer(ref, body); + }, + }); + + function App() { + return use(response); + } + + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render(); + }); + expect(container.innerHTML).toBe('Click Me'); + expect(typeof actionProxy).toBe('function'); + expect(actionProxy).not.toBe(boundFn); + + const result = await actionProxy('hi'); + expect(result).toBe('Hello HI'); + }); + + it('can bind arguments to a server reference', async () => { + let actionProxy; + + function Client({action}) { + actionProxy = action; + return 'Click Me'; + } + + const greet = serverExports(function greet(a, b, c) { + return a + ' ' + b + c; + }); + const ClientRef = clientExports(Client); + + const stream = ReactServerDOMServer.renderToReadableStream( + , + webpackMap, + ); + + const response = ReactServerDOMClient.createFromReadableStream(stream, { + async callServer(actionId, args) { + const body = await ReactServerDOMClient.encodeReply(args); + return callServer(actionId, body); + }, + }); + + function App() { + return use(response); + } + + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render(); + }); + expect(container.innerHTML).toBe('Click Me'); + expect(typeof actionProxy).toBe('function'); + expect(actionProxy).not.toBe(greet); + + const result = await actionProxy('!'); + expect(result).toBe('Hello World!'); + }); + + it('propagates server reference errors to the client', async () => { + let actionProxy; + + function Client({action}) { + actionProxy = action; + return 'Click Me'; + } + + async function send(text) { + return Promise.reject(new Error(`Error for ${text}`)); + } + + const ServerModule = serverExports({send}); + const ClientRef = clientExports(Client); + + const stream = ReactServerDOMServer.renderToReadableStream( + , + webpackMap, + ); + + const response = ReactServerDOMClient.createFromReadableStream(stream, { + async callServer(actionId, args) { + const body = await ReactServerDOMClient.encodeReply(args); + return ReactServerDOMClient.createFromReadableStream( + ReactServerDOMServer.renderToReadableStream( + callServer(actionId, body), + null, + {onError: error => 'test-error-digest'}, + ), + ); + }, + }); + + function App() { + return use(response); + } + + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render(); + }); + + if (__DEV__) { + await expect(actionProxy('test')).rejects.toThrow('Error for test'); + } else { + let thrownError; + + try { + await actionProxy('test'); + } catch (error) { + thrownError = error; + } + + expect(thrownError).toEqual( + new Error( + 'An error occurred in the Server Components render. The specific message is omitted in production builds to avoid leaking sensitive details. A digest property is included on this error instance which may provide additional details about the nature of the error.', + ), + ); + + expect(thrownError.digest).toBe('test-error-digest'); + } + }); }); diff --git a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMEdge-test.js b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMEdge-test.js new file mode 100644 index 000000000000..e090add747ce --- /dev/null +++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMEdge-test.js @@ -0,0 +1,102 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @emails react-core + */ + +'use strict'; + +// Polyfills for test environment +global.ReadableStream = + require('web-streams-polyfill/ponyfill/es6').ReadableStream; +global.TextEncoder = require('util').TextEncoder; +global.TextDecoder = require('util').TextDecoder; + +// Don't wait before processing work on the server. +// TODO: we can replace this with FlightServer.act(). +global.setTimeout = cb => cb(); + +let clientExports; +let webpackMap; +let webpackModules; +let React; +let ReactDOMServer; +let ReactServerDOMServer; +let ReactServerDOMClient; +let use; + +describe('ReactFlightDOMEdge', () => { + beforeEach(() => { + jest.resetModules(); + const WebpackMock = require('./utils/WebpackMock'); + clientExports = WebpackMock.clientExports; + webpackMap = WebpackMock.webpackMap; + webpackModules = WebpackMock.webpackModules; + React = require('react'); + ReactDOMServer = require('react-dom/server.edge'); + ReactServerDOMServer = require('react-server-dom-webpack/server.edge'); + ReactServerDOMClient = require('react-server-dom-webpack/client.edge'); + use = React.use; + }); + + async function readResult(stream) { + const reader = stream.getReader(); + let result = ''; + while (true) { + const {done, value} = await reader.read(); + if (done) { + return result; + } + result += Buffer.from(value).toString('utf8'); + } + } + + // @gate enableUseHook + it('should allow an alternative module mapping to be used for SSR', async () => { + function ClientComponent() { + return Client Component; + } + // The Client build may not have the same IDs as the Server bundles for the same + // component. + const ClientComponentOnTheClient = clientExports(ClientComponent); + const ClientComponentOnTheServer = clientExports(ClientComponent); + + // In the SSR bundle this module won't exist. We simulate this by deleting it. + const clientId = webpackMap[ClientComponentOnTheClient.$$id].id; + delete webpackModules[clientId]; + + // Instead, we have to provide a translation from the client meta data to the SSR + // meta data. + const ssrMetadata = webpackMap[ClientComponentOnTheServer.$$id]; + const translationMap = { + [clientId]: { + '*': ssrMetadata, + }, + }; + + function App() { + return ; + } + + const stream = ReactServerDOMServer.renderToReadableStream( + , + webpackMap, + ); + const response = ReactServerDOMClient.createFromReadableStream(stream, { + moduleMap: translationMap, + }); + + function ClientRoot() { + return use(response); + } + + const ssrStream = await ReactDOMServer.renderToReadableStream( + , + ); + const result = await readResult(ssrStream); + expect(result).toEqual('Client Component'); + }); +}); diff --git a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMNode-test.js b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMNode-test.js new file mode 100644 index 000000000000..17b65bcddf78 --- /dev/null +++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMNode-test.js @@ -0,0 +1,108 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @emails react-core + */ + +'use strict'; + +// Don't wait before processing work on the server. +// TODO: we can replace this with FlightServer.act(). +global.setImmediate = cb => cb(); + +let clientExports; +let webpackMap; +let webpackModules; +let React; +let ReactDOMServer; +let ReactServerDOMServer; +let ReactServerDOMClient; +let Stream; +let use; + +describe('ReactFlightDOMNode', () => { + beforeEach(() => { + jest.resetModules(); + const WebpackMock = require('./utils/WebpackMock'); + clientExports = WebpackMock.clientExports; + webpackMap = WebpackMock.webpackMap; + webpackModules = WebpackMock.webpackModules; + React = require('react'); + ReactDOMServer = require('react-dom/server.node'); + ReactServerDOMServer = require('react-server-dom-webpack/server.node'); + ReactServerDOMClient = require('react-server-dom-webpack/client.node'); + Stream = require('stream'); + use = React.use; + }); + + function readResult(stream) { + return new Promise((resolve, reject) => { + let buffer = ''; + const writable = new Stream.PassThrough(); + writable.setEncoding('utf8'); + writable.on('data', chunk => { + buffer += chunk; + }); + writable.on('error', error => { + reject(error); + }); + writable.on('end', () => { + resolve(buffer); + }); + stream.pipe(writable); + }); + } + + // @gate enableUseHook + it('should allow an alternative module mapping to be used for SSR', async () => { + function ClientComponent() { + return Client Component; + } + // The Client build may not have the same IDs as the Server bundles for the same + // component. + const ClientComponentOnTheClient = clientExports(ClientComponent); + const ClientComponentOnTheServer = clientExports(ClientComponent); + + // In the SSR bundle this module won't exist. We simulate this by deleting it. + const clientId = webpackMap[ClientComponentOnTheClient.$$id].id; + delete webpackModules[clientId]; + + // Instead, we have to provide a translation from the client meta data to the SSR + // meta data. + const ssrMetadata = webpackMap[ClientComponentOnTheServer.$$id]; + const translationMap = { + [clientId]: { + '*': ssrMetadata, + }, + }; + + function App() { + return ; + } + + const stream = ReactServerDOMServer.renderToPipeableStream( + , + webpackMap, + ); + const readable = new Stream.PassThrough(); + const response = ReactServerDOMClient.createFromNodeStream( + readable, + translationMap, + ); + + stream.pipe(readable); + + function ClientRoot() { + return use(response); + } + + const ssrStream = await ReactDOMServer.renderToPipeableStream( + , + ); + const result = await readResult(ssrStream); + expect(result).toEqual('Client Component'); + }); +}); diff --git a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMReply-test.js b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMReply-test.js new file mode 100644 index 000000000000..e09addaf5e84 --- /dev/null +++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMReply-test.js @@ -0,0 +1,85 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @emails react-core + */ + +'use strict'; + +// Polyfills for test environment +global.ReadableStream = + require('web-streams-polyfill/ponyfill/es6').ReadableStream; +global.TextEncoder = require('util').TextEncoder; +global.TextDecoder = require('util').TextDecoder; + +// let serverExports; +let webpackServerMap; +let ReactServerDOMServer; +let ReactServerDOMClient; + +describe('ReactFlightDOMReply', () => { + beforeEach(() => { + jest.resetModules(); + const WebpackMock = require('./utils/WebpackMock'); + // serverExports = WebpackMock.serverExports; + webpackServerMap = WebpackMock.webpackServerMap; + ReactServerDOMServer = require('react-server-dom-webpack/server.browser'); + ReactServerDOMClient = require('react-server-dom-webpack/client'); + }); + + it('can pass undefined as a reply', async () => { + const body = await ReactServerDOMClient.encodeReply(undefined); + const missing = await ReactServerDOMServer.decodeReply( + body, + webpackServerMap, + ); + expect(missing).toBe(undefined); + + const body2 = await ReactServerDOMClient.encodeReply({ + array: [undefined, null, undefined], + prop: undefined, + }); + const object = await ReactServerDOMServer.decodeReply( + body2, + webpackServerMap, + ); + expect(object.array.length).toBe(3); + expect(object.array[0]).toBe(undefined); + expect(object.array[1]).toBe(null); + expect(object.array[3]).toBe(undefined); + expect(object.prop).toBe(undefined); + // These should really be true but our deserialization doesn't currently deal with it. + expect('3' in object.array).toBe(false); + expect('prop' in object).toBe(false); + }); + + it('can pass an iterable as a reply', async () => { + const body = await ReactServerDOMClient.encodeReply({ + [Symbol.iterator]: function* () { + yield 'A'; + yield 'B'; + yield 'C'; + }, + }); + const iterable = await ReactServerDOMServer.decodeReply( + body, + webpackServerMap, + ); + const items = []; + // eslint-disable-next-line no-for-of-loops/no-for-of-loops + for (const item of iterable) { + items.push(item); + } + expect(items).toEqual(['A', 'B', 'C']); + }); + + it('can pass a BigInt as a reply', async () => { + const body = await ReactServerDOMClient.encodeReply(90071992547409910000n); + const n = await ReactServerDOMServer.decodeReply(body, webpackServerMap); + + expect(n).toEqual(90071992547409910000n); + }); +}); diff --git a/packages/react-server-dom-webpack/src/__tests__/utils/WebpackMock.js b/packages/react-server-dom-webpack/src/__tests__/utils/WebpackMock.js index 82a65409e507..a911c37602f5 100644 --- a/packages/react-server-dom-webpack/src/__tests__/utils/WebpackMock.js +++ b/packages/react-server-dom-webpack/src/__tests__/utils/WebpackMock.js @@ -11,82 +11,79 @@ const url = require('url'); const Module = require('module'); let webpackModuleIdx = 0; -const webpackModules = {}; +const webpackServerModules = {}; +const webpackClientModules = {}; const webpackErroredModules = {}; -const webpackMap = {}; -global.__webpack_require__ = function(id) { +const webpackServerMap = {}; +const webpackClientMap = {}; +global.__webpack_require__ = function (id) { if (webpackErroredModules[id]) { throw webpackErroredModules[id]; } - return webpackModules[id]; + return webpackClientModules[id] || webpackServerModules[id]; }; -const previousLoader = Module._extensions['.client.js']; +const previousCompile = Module.prototype._compile; const register = require('react-server-dom-webpack/node-register'); -// Register node loader +// Register node compile register(); -const nodeLoader = Module._extensions['.client.js']; +const nodeCompile = Module.prototype._compile; -if (previousLoader === nodeLoader) { +if (previousCompile === nodeCompile) { throw new Error( - 'Expected the Node loader to register the .client.js extension', + 'Expected the Node loader to register the _compile extension', ); } -Module._extensions['.client.js'] = previousLoader; +Module.prototype._compile = previousCompile; -exports.webpackMap = webpackMap; -exports.webpackModules = webpackModules; +exports.webpackMap = webpackClientMap; +exports.webpackModules = webpackClientModules; +exports.webpackServerMap = webpackServerMap; exports.clientModuleError = function clientModuleError(moduleError) { const idx = '' + webpackModuleIdx++; webpackErroredModules[idx] = moduleError; const path = url.pathToFileURL(idx).href; - webpackMap[path] = { - '': { - id: idx, - chunks: [], - name: '', - }, - '*': { - id: idx, - chunks: [], - name: '*', - }, + webpackClientMap[path] = { + id: idx, + chunks: [], + name: '*', + }; + webpackClientMap[path + '#'] = { + id: idx, + chunks: [], + name: '', }; const mod = {exports: {}}; - nodeLoader(mod, idx); + nodeCompile.call(mod, '"use client"', idx); return mod.exports; }; exports.clientExports = function clientExports(moduleExports) { const idx = '' + webpackModuleIdx++; - webpackModules[idx] = moduleExports; + webpackClientModules[idx] = moduleExports; const path = url.pathToFileURL(idx).href; - webpackMap[path] = { - '': { - id: idx, - chunks: [], - name: '', - }, - '*': { - id: idx, - chunks: [], - name: '*', - }, + webpackClientMap[path] = { + id: idx, + chunks: [], + name: '*', + }; + webpackClientMap[path + '#'] = { + id: idx, + chunks: [], + name: '', }; if (typeof moduleExports.then === 'function') { moduleExports.then( asyncModuleExports => { for (const name in asyncModuleExports) { - webpackMap[path] = { - [name]: { - id: idx, - chunks: [], - name: name, - }, + webpackClientMap[path + '#' + name] = { + id: idx, + chunks: [], + name: name, }; } }, @@ -94,15 +91,40 @@ exports.clientExports = function clientExports(moduleExports) { ); } for (const name in moduleExports) { - webpackMap[path] = { - [name]: { - id: idx, - chunks: [], - name: name, - }, + webpackClientMap[path + '#' + name] = { + id: idx, + chunks: [], + name: name, }; } const mod = {exports: {}}; - nodeLoader(mod, idx); + nodeCompile.call(mod, '"use client"', idx); + return mod.exports; +}; + +// This tests server to server references. There's another case of client to server references. +exports.serverExports = function serverExports(moduleExports) { + const idx = '' + webpackModuleIdx++; + webpackServerModules[idx] = moduleExports; + const path = url.pathToFileURL(idx).href; + webpackServerMap[path] = { + id: idx, + chunks: [], + name: '*', + }; + webpackServerMap[path + '#'] = { + id: idx, + chunks: [], + name: '', + }; + for (const name in moduleExports) { + webpackServerMap[path + '#' + name] = { + id: idx, + chunks: [], + name: name, + }; + } + const mod = {exports: moduleExports}; + nodeCompile.call(mod, '"use server"', idx); return mod.exports; }; diff --git a/packages/react-server-native-relay/src/ReactFlightNativeRelayClient.js b/packages/react-server-native-relay/src/ReactFlightNativeRelayClient.js index 94f287971441..289a17938804 100644 --- a/packages/react-server-native-relay/src/ReactFlightNativeRelayClient.js +++ b/packages/react-server-native-relay/src/ReactFlightNativeRelayClient.js @@ -15,7 +15,6 @@ import { createResponse, resolveModel, resolveModule, - resolveSymbol, resolveErrorDev, resolveErrorProd, close, @@ -25,29 +24,32 @@ import { export {createResponse, close, getRoot}; export function resolveRow(response: Response, chunk: RowEncoding): void { - if (chunk[0] === 'J') { - // $FlowFixMe `Chunk` doesn't flow into `JSONValue` because of the `E` row type. + if (chunk[0] === 'O') { + // $FlowFixMe[incompatible-call] `Chunk` doesn't flow into `JSONValue` because of the `E` row type. resolveModel(response, chunk[1], chunk[2]); - } else if (chunk[0] === 'M') { - // $FlowFixMe `Chunk` doesn't flow into `JSONValue` because of the `E` row type. + } else if (chunk[0] === 'I') { + // $FlowFixMe[incompatible-call] `Chunk` doesn't flow into `JSONValue` because of the `E` row type. resolveModule(response, chunk[1], chunk[2]); - } else if (chunk[0] === 'S') { - // $FlowFixMe: Flow doesn't support disjoint unions on tuples. - resolveSymbol(response, chunk[1], chunk[2]); } else { if (__DEV__) { resolveErrorDev( response, chunk[1], - // $FlowFixMe: Flow doesn't support disjoint unions on tuples. + // $FlowFixMe[incompatible-call]: Flow doesn't support disjoint unions on tuples. + // $FlowFixMe[incompatible-use] + // $FlowFixMe[prop-missing] chunk[2].digest, - // $FlowFixMe: Flow doesn't support disjoint unions on tuples. + // $FlowFixMe[incompatible-call]: Flow doesn't support disjoint unions on tuples. + // $FlowFixMe[incompatible-use] chunk[2].message || '', - // $FlowFixMe: Flow doesn't support disjoint unions on tuples. + // $FlowFixMe[incompatible-call]: Flow doesn't support disjoint unions on tuples. + // $FlowFixMe[incompatible-use] chunk[2].stack || '', ); } else { - // $FlowFixMe: Flow doesn't support disjoint unions on tuples. + // $FlowFixMe[incompatible-call]: Flow doesn't support disjoint unions on tuples. + // $FlowFixMe[incompatible-use] + // $FlowFixMe[prop-missing] resolveErrorProd(response, chunk[1], chunk[2].digest); } } diff --git a/packages/react-server-native-relay/src/ReactFlightNativeRelayClientHostConfig.js b/packages/react-server-native-relay/src/ReactFlightNativeRelayClientHostConfig.js index 012d005ac83a..e21df4d13e80 100644 --- a/packages/react-server-native-relay/src/ReactFlightNativeRelayClientHostConfig.js +++ b/packages/react-server-native-relay/src/ReactFlightNativeRelayClientHostConfig.js @@ -11,9 +11,9 @@ import type {JSONValue, ResponseBase} from 'react-client/src/ReactFlightClient'; import type {JSResourceReference} from 'JSResourceReference'; -import type {ModuleMetaData} from 'ReactFlightNativeRelayClientIntegration'; +import type {ClientReferenceMetadata} from 'ReactFlightNativeRelayClientIntegration'; -export type ModuleReference = JSResourceReference; +export type ClientReference = JSResourceReference; import { parseModelString, @@ -25,32 +25,46 @@ export { requireModule, } from 'ReactFlightNativeRelayClientIntegration'; -import {resolveModuleReference as resolveModuleReferenceImpl} from 'ReactFlightNativeRelayClientIntegration'; +import {resolveClientReference as resolveClientReferenceImpl} from 'ReactFlightNativeRelayClientIntegration'; import isArray from 'shared/isArray'; -export type {ModuleMetaData} from 'ReactFlightNativeRelayClientIntegration'; +export type {ClientReferenceMetadata} from 'ReactFlightNativeRelayClientIntegration'; -export type BundlerConfig = null; +export type SSRManifest = null; +export type ServerManifest = null; +export type ServerReferenceId = string; export type UninitializedModel = JSONValue; export type Response = ResponseBase; -export function resolveModuleReference( - bundlerConfig: BundlerConfig, - moduleData: ModuleMetaData, -): ModuleReference { - return resolveModuleReferenceImpl(moduleData); +export function resolveClientReference( + bundlerConfig: SSRManifest, + metadata: ClientReferenceMetadata, +): ClientReference { + return resolveClientReferenceImpl(metadata); } -function parseModelRecursively(response: Response, parentObj, key, value) { +export function resolveServerReference( + bundlerConfig: ServerManifest, + id: ServerReferenceId, +): ClientReference { + throw new Error('Not implemented.'); +} + +function parseModelRecursively( + response: Response, + parentObj: {+[key: string]: JSONValue} | $ReadOnlyArray, + key: string, + value: JSONValue, +): $FlowFixMe { if (typeof value === 'string') { return parseModelString(response, parentObj, key, value); } if (typeof value === 'object' && value !== null) { if (isArray(value)) { - const parsedValue = []; + const parsedValue: Array<$FlowFixMe> = []; for (let i = 0; i < value.length; i++) { (parsedValue: any)[i] = parseModelRecursively( response, diff --git a/packages/react-server-native-relay/src/ReactFlightNativeRelayProtocol.js b/packages/react-server-native-relay/src/ReactFlightNativeRelayProtocol.js index a69b14407aec..a242ba36cc92 100644 --- a/packages/react-server-native-relay/src/ReactFlightNativeRelayProtocol.js +++ b/packages/react-server-native-relay/src/ReactFlightNativeRelayProtocol.js @@ -7,7 +7,7 @@ * @flow */ -import type {ModuleMetaData} from 'ReactFlightNativeRelayServerIntegration'; +import type {ClientReferenceMetadata} from 'ReactFlightNativeRelayServerIntegration'; export type JSONValue = | string @@ -18,8 +18,8 @@ export type JSONValue = | Array; export type RowEncoding = - | ['J', number, JSONValue] - | ['M', number, ModuleMetaData] + | ['O', number, JSONValue] + | ['I', number, ClientReferenceMetadata] | ['P', number, string] | ['S', number, string] | [ diff --git a/packages/react-server-native-relay/src/ReactFlightNativeRelayServer.js b/packages/react-server-native-relay/src/ReactFlightNativeRelayServer.js index 4f059abd897d..5e049368f72f 100644 --- a/packages/react-server-native-relay/src/ReactFlightNativeRelayServer.js +++ b/packages/react-server-native-relay/src/ReactFlightNativeRelayServer.js @@ -7,9 +7,9 @@ * @flow */ -import type {ReactModel} from 'react-server/src/ReactFlightServer'; +import type {ReactClientValue} from 'react-server/src/ReactFlightServer'; import type { - BundlerConfig, + ClientManifest, Destination, } from './ReactFlightNativeRelayServerHostConfig'; @@ -20,9 +20,9 @@ import { } from 'react-server/src/ReactFlightServer'; function render( - model: ReactModel, + model: ReactClientValue, destination: Destination, - config: BundlerConfig, + config: ClientManifest, ): void { const request = createRequest(model, config); startWork(request); diff --git a/packages/react-server-native-relay/src/ReactFlightNativeRelayServerHostConfig.js b/packages/react-server-native-relay/src/ReactFlightNativeRelayServerHostConfig.js index 814773b3f128..ab815ae2f014 100644 --- a/packages/react-server-native-relay/src/ReactFlightNativeRelayServerHostConfig.js +++ b/packages/react-server-native-relay/src/ReactFlightNativeRelayServerHostConfig.js @@ -8,18 +8,23 @@ */ import type {RowEncoding, JSONValue} from './ReactFlightNativeRelayProtocol'; -import type {Request, ReactModel} from 'react-server/src/ReactFlightServer'; +import type { + Request, + ReactClientValue, +} from 'react-server/src/ReactFlightServer'; import hasOwnProperty from 'shared/hasOwnProperty'; import isArray from 'shared/isArray'; import type {JSResourceReference} from 'JSResourceReference'; import JSResourceReferenceImpl from 'JSResourceReferenceImpl'; -export type ModuleReference = JSResourceReference; +export type ClientReference = JSResourceReference; +export type ServerReference = T; +export type ServerReferenceId = {}; import type { Destination, - BundlerConfig, - ModuleMetaData, + BundlerConfig as ClientManifest, + ClientReferenceMetadata, } from 'ReactFlightNativeRelayServerIntegration'; import {resolveModelToJSON} from 'react-server/src/ReactFlightServer'; @@ -27,32 +32,52 @@ import {resolveModelToJSON} from 'react-server/src/ReactFlightServer'; import { emitRow, close, - resolveModuleMetaData as resolveModuleMetaDataImpl, + resolveClientReferenceMetadata as resolveClientReferenceMetadataImpl, } from 'ReactFlightNativeRelayServerIntegration'; export type { Destination, - BundlerConfig, - ModuleMetaData, + BundlerConfig as ClientManifest, + ClientReferenceMetadata, } from 'ReactFlightNativeRelayServerIntegration'; -export function isModuleReference(reference: Object): boolean { +export function isClientReference(reference: Object): boolean { return reference instanceof JSResourceReferenceImpl; } -export type ModuleKey = ModuleReference; +export function isServerReference(reference: Object): boolean { + return false; +} + +export type ClientReferenceKey = ClientReference; -export function getModuleKey(reference: ModuleReference): ModuleKey { +export function getClientReferenceKey( + reference: ClientReference, +): ClientReferenceKey { // We use the reference object itself as the key because we assume the // object will be cached by the bundler runtime. return reference; } -export function resolveModuleMetaData( - config: BundlerConfig, - resource: ModuleReference, -): ModuleMetaData { - return resolveModuleMetaDataImpl(config, resource); +export function resolveClientReferenceMetadata( + config: ClientManifest, + resource: ClientReference, +): ClientReferenceMetadata { + return resolveClientReferenceMetadataImpl(config, resource); +} + +export function getServerReferenceId( + config: ClientManifest, + resource: ServerReference, +): ServerReferenceId { + throw new Error('Not implemented.'); +} + +export function getServerReferenceBoundArguments( + config: ClientManifest, + resource: ServerReference, +): Array { + throw new Error('Not implemented.'); } export type Chunk = RowEncoding; @@ -106,9 +131,9 @@ export function processErrorChunkDev( function convertModelToJSON( request: Request, - parent: {+[key: string]: ReactModel} | $ReadOnlyArray, + parent: {+[key: string]: ReactClientValue} | $ReadOnlyArray, key: string, - model: ReactModel, + model: ReactClientValue, ): JSONValue { const json = resolveModelToJSON(request, parent, key, model); if (typeof json === 'object' && json !== null) { @@ -119,7 +144,6 @@ function convertModelToJSON( } return jsonArray; } else { - // $FlowFixMe no good way to define an empty exact object const jsonObj: {[key: string]: JSONValue} = {}; for (const nextKey in json) { if (hasOwnProperty.call(json, nextKey)) { @@ -140,11 +164,10 @@ function convertModelToJSON( export function processModelChunk( request: Request, id: number, - model: ReactModel, + model: ReactClientValue, ): Chunk { - // $FlowFixMe no good way to define an empty exact object const json = convertModelToJSON(request, {}, '', model); - return ['J', id, json]; + return ['O', id, json]; } export function processReferenceChunk( @@ -152,32 +175,16 @@ export function processReferenceChunk( id: number, reference: string, ): Chunk { - return ['J', id, reference]; -} - -export function processModuleChunk( - request: Request, - id: number, - moduleMetaData: ModuleMetaData, -): Chunk { - // The moduleMetaData is already a JSON serializable value. - return ['M', id, moduleMetaData]; -} - -export function processProviderChunk( - request: Request, - id: number, - contextName: string, -): Chunk { - return ['P', id, contextName]; + return ['O', id, reference]; } -export function processSymbolChunk( +export function processImportChunk( request: Request, id: number, - name: string, + clientReferenceMetadata: ClientReferenceMetadata, ): Chunk { - return ['S', id, name]; + // The clientReferenceMetadata is already a JSON serializable value. + return ['I', id, clientReferenceMetadata]; } export function scheduleWork(callback: () => void) { @@ -187,14 +194,13 @@ export function scheduleWork(callback: () => void) { export function flushBuffered(destination: Destination) {} export const supportsRequestStorage = false; -export const requestStorage: AsyncLocalStorage< - Map, -> = (null: any); +export const requestStorage: AsyncLocalStorage> = + (null: any); export function beginWriting(destination: Destination) {} export function writeChunk(destination: Destination, chunk: Chunk): void { - // $FlowFixMe `Chunk` doesn't flow into `JSONValue` because of the `E` row type. + // $FlowFixMe[incompatible-call] `Chunk` doesn't flow into `JSONValue` because of the `E` row type. emitRow(destination, chunk); } @@ -202,7 +208,7 @@ export function writeChunkAndReturn( destination: Destination, chunk: Chunk, ): boolean { - // $FlowFixMe `Chunk` doesn't flow into `JSONValue` because of the `E` row type. + // $FlowFixMe[incompatible-call] `Chunk` doesn't flow into `JSONValue` because of the `E` row type. emitRow(destination, chunk); return true; } diff --git a/packages/react-server-native-relay/src/__mocks__/JSResourceReferenceImpl.js b/packages/react-server-native-relay/src/__mocks__/JSResourceReferenceImpl.js deleted file mode 100644 index 70414ab21135..000000000000 --- a/packages/react-server-native-relay/src/__mocks__/JSResourceReferenceImpl.js +++ /dev/null @@ -1,19 +0,0 @@ -/** - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -'use strict'; - -class JSResourceReferenceImpl { - constructor(moduleId) { - this._moduleId = moduleId; - } - getModuleId() { - return this._moduleId; - } -} - -module.exports = JSResourceReferenceImpl; diff --git a/packages/react-server-native-relay/src/__mocks__/ReactFlightNativeRelayClientIntegration.js b/packages/react-server-native-relay/src/__mocks__/ReactFlightNativeRelayClientIntegration.js index 2d258288359b..40befb5c70ea 100644 --- a/packages/react-server-native-relay/src/__mocks__/ReactFlightNativeRelayClientIntegration.js +++ b/packages/react-server-native-relay/src/__mocks__/ReactFlightNativeRelayClientIntegration.js @@ -10,12 +10,12 @@ import JSResourceReferenceImpl from 'JSResourceReferenceImpl'; const ReactFlightNativeRelayClientIntegration = { - resolveModuleReference(moduleData) { - return new JSResourceReferenceImpl(moduleData); + resolveClientReference(metadata) { + return new JSResourceReferenceImpl(metadata); }, - preloadModule(moduleReference) {}, - requireModule(moduleReference) { - return moduleReference._moduleId; + preloadModule(clientReference) {}, + requireModule(clientReference) { + return clientReference._moduleId; }, }; diff --git a/packages/react-server-native-relay/src/__mocks__/ReactFlightNativeRelayServerIntegration.js b/packages/react-server-native-relay/src/__mocks__/ReactFlightNativeRelayServerIntegration.js index 4eab1733c46b..52371c59eaba 100644 --- a/packages/react-server-native-relay/src/__mocks__/ReactFlightNativeRelayServerIntegration.js +++ b/packages/react-server-native-relay/src/__mocks__/ReactFlightNativeRelayServerIntegration.js @@ -12,7 +12,7 @@ const ReactFlightNativeRelayServerIntegration = { destination.push(json); }, close(destination) {}, - resolveModuleMetaData(config, resource) { + resolveClientReferenceMetadata(config, resource) { return resource._moduleId; }, }; diff --git a/packages/react-server-native-relay/src/__tests__/ReactFlightNativeRelay-test.internal.js b/packages/react-server-native-relay/src/__tests__/ReactFlightNativeRelay-test.internal.js index 1c514d5826ec..1ed48fe30fc9 100644 --- a/packages/react-server-native-relay/src/__tests__/ReactFlightNativeRelay-test.internal.js +++ b/packages/react-server-native-relay/src/__tests__/ReactFlightNativeRelay-test.internal.js @@ -25,8 +25,9 @@ describe('ReactFlightNativeRelay', () => { React = require('react'); // TODO: Switch this out to react-native ReactFabric = require('react-native-renderer/fabric'); - createReactNativeComponentClass = require('react-native/Libraries/ReactPrivate/ReactNativePrivateInterface') - .ReactNativeViewConfigRegistry.register; + createReactNativeComponentClass = + require('react-native/Libraries/ReactPrivate/ReactNativePrivateInterface') + .ReactNativeViewConfigRegistry.register; View = createReactNativeComponentClass('RCTView', () => ({ validAttributes: {}, uiViewClassName: 'RCTView', @@ -122,7 +123,7 @@ describe('ReactFlightNativeRelay', () => { Foo.prototype = Object.create(Bar.prototype); // This is enumerable which some polyfills do. Foo.prototype.constructor = Foo; - Foo.prototype.method = function() {}; + Foo.prototype.method = function () {}; expect(() => { const transport = []; diff --git a/packages/react-server-native-relay/src/__tests__/__snapshots__/ReactFlightNativeRelay-test.internal.js.snap b/packages/react-server-native-relay/src/__tests__/__snapshots__/ReactFlightNativeRelay-test.internal.js.snap index cdeab0b8fb47..324f64d70660 100644 --- a/packages/react-server-native-relay/src/__tests__/__snapshots__/ReactFlightNativeRelay-test.internal.js.snap +++ b/packages/react-server-native-relay/src/__tests__/__snapshots__/ReactFlightNativeRelay-test.internal.js.snap @@ -3,14 +3,14 @@ exports[`ReactFlightNativeRelay can render a Client Component using a module reference and render there 1`] = ` "1 RCTText null - RCTRawText {\\"text\\":\\"Hello\\"} - RCTRawText {\\"text\\":\\", \\"} - RCTRawText {\\"text\\":\\"Seb Smith\\"}" + RCTRawText {"text":"Hello"} + RCTRawText {"text":", "} + RCTRawText {"text":"Seb Smith"}" `; exports[`ReactFlightNativeRelay can render a Server Component 1`] = ` -Object { - "foo": Object { +{ + "foo": { "bar": A diff --git a/packages/react-server/src/ReactFizzClassComponent.js b/packages/react-server/src/ReactFizzClassComponent.js index 291a6d60f1cd..e624dfa94129 100644 --- a/packages/react-server/src/ReactFizzClassComponent.js +++ b/packages/react-server/src/ReactFizzClassComponent.js @@ -10,41 +10,38 @@ import {emptyContextObject} from './ReactFizzContext'; import {readContext} from './ReactFizzNewContext'; -import { - disableLegacyContext, - warnAboutDeprecatedLifecycles, -} from 'shared/ReactFeatureFlags'; +import {disableLegacyContext} from 'shared/ReactFeatureFlags'; import {get as getInstance, set as setInstance} from 'shared/ReactInstanceMap'; import getComponentNameFromType from 'shared/getComponentNameFromType'; import {REACT_CONTEXT_TYPE, REACT_PROVIDER_TYPE} from 'shared/ReactSymbols'; import assign from 'shared/assign'; import isArray from 'shared/isArray'; -const didWarnAboutNoopUpdateForComponent = {}; -const didWarnAboutDeprecatedWillMount = {}; +const didWarnAboutNoopUpdateForComponent: {[string]: boolean} = {}; +const didWarnAboutDeprecatedWillMount: {[string]: boolean} = {}; let didWarnAboutUninitializedState; let didWarnAboutGetSnapshotBeforeUpdateWithoutDidUpdate; let didWarnAboutLegacyLifecyclesAndDerivedState; let didWarnAboutUndefinedDerivedState; -let warnOnUndefinedDerivedState; -let warnOnInvalidCallback; let didWarnAboutDirectlyAssigningPropsToState; let didWarnAboutContextTypeAndContextTypes; let didWarnAboutInvalidateContextType; +let didWarnOnInvalidCallback; if (__DEV__) { - didWarnAboutUninitializedState = new Set(); - didWarnAboutGetSnapshotBeforeUpdateWithoutDidUpdate = new Set(); - didWarnAboutLegacyLifecyclesAndDerivedState = new Set(); - didWarnAboutDirectlyAssigningPropsToState = new Set(); - didWarnAboutUndefinedDerivedState = new Set(); - didWarnAboutContextTypeAndContextTypes = new Set(); - didWarnAboutInvalidateContextType = new Set(); - - const didWarnOnInvalidCallback = new Set(); + didWarnAboutUninitializedState = new Set(); + didWarnAboutGetSnapshotBeforeUpdateWithoutDidUpdate = new Set(); + didWarnAboutLegacyLifecyclesAndDerivedState = new Set(); + didWarnAboutDirectlyAssigningPropsToState = new Set(); + didWarnAboutUndefinedDerivedState = new Set(); + didWarnAboutContextTypeAndContextTypes = new Set(); + didWarnAboutInvalidateContextType = new Set(); + didWarnOnInvalidCallback = new Set(); +} - warnOnInvalidCallback = function(callback: mixed, callerName: string) { +function warnOnInvalidCallback(callback: mixed, callerName: string) { + if (__DEV__) { if (callback === null || typeof callback === 'function') { return; } @@ -58,9 +55,11 @@ if (__DEV__) { callback, ); } - }; + } +} - warnOnUndefinedDerivedState = function(type, partialState) { +function warnOnUndefinedDerivedState(type: any, partialState: any) { + if (__DEV__) { if (partialState === undefined) { const componentName = getComponentNameFromType(type) || 'Component'; if (!didWarnAboutUndefinedDerivedState.has(componentName)) { @@ -72,7 +71,7 @@ if (__DEV__) { ); } } - }; + } } function warnNoop( @@ -106,10 +105,11 @@ type InternalInstance = { }; const classComponentUpdater = { - isMounted(inst) { + isMounted(inst: any) { return false; }, - enqueueSetState(inst, payload, callback) { + // $FlowFixMe[missing-local-annot] + enqueueSetState(inst: any, payload: any, callback) { const internals: InternalInstance = getInstance(inst); if (internals.queue === null) { warnNoop(inst, 'setState'); @@ -122,7 +122,7 @@ const classComponentUpdater = { } } }, - enqueueReplaceState(inst, payload, callback) { + enqueueReplaceState(inst: any, payload: any, callback: null) { const internals: InternalInstance = getInstance(inst); internals.replace = true; internals.queue = [payload]; @@ -132,7 +132,8 @@ const classComponentUpdater = { } } }, - enqueueForceUpdate(inst, callback) { + // $FlowFixMe[missing-local-annot] + enqueueForceUpdate(inst: any, callback) { const internals: InternalInstance = getInstance(inst); if (internals.queue === null) { warnNoop(inst, 'forceUpdate'); @@ -532,15 +533,12 @@ function checkClassInstance(instance: any, ctor: any, newProps: any) { } } -function callComponentWillMount(type, instance) { +function callComponentWillMount(type: any, instance: any) { const oldState = instance.state; if (typeof instance.componentWillMount === 'function') { if (__DEV__) { - if ( - warnAboutDeprecatedLifecycles && - instance.componentWillMount.__suppressDeprecationWarning !== true - ) { + if (instance.componentWillMount.__suppressDeprecationWarning !== true) { const componentName = getComponentNameFromType(type) || 'Unknown'; if (!didWarnAboutDeprecatedWillMount[componentName]) { diff --git a/packages/react-server/src/ReactFizzContext.js b/packages/react-server/src/ReactFizzContext.js index 0a64c0997be2..4dcbdf39fee3 100644 --- a/packages/react-server/src/ReactFizzContext.js +++ b/packages/react-server/src/ReactFizzContext.js @@ -14,10 +14,9 @@ import checkPropTypes from 'shared/checkPropTypes'; let warnedAboutMissingGetChildContext; if (__DEV__) { - warnedAboutMissingGetChildContext = {}; + warnedAboutMissingGetChildContext = ({}: {[string]: boolean}); } -// $FlowFixMe[incompatible-exact] export const emptyContextObject: {} = {}; if (__DEV__) { Object.freeze(emptyContextObject); @@ -32,7 +31,7 @@ export function getMaskedContext(type: any, unmaskedContext: Object): Object { return emptyContextObject; } - const context = {}; + const context: {[string]: $FlowFixMe} = {}; for (const key in contextTypes) { context[key] = unmaskedContext[key]; } @@ -79,8 +78,9 @@ export function processChildContext( for (const contextKey in childContext) { if (!(contextKey in childContextTypes)) { throw new Error( - `${getComponentNameFromType(type) || - 'Unknown'}.getChildContext(): key "${contextKey}" is not defined in childContextTypes.`, + `${ + getComponentNameFromType(type) || 'Unknown' + }.getChildContext(): key "${contextKey}" is not defined in childContextTypes.`, ); } } diff --git a/packages/react-server/src/ReactFizzHooks.js b/packages/react-server/src/ReactFizzHooks.js index b8c1e4c92773..78d6c0f84f70 100644 --- a/packages/react-server/src/ReactFizzHooks.js +++ b/packages/react-server/src/ReactFizzHooks.js @@ -32,7 +32,7 @@ import {makeId} from './ReactServerFormatConfig'; import { enableCache, enableUseHook, - enableUseEventHook, + enableUseEffectEventHook, enableUseMemoCacheHook, } from 'shared/ReactFeatureFlags'; import is from 'shared/objectIs'; @@ -291,7 +291,7 @@ function useContext(context: ReactContext): T { } function basicStateReducer(state: S, action: BasicStateAction): S { - // $FlowFixMe: Flow doesn't like mixed types + // $FlowFixMe[incompatible-use]: Flow doesn't like mixed types return typeof action === 'function' ? action(state) : action; } @@ -440,28 +440,11 @@ function useRef(initialValue: T): {current: T} { } } -export function useLayoutEffect( - create: () => (() => void) | void, - inputs: Array | void | null, -) { - if (__DEV__) { - currentHookNameInDev = 'useLayoutEffect'; - console.error( - 'useLayoutEffect does nothing on the server, because its effect cannot ' + - "be encoded into the server renderer's output format. This will lead " + - 'to a mismatch between the initial, non-hydrated UI and the intended ' + - 'UI. To avoid this, useLayoutEffect should only be used in ' + - 'components that render exclusively on the client. ' + - 'See https://reactjs.org/link/uselayouteffect-ssr for common fixes.', - ); - } -} - function dispatchAction( componentIdentity: Object, queue: UpdateQueue, action: A, -) { +): void { if (numberOfReRenders >= RE_RENDER_LIMIT) { throw new Error( 'Too many re-renders. React limits the number of renders to prevent ' + @@ -507,17 +490,17 @@ export function useCallback( return useMemo(() => callback, deps); } -function throwOnUseEventCall() { +function throwOnUseEffectEventCall() { throw new Error( - "A function wrapped in useEvent can't be called during rendering.", + "A function wrapped in useEffectEvent can't be called during rendering.", ); } -export function useEvent) => Return>( +export function useEffectEvent) => Return>( callback: F, ): F { // $FlowIgnore[incompatible-return] - return throwOnUseEventCall; + return throwOnUseEffectEventCall; } // TODO Decide on how to implement this hook for server rendering. @@ -584,15 +567,7 @@ function use(usable: Usable): T { if (typeof usable.then === 'function') { // This is a thenable. const thenable: Thenable = (usable: any); - - // Track the position of the thenable within this fiber. - const index = thenableIndexCounter; - thenableIndexCounter += 1; - - if (thenableState === null) { - thenableState = createThenableState(); - } - return trackUsedThenable(thenableState, thenable, index); + return unwrapThenable(thenable); } else if ( usable.$$typeof === REACT_CONTEXT_TYPE || usable.$$typeof === REACT_SERVER_CONTEXT_TYPE @@ -606,6 +581,15 @@ function use(usable: Usable): T { throw new Error('An unsupported type was passed to use(): ' + String(usable)); } +export function unwrapThenable(thenable: Thenable): T { + const index = thenableIndexCounter; + thenableIndexCounter += 1; + if (thenableState === null) { + thenableState = createThenableState(); + } + return trackUsedThenable(thenableState, thenable, index); +} + function unsupportedRefresh() { throw new Error('Cache cannot be refreshed during server rendering.'); } @@ -615,7 +599,7 @@ function useCacheRefresh(): (?() => T, ?T) => void { } function useMemoCache(size: number): Array { - const data = new Array(size); + const data = new Array(size); for (let i = 0; i < size; i++) { data[i] = REACT_MEMO_CACHE_SENTINEL; } @@ -632,7 +616,7 @@ export const HooksDispatcher: Dispatcher = { useRef, useState, useInsertionEffect: noop, - useLayoutEffect, + useLayoutEffect: noop, useCallback, // useImperativeHandle is not run in the server environment useImperativeHandle: noop, @@ -651,8 +635,8 @@ export const HooksDispatcher: Dispatcher = { if (enableCache) { HooksDispatcher.useCacheRefresh = useCacheRefresh; } -if (enableUseEventHook) { - HooksDispatcher.useEvent = useEvent; +if (enableUseEffectEventHook) { + HooksDispatcher.useEffectEvent = useEffectEvent; } if (enableUseMemoCacheHook) { HooksDispatcher.useMemoCache = useMemoCache; diff --git a/packages/react-server/src/ReactFizzNewContext.js b/packages/react-server/src/ReactFizzNewContext.js index d0cd3fc112ba..298b9005ab39 100644 --- a/packages/react-server/src/ReactFizzNewContext.js +++ b/packages/react-server/src/ReactFizzNewContext.js @@ -163,7 +163,7 @@ export function switchContext(newSnapshot: ContextSnapshot): void { const next = newSnapshot; if (prev !== next) { if (prev === null) { - // $FlowFixMe: This has to be non-null since it's not equal to prev. + // $FlowFixMe[incompatible-call]: This has to be non-null since it's not equal to prev. pushAllNext(next); } else if (next === null) { popAllPrevious(prev); diff --git a/packages/react-server/src/ReactFizzServer.js b/packages/react-server/src/ReactFizzServer.js index 236c68dfe867..4d8eb6bdd632 100644 --- a/packages/react-server/src/ReactFizzServer.js +++ b/packages/react-server/src/ReactFizzServer.js @@ -18,6 +18,7 @@ import type { ReactProviderType, OffscreenMode, Wakeable, + Thenable, } from 'shared/ReactTypes'; import type {LazyComponent as LazyComponentType} from 'react/src/ReactLazy'; import type { @@ -65,10 +66,11 @@ import { UNINITIALIZED_SUSPENSE_BOUNDARY_ID, assignSuspenseBoundaryID, getChildFormatContext, - writeInitialResources, - writeImmediateResources, + writeResourcesForBoundary, + writePreamble, + writeHoistables, + writePostamble, hoistResources, - hoistResourcesToRoot, prepareToRender, cleanupAfterRender, setCurrentlyRenderingBoundaryResourcesTarget, @@ -101,6 +103,7 @@ import { currentResponseState, setCurrentResponseState, getThenableStateAfterSuspending, + unwrapThenable, } from './ReactFizzHooks'; import {DefaultCacheDispatcher} from './ReactFizzCache'; import {getStackByComponentStackNode} from './ReactFizzComponentStack'; @@ -122,6 +125,7 @@ import { REACT_MEMO_TYPE, REACT_PROVIDER_TYPE, REACT_CONTEXT_TYPE, + REACT_SERVER_CONTEXT_TYPE, REACT_SCOPE_TYPE, REACT_OFFSCREEN_TYPE, } from 'shared/ReactSymbols'; @@ -129,7 +133,6 @@ import ReactSharedInternals from 'shared/ReactSharedInternals'; import { disableLegacyContext, disableModulePatternComponents, - warnAboutDefaultPropsOnFunctionComponents, enableScopeAPI, enableSuspenseAvoidThisFallbackFizz, enableFloat, @@ -222,8 +225,6 @@ export opaque type Request = { clientRenderedBoundaries: Array, // Errored or client rendered but not yet flushed. completedBoundaries: Array, // Completed but not yet fully flushed boundaries to show. partialBoundaries: Array, // Partially completed boundaries that can flush its segments early. - +preamble: Array, // Chunks that need to be emitted before any segment chunks. - +postamble: Array, // Chunks that need to be emitted after segments, waiting for all pending root tasks to finish // onError is called when an error happens anywhere in the tree. It might recover. // The return string is used in production primarily to avoid leaking internals, secondarily to save bytes. // Returning null/undefined will cause a defualt error message in production @@ -276,10 +277,10 @@ export function createRequest( onShellError: void | ((error: mixed) => void), onFatalError: void | ((error: mixed) => void), ): Request { - const pingedTasks = []; + const pingedTasks: Array = []; const abortSet: Set = new Set(); const resources: Resources = createResources(); - const request = { + const request: Request = { destination: null, responseState, progressiveChunkSize: @@ -295,11 +296,9 @@ export function createRequest( completedRootSegment: null, abortableTasks: abortSet, pingedTasks: pingedTasks, - clientRenderedBoundaries: [], - completedBoundaries: [], - partialBoundaries: [], - preamble: [], - postamble: [], + clientRenderedBoundaries: ([]: Array), + completedBoundaries: ([]: Array), + partialBoundaries: ([]: Array), onError: onError === undefined ? defaultErrorHandler : onError, onAllReady: onAllReady === undefined ? noop : onAllReady, onShellReady: onShellReady === undefined ? noop : onShellReady, @@ -597,11 +596,6 @@ function renderSuspenseBoundary( contentRootSegment.textEmbedded, ); contentRootSegment.status = COMPLETED; - if (enableFloat) { - if (newBoundary.pendingTasks === 0) { - hoistCompletedBoundaryResources(request, newBoundary); - } - } queueCompletedSegment(newBoundary, contentRootSegment); if (newBoundary.pendingTasks === 0) { // This must have been the last segment we were waiting on. This boundary is now complete. @@ -655,19 +649,6 @@ function renderSuspenseBoundary( popComponentStackInDEV(task); } -function hoistCompletedBoundaryResources( - request: Request, - completedBoundary: SuspenseBoundary, -): void { - if (request.completedRootSegment !== null || request.pendingRootTasks > 0) { - // The Shell has not flushed yet. we can hoist Resources for this boundary - // all the way to the Root. - hoistResourcesToRoot(request.resources, completedBoundary.resources); - } - // We don't hoist if the root already flushed because late resources will be hoisted - // as boundaries flush -} - function renderBackupSuspenseBoundary( request: Request, task: Task, @@ -696,9 +677,9 @@ function renderHostElement( const children = pushStartInstance( segment.chunks, - request.preamble, type, props, + request.resources, request.responseState, segment.formatContext, segment.lastPushedText, @@ -714,12 +695,18 @@ function renderHostElement( // We expect that errors will fatal the whole task and that we don't need // the correct context. Therefore this is not in a finally. segment.formatContext = prevContext; - pushEndInstance(segment.chunks, request.postamble, type, props); + pushEndInstance( + segment.chunks, + type, + props, + request.responseState, + prevContext, + ); segment.lastPushedText = false; popComponentStackInDEV(task); } -function shouldConstruct(Component) { +function shouldConstruct(Component: any) { return Component.prototype && Component.prototype.isReactComponent; } @@ -795,12 +782,12 @@ function renderClassComponent( popComponentStackInDEV(task); } -const didWarnAboutBadClass = {}; -const didWarnAboutModulePatternComponent = {}; -const didWarnAboutContextTypeOnFunctionComponent = {}; -const didWarnAboutGetDerivedStateOnFunctionComponent = {}; +const didWarnAboutBadClass: {[string]: boolean} = {}; +const didWarnAboutModulePatternComponent: {[string]: boolean} = {}; +const didWarnAboutContextTypeOnFunctionComponent: {[string]: boolean} = {}; +const didWarnAboutGetDerivedStateOnFunctionComponent: {[string]: boolean} = {}; let didWarnAboutReassigningProps = false; -const didWarnAboutDefaultPropsOnFunctionComponent = {}; +const didWarnAboutDefaultPropsOnFunctionComponent: {[string]: boolean} = {}; let didWarnAboutGenerators = false; let didWarnAboutMaps = false; let hasWarnedAboutUsingContextAsConsumer = false; @@ -949,10 +936,7 @@ function validateFunctionComponentInDev(Component: any): void { } } - if ( - warnAboutDefaultPropsOnFunctionComponents && - Component.defaultProps !== undefined - ) { + if (Component.defaultProps !== undefined) { const componentName = getComponentNameFromType(Component) || 'Unknown'; if (!didWarnAboutDefaultPropsOnFunctionComponent[componentName]) { @@ -1012,7 +996,7 @@ function resolveDefaultProps(Component: any, baseProps: Object): Object { function renderForwardRef( request: Request, task: Task, - prevThenableState, + prevThenableState: null | ThenableState, type: any, props: Object, ref: any, @@ -1301,13 +1285,13 @@ function renderElement( ); } +// $FlowFixMe[missing-local-annot] function validateIterable(iterable, iteratorFn: Function): void { if (__DEV__) { // We don't support rendering Generators because it's a mutation. // See https://github.com/facebook/react/issues/12995 if ( typeof Symbol === 'function' && - // $FlowFixMe Flow doesn't know about toStringTag iterable[Symbol.toStringTag] === 'Generator' ) { if (!didWarnAboutGenerators) { @@ -1458,6 +1442,39 @@ function renderNodeDestructiveImpl( } } + // Usables are a valid React node type. When React encounters a Usable in + // a child position, it unwraps it using the same algorithm as `use`. For + // example, for promises, React will throw an exception to unwind the + // stack, then replay the component once the promise resolves. + // + // A difference from `use` is that React will keep unwrapping the value + // until it reaches a non-Usable type. + // + // e.g. Usable>> should resolve to T + const maybeUsable: Object = node; + if (typeof maybeUsable.then === 'function') { + const thenable: Thenable = (maybeUsable: any); + return renderNodeDestructiveImpl( + request, + task, + null, + unwrapThenable(thenable), + ); + } + + if ( + maybeUsable.$$typeof === REACT_CONTEXT_TYPE || + maybeUsable.$$typeof === REACT_SERVER_CONTEXT_TYPE + ) { + const context: ReactContext = (maybeUsable: any); + return renderNodeDestructiveImpl( + request, + task, + null, + readContext(context), + ); + } + // $FlowFixMe[method-unbinding] const childString = Object.prototype.toString.call(node); @@ -1505,7 +1522,11 @@ function renderNodeDestructiveImpl( } } -function renderChildrenArray(request, task, children) { +function renderChildrenArray( + request: Request, + task: Task, + children: Array, +) { const totalChildren = children.length; for (let i = 0; i < totalChildren; i++) { const prevTreeContext = task.treeContext; @@ -1668,7 +1689,7 @@ function erroredTask( } } -function abortTaskSoft(task: Task): void { +function abortTaskSoft(this: Request, task: Task): void { // This aborts task without aborting the parent boundary that it blocks. // It's used for when we didn't need this task to complete the tree. // If task was needed, then it should use abortTask instead. @@ -1797,9 +1818,6 @@ function finishedTask( queueCompletedSegment(boundary, segment); } } - if (enableFloat) { - hoistCompletedBoundaryResources(request, boundary); - } if (boundary.parentFlushed) { // The segment might be part of a segment that didn't flush yet, but if the boundary's // parent flushed, we need to schedule the boundary to be emitted. @@ -2027,7 +2045,7 @@ function flushSubtree( function flushSegment( request: Request, - destination, + destination: Destination, segment: Segment, ): boolean { const boundary = segment.boundary; @@ -2122,31 +2140,6 @@ function flushSegment( } } -function flushInitialResources( - destination: Destination, - resources: Resources, - responseState: ResponseState, - willFlushAllSegments: boolean, -): void { - writeInitialResources( - destination, - resources, - responseState, - willFlushAllSegments, - ); -} - -function flushImmediateResources( - destination: Destination, - request: Request, -): void { - writeImmediateResources( - destination, - request.resources, - request.responseState, - ); -} - function flushClientRenderedBoundary( request: Request, destination: Destination, @@ -2196,6 +2189,14 @@ function flushCompletedBoundary( } completedSegments.length = 0; + if (enableFloat) { + writeResourcesForBoundary( + destination, + boundary.resources, + request.responseState, + ); + } + return writeCompletedBoundaryInstruction( destination, request.responseState, @@ -2231,7 +2232,20 @@ function flushPartialBoundary( } } completedSegments.splice(0, i); - return true; + + if (enableFloat) { + // The way this is structured we only write resources for partial boundaries + // if there is no backpressure. Later before we complete the boundary we + // will write resources regardless of backpressure before we emit the + // completion instruction + return writeResourcesForBoundary( + destination, + boundary.resources, + request.responseState, + ); + } else { + return true; + } } function flushPartiallyCompletedSegment( @@ -2284,13 +2298,7 @@ function flushCompletedQueues( if (completedRootSegment !== null) { if (request.pendingRootTasks === 0) { if (enableFloat) { - const preamble = request.preamble; - for (i = 0; i < preamble.length; i++) { - // we expect the preamble to be tiny and will ignore backpressure - writeChunk(destination, preamble[i]); - } - - flushInitialResources( + writePreamble( destination, request.resources, request.responseState, @@ -2306,7 +2314,7 @@ function flushCompletedQueues( return; } } else if (enableFloat) { - flushImmediateResources(destination, request); + writeHoistables(destination, request.resources, request.responseState); } // We emit client rendering instructions for already emitted boundaries first. @@ -2384,10 +2392,7 @@ function flushCompletedQueues( // either they have pending task or they're complete. ) { if (enableFloat) { - const postamble = request.postamble; - for (let i = 0; i < postamble.length; i++) { - writeChunk(destination, postamble[i]); - } + writePostamble(destination, request.responseState); } completeWriting(destination); flushBuffered(destination); diff --git a/packages/react-server/src/ReactFlightCache.js b/packages/react-server/src/ReactFlightCache.js index 20e5075118b6..7ac8aaa66222 100644 --- a/packages/react-server/src/ReactFlightCache.js +++ b/packages/react-server/src/ReactFlightCache.js @@ -36,7 +36,6 @@ export const DefaultCacheDispatcher: CacheDispatcher = { let entry: AbortSignal | void = (cache.get(createSignal): any); if (entry === undefined) { entry = createSignal(); - // $FlowFixMe[incompatible-use] found when upgrading Flow cache.set(createSignal, entry); } return entry; @@ -47,7 +46,6 @@ export const DefaultCacheDispatcher: CacheDispatcher = { if (entry === undefined) { entry = resourceType(); // TODO: Warn if undefined? - // $FlowFixMe[incompatible-use] found when upgrading Flow cache.set(resourceType, entry); } return entry; diff --git a/packages/react-server/src/ReactFlightHooks.js b/packages/react-server/src/ReactFlightHooks.js index d1fb68326062..270e1f24007b 100644 --- a/packages/react-server/src/ReactFlightHooks.js +++ b/packages/react-server/src/ReactFlightHooks.js @@ -18,6 +18,7 @@ import { import {readContext as readContextImpl} from './ReactFlightNewContext'; import {enableUseHook} from 'shared/ReactFeatureFlags'; import {createThenableState, trackUsedThenable} from './ReactFlightThenable'; +import {isClientReference} from './ReactFlightServerConfig'; let currentRequest = null; let thenableIndexCounter = 0; @@ -47,9 +48,13 @@ export function getThenableStateAfterSuspending(): null | ThenableState { function readContext(context: ReactServerContext): T { if (__DEV__) { if (context.$$typeof !== REACT_SERVER_CONTEXT_TYPE) { - console.error( - 'Only createServerContext is supported in Server Components.', - ); + if (isClientReference(context)) { + console.error('Cannot read a Client Context from a Server Component.'); + } else { + console.error( + 'Only createServerContext is supported in Server Components.', + ); + } } if (currentRequest === null) { console.error( @@ -89,7 +94,7 @@ export const HooksDispatcher: Dispatcher = { return unsupportedRefresh; }, useMemoCache(size: number): Array { - const data = new Array(size); + const data = new Array(size); for (let i = 0; i < size; i++) { data[i] = REACT_MEMO_CACHE_SENTINEL; } @@ -118,7 +123,10 @@ function useId(): string { } function use(usable: Usable): T { - if (usable !== null && typeof usable === 'object') { + if ( + (usable !== null && typeof usable === 'object') || + typeof usable === 'function' + ) { // $FlowFixMe[method-unbinding] if (typeof usable.then === 'function') { // This is a thenable. @@ -138,6 +146,12 @@ function use(usable: Usable): T { } } + if (__DEV__) { + if (isClientReference(usable)) { + console.error('Cannot use() an already resolved Client Reference.'); + } + } + // eslint-disable-next-line react-internal/safe-string-coercion throw new Error('An unsupported type was passed to use(): ' + String(usable)); } diff --git a/packages/react-server/src/ReactFlightNewContext.js b/packages/react-server/src/ReactFlightNewContext.js index 235f43ad80dc..1775110f13dc 100644 --- a/packages/react-server/src/ReactFlightNewContext.js +++ b/packages/react-server/src/ReactFlightNewContext.js @@ -165,7 +165,7 @@ export function switchContext(newSnapshot: ContextSnapshot): void { const next = newSnapshot; if (prev !== next) { if (prev === null) { - // $FlowFixMe: This has to be non-null since it's not equal to prev. + // $FlowFixMe[incompatible-call]: This has to be non-null since it's not equal to prev. pushAllNext(next); } else if (next === null) { popAllPrevious(prev); diff --git a/packages/react-server/src/ReactFlightReplyServer.js b/packages/react-server/src/ReactFlightReplyServer.js new file mode 100644 index 000000000000..83318c418df2 --- /dev/null +++ b/packages/react-server/src/ReactFlightReplyServer.js @@ -0,0 +1,505 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import type {Thenable} from 'shared/ReactTypes'; + +// The server acts as a Client of itself when resolving Server References. +// That's why we import the Client configuration from the Server. +// Everything is aliased as their Server equivalence for clarity. +import type { + ServerReferenceId, + ServerManifest, + ClientReference as ServerReference, +} from 'react-client/src/ReactFlightClientHostConfig'; + +import { + resolveServerReference, + preloadModule, + requireModule, +} from 'react-client/src/ReactFlightClientHostConfig'; + +export type JSONValue = + | number + | null + | boolean + | string + | {+[key: string]: JSONValue} + | $ReadOnlyArray; + +const PENDING = 'pending'; +const BLOCKED = 'blocked'; +const RESOLVED_MODEL = 'resolved_model'; +const INITIALIZED = 'fulfilled'; +const ERRORED = 'rejected'; + +type PendingChunk = { + status: 'pending', + value: null | Array<(T) => mixed>, + reason: null | Array<(mixed) => mixed>, + _response: Response, + then(resolve: (T) => mixed, reject: (mixed) => mixed): void, +}; +type BlockedChunk = { + status: 'blocked', + value: null | Array<(T) => mixed>, + reason: null | Array<(mixed) => mixed>, + _response: Response, + then(resolve: (T) => mixed, reject: (mixed) => mixed): void, +}; +type ResolvedModelChunk = { + status: 'resolved_model', + value: string, + reason: null, + _response: Response, + then(resolve: (T) => mixed, reject: (mixed) => mixed): void, +}; +type InitializedChunk = { + status: 'fulfilled', + value: T, + reason: null, + _response: Response, + then(resolve: (T) => mixed, reject: (mixed) => mixed): void, +}; +type ErroredChunk = { + status: 'rejected', + value: null, + reason: mixed, + _response: Response, + then(resolve: (T) => mixed, reject: (mixed) => mixed): void, +}; +type SomeChunk = + | PendingChunk + | BlockedChunk + | ResolvedModelChunk + | InitializedChunk + | ErroredChunk; + +// $FlowFixMe[missing-this-annot] +function Chunk(status: any, value: any, reason: any, response: Response) { + this.status = status; + this.value = value; + this.reason = reason; + this._response = response; +} +// We subclass Promise.prototype so that we get other methods like .catch +Chunk.prototype = (Object.create(Promise.prototype): any); +// TODO: This doesn't return a new Promise chain unlike the real .then +Chunk.prototype.then = function ( + this: SomeChunk, + resolve: (value: T) => mixed, + reject: (reason: mixed) => mixed, +) { + const chunk: SomeChunk = this; + // If we have resolved content, we try to initialize it first which + // might put us back into one of the other states. + switch (chunk.status) { + case RESOLVED_MODEL: + initializeModelChunk(chunk); + break; + } + // The status might have changed after initialization. + switch (chunk.status) { + case INITIALIZED: + resolve(chunk.value); + break; + case PENDING: + case BLOCKED: + if (resolve) { + if (chunk.value === null) { + chunk.value = ([]: Array<(T) => mixed>); + } + chunk.value.push(resolve); + } + if (reject) { + if (chunk.reason === null) { + chunk.reason = ([]: Array<(mixed) => mixed>); + } + chunk.reason.push(reject); + } + break; + default: + reject(chunk.reason); + break; + } +}; + +export type Response = { + _bundlerConfig: ServerManifest, + _chunks: Map>, + _fromJSON: (key: string, value: JSONValue) => any, +}; + +export function getRoot(response: Response): Thenable { + const chunk = getChunk(response, 0); + return (chunk: any); +} + +function createPendingChunk(response: Response): PendingChunk { + // $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors + return new Chunk(PENDING, null, null, response); +} + +function wakeChunk(listeners: Array<(T) => mixed>, value: T): void { + for (let i = 0; i < listeners.length; i++) { + const listener = listeners[i]; + listener(value); + } +} + +function wakeChunkIfInitialized( + chunk: SomeChunk, + resolveListeners: Array<(T) => mixed>, + rejectListeners: null | Array<(mixed) => mixed>, +): void { + switch (chunk.status) { + case INITIALIZED: + wakeChunk(resolveListeners, chunk.value); + break; + case PENDING: + case BLOCKED: + chunk.value = resolveListeners; + chunk.reason = rejectListeners; + break; + case ERRORED: + if (rejectListeners) { + wakeChunk(rejectListeners, chunk.reason); + } + break; + } +} + +function triggerErrorOnChunk(chunk: SomeChunk, error: mixed): void { + if (chunk.status !== PENDING && chunk.status !== BLOCKED) { + // We already resolved. We didn't expect to see this. + return; + } + const listeners = chunk.reason; + const erroredChunk: ErroredChunk = (chunk: any); + erroredChunk.status = ERRORED; + erroredChunk.reason = error; + if (listeners !== null) { + wakeChunk(listeners, error); + } +} + +function createResolvedModelChunk( + response: Response, + value: string, +): ResolvedModelChunk { + // $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors + return new Chunk(RESOLVED_MODEL, value, null, response); +} + +function resolveModelChunk(chunk: SomeChunk, value: string): void { + if (chunk.status !== PENDING) { + // We already resolved. We didn't expect to see this. + return; + } + const resolveListeners = chunk.value; + const rejectListeners = chunk.reason; + const resolvedChunk: ResolvedModelChunk = (chunk: any); + resolvedChunk.status = RESOLVED_MODEL; + resolvedChunk.value = value; + if (resolveListeners !== null) { + // This is unfortunate that we're reading this eagerly if + // we already have listeners attached since they might no + // longer be rendered or might not be the highest pri. + initializeModelChunk(resolvedChunk); + // The status might have changed after initialization. + wakeChunkIfInitialized(chunk, resolveListeners, rejectListeners); + } +} + +function bindArgs(fn: any, args: any) { + return fn.bind.apply(fn, [null].concat(args)); +} + +function loadServerReference( + response: Response, + id: ServerReferenceId, + bound: null | Thenable>, + parentChunk: SomeChunk, + parentObject: Object, + key: string, +): T { + const serverReference: ServerReference = + resolveServerReference<$FlowFixMe>(response._bundlerConfig, id); + // We expect most servers to not really need this because you'd just have all + // the relevant modules already loaded but it allows for lazy loading of code + // if needed. + const preloadPromise = preloadModule(serverReference); + let promise: Promise; + if (bound) { + promise = Promise.all([(bound: any), preloadPromise]).then( + ([args]: Array) => bindArgs(requireModule(serverReference), args), + ); + } else { + if (preloadPromise) { + promise = Promise.resolve(preloadPromise).then(() => + requireModule(serverReference), + ); + } else { + // Synchronously available + return requireModule(serverReference); + } + } + promise.then( + createModelResolver(parentChunk, parentObject, key), + createModelReject(parentChunk), + ); + // We need a placeholder value that will be replaced later. + return (null: any); +} + +let initializingChunk: ResolvedModelChunk = (null: any); +let initializingChunkBlockedModel: null | {deps: number, value: any} = null; +function initializeModelChunk(chunk: ResolvedModelChunk): void { + const prevChunk = initializingChunk; + const prevBlocked = initializingChunkBlockedModel; + initializingChunk = chunk; + initializingChunkBlockedModel = null; + try { + const value: T = JSON.parse(chunk.value, chunk._response._fromJSON); + if ( + initializingChunkBlockedModel !== null && + initializingChunkBlockedModel.deps > 0 + ) { + initializingChunkBlockedModel.value = value; + // We discovered new dependencies on modules that are not yet resolved. + // We have to go the BLOCKED state until they're resolved. + const blockedChunk: BlockedChunk = (chunk: any); + blockedChunk.status = BLOCKED; + blockedChunk.value = null; + blockedChunk.reason = null; + } else { + const initializedChunk: InitializedChunk = (chunk: any); + initializedChunk.status = INITIALIZED; + initializedChunk.value = value; + } + } catch (error) { + const erroredChunk: ErroredChunk = (chunk: any); + erroredChunk.status = ERRORED; + erroredChunk.reason = error; + } finally { + initializingChunk = prevChunk; + initializingChunkBlockedModel = prevBlocked; + } +} + +// Report that any missing chunks in the model is now going to throw this +// error upon read. Also notify any pending promises. +export function reportGlobalError(response: Response, error: Error): void { + response._chunks.forEach(chunk => { + // If this chunk was already resolved or errored, it won't + // trigger an error but if it wasn't then we need to + // because we won't be getting any new data to resolve it. + if (chunk.status === PENDING) { + triggerErrorOnChunk(chunk, error); + } + }); +} + +function getChunk(response: Response, id: number): SomeChunk { + const chunks = response._chunks; + let chunk = chunks.get(id); + if (!chunk) { + chunk = createPendingChunk(response); + chunks.set(id, chunk); + } + return chunk; +} + +function createModelResolver( + chunk: SomeChunk, + parentObject: Object, + key: string, +): (value: any) => void { + let blocked; + if (initializingChunkBlockedModel) { + blocked = initializingChunkBlockedModel; + blocked.deps++; + } else { + blocked = initializingChunkBlockedModel = { + deps: 1, + value: null, + }; + } + return value => { + parentObject[key] = value; + blocked.deps--; + if (blocked.deps === 0) { + if (chunk.status !== BLOCKED) { + return; + } + const resolveListeners = chunk.value; + const initializedChunk: InitializedChunk = (chunk: any); + initializedChunk.status = INITIALIZED; + initializedChunk.value = blocked.value; + if (resolveListeners !== null) { + wakeChunk(resolveListeners, blocked.value); + } + } + }; +} + +function createModelReject(chunk: SomeChunk): (error: mixed) => void { + return (error: mixed) => triggerErrorOnChunk(chunk, error); +} + +function parseModelString( + response: Response, + parentObject: Object, + key: string, + value: string, +): any { + if (value[0] === '$') { + switch (value[1]) { + case '$': { + // This was an escaped string value. + return value.substring(1); + } + case '@': { + // Promise + const id = parseInt(value.substring(2), 16); + const chunk = getChunk(response, id); + return chunk; + } + case 'S': { + // Symbol + return Symbol.for(value.substring(2)); + } + case 'F': { + // Server Reference + const id = parseInt(value.substring(2), 16); + const chunk = getChunk(response, id); + if (chunk.status === RESOLVED_MODEL) { + initializeModelChunk(chunk); + } + if (chunk.status !== INITIALIZED) { + // We know that this is emitted earlier so otherwise it's an error. + throw chunk.reason; + } + // TODO: Just encode this in the reference inline instead of as a model. + const metaData: {id: ServerReferenceId, bound: Thenable>} = + chunk.value; + return loadServerReference( + response, + metaData.id, + metaData.bound, + initializingChunk, + parentObject, + key, + ); + } + case 'u': { + // matches "$undefined" + // Special encoding for `undefined` which can't be serialized as JSON otherwise. + return undefined; + } + case 'n': { + // BigInt + return BigInt(value.substring(2)); + } + default: { + // We assume that anything else is a reference ID. + const id = parseInt(value.substring(1), 16); + const chunk = getChunk(response, id); + switch (chunk.status) { + case RESOLVED_MODEL: + initializeModelChunk(chunk); + break; + } + // The status might have changed after initialization. + switch (chunk.status) { + case INITIALIZED: + return chunk.value; + case PENDING: + case BLOCKED: + const parentChunk = initializingChunk; + chunk.then( + createModelResolver(parentChunk, parentObject, key), + createModelReject(parentChunk), + ); + return null; + default: + throw chunk.reason; + } + } + } + } + return value; +} + +export function createResponse(bundlerConfig: ServerManifest): Response { + const chunks: Map> = new Map(); + const response: Response = { + _bundlerConfig: bundlerConfig, + _chunks: chunks, + _fromJSON: function (this: any, key: string, value: JSONValue) { + if (typeof value === 'string') { + // We can't use .bind here because we need the "this" value. + return parseModelString(response, this, key, value); + } + return value; + }, + }; + return response; +} + +export function resolveField( + response: Response, + id: number, + model: string, +): void { + const chunks = response._chunks; + const chunk = chunks.get(id); + if (!chunk) { + chunks.set(id, createResolvedModelChunk(response, model)); + } else { + resolveModelChunk(chunk, model); + } +} + +export function resolveFile(response: Response, id: number, file: File): void { + throw new Error('Not implemented.'); +} + +export opaque type FileHandle = {}; + +export function resolveFileInfo( + response: Response, + id: number, + filename: string, + mime: string, +): FileHandle { + throw new Error('Not implemented.'); +} + +export function resolveFileChunk( + response: Response, + handle: FileHandle, + chunk: Uint8Array, +): void { + throw new Error('Not implemented.'); +} + +export function resolveFileComplete( + response: Response, + handle: FileHandle, +): void { + throw new Error('Not implemented.'); +} + +export function close(response: Response): void { + // In case there are any remaining unresolved chunks, they won't + // be resolved now. So we need to issue an error to those. + // Ideally we should be able to early bail out if we kept a + // ref count of pending chunks. + reportGlobalError(response, new Error('Connection closed.')); +} diff --git a/packages/react-server/src/ReactFlightServer.js b/packages/react-server/src/ReactFlightServer.js index 3a0876a56f5e..1c127c886b7b 100644 --- a/packages/react-server/src/ReactFlightServer.js +++ b/packages/react-server/src/ReactFlightServer.js @@ -10,10 +10,12 @@ import type { Destination, Chunk, - BundlerConfig, - ModuleMetaData, - ModuleReference, - ModuleKey, + ClientManifest, + ClientReferenceMetadata, + ClientReference, + ClientReferenceKey, + ServerReference, + ServerReferenceId, } from './ReactFlightServerConfig'; import type {ContextSnapshot} from './ReactFlightNewContext'; import type {ThenableState} from './ReactFlightThenable'; @@ -25,6 +27,7 @@ import type { PendingThenable, FulfilledThenable, RejectedThenable, + ReactServerContext, } from 'shared/ReactTypes'; import type {LazyComponent} from 'react/src/ReactLazy'; @@ -37,15 +40,16 @@ import { close, closeWithError, processModelChunk, - processModuleChunk, - processProviderChunk, - processSymbolChunk, + processImportChunk, processErrorChunkProd, processErrorChunkDev, processReferenceChunk, - resolveModuleMetaData, - getModuleKey, - isModuleReference, + resolveClientReferenceMetadata, + getServerReferenceId, + getServerReferenceBoundArguments, + getClientReferenceKey, + isClientReference, + isServerReference, supportsRequestStorage, requestStorage, } from './ReactFlightServerConfig'; @@ -71,16 +75,24 @@ import { } from './ReactFlightNewContext'; import { + getIteratorFn, REACT_ELEMENT_TYPE, REACT_FORWARD_REF_TYPE, REACT_FRAGMENT_TYPE, REACT_LAZY_TYPE, REACT_MEMO_TYPE, REACT_PROVIDER_TYPE, - REACT_SUSPENSE_TYPE, - REACT_SUSPENSE_LIST_TYPE, } from 'shared/ReactSymbols'; +import { + describeValueForErrorMessage, + describeObjectForErrorMessage, + isSimpleObject, + jsxPropsParents, + jsxChildrenParents, + objectName, +} from 'shared/ReactSerializationErrors'; + import {getOrCreateServerContext} from 'shared/ReactServerContextRegistry'; import ReactSharedInternals from 'shared/ReactSharedInternals'; import isArray from 'shared/isArray'; @@ -92,20 +104,33 @@ type ReactJSONValue = | number | null | $ReadOnlyArray - | ReactModelObject; - -export type ReactModel = - | React$Element - | LazyComponent + | ReactClientObject; + +// Serializable values +export type ReactClientValue = + // Server Elements and Lazy Components are unwrapped on the Server + | React$Element> + | LazyComponent + // References are passed by their value + | ClientReference + | ServerReference + // The rest are passed as is. Sub-types can be passed in but lose their + // subtype, so the receiver can only accept once of these. + | React$Element + | React$Element & any> + | ReactServerContext | string | boolean | number | symbol | null - | Iterable - | ReactModelObject; + | void + | Iterable + | Array + | ReactClientObject + | Promise; // Thenable -type ReactModelObject = {+[key: string]: ReactModel}; +type ReactClientObject = {+[key: string]: ReactClientValue}; const PENDING = 0; const COMPLETED = 1; @@ -115,7 +140,7 @@ const ERRORED = 4; type Task = { id: number, status: 0 | 1 | 3 | 4, - model: ReactModel, + model: ReactClientValue, ping: () => void, context: ContextSnapshot, thenableState: ThenableState | null, @@ -125,22 +150,23 @@ export type Request = { status: 0 | 1 | 2, fatalError: mixed, destination: null | Destination, - bundlerConfig: BundlerConfig, + bundlerConfig: ClientManifest, cache: Map, nextChunkId: number, pendingChunks: number, abortableTasks: Set, pingedTasks: Array, - completedModuleChunks: Array, + completedImportChunks: Array, completedJSONChunks: Array, completedErrorChunks: Array, writtenSymbols: Map, - writtenModules: Map, + writtenClientReferences: Map, + writtenServerReferences: Map, number>, writtenProviders: Map, identifierPrefix: string, identifierCount: number, onError: (error: mixed) => ?string, - toJSON: (key: string, value: ReactModel) => ReactJSONValue, + toJSON: (key: string, value: ReactClientValue) => ReactJSONValue, }; const ReactCurrentDispatcher = ReactSharedInternals.ReactCurrentDispatcher; @@ -156,8 +182,8 @@ const CLOSING = 1; const CLOSED = 2; export function createRequest( - model: ReactModel, - bundlerConfig: BundlerConfig, + model: ReactClientValue, + bundlerConfig: ClientManifest, onError: void | ((error: mixed) => ?string), context?: Array<[string, ServerContextJSONValue]>, identifierPrefix?: string, @@ -173,8 +199,8 @@ export function createRequest( ReactCurrentCache.current = DefaultCacheDispatcher; const abortSet: Set = new Set(); - const pingedTasks = []; - const request = { + const pingedTasks: Array = []; + const request: Request = { status: OPEN, fatalError: null, destination: null, @@ -184,16 +210,18 @@ export function createRequest( pendingChunks: 0, abortableTasks: abortSet, pingedTasks: pingedTasks, - completedModuleChunks: [], - completedJSONChunks: [], - completedErrorChunks: [], + completedImportChunks: ([]: Array), + completedJSONChunks: ([]: Array), + completedErrorChunks: ([]: Array), writtenSymbols: new Map(), - writtenModules: new Map(), + writtenClientReferences: new Map(), + writtenServerReferences: new Map(), writtenProviders: new Map(), identifierPrefix: identifierPrefix || '', identifierCount: 1, onError: onError === undefined ? defaultErrorHandler : onError, - toJSON: function(key: string, value: ReactModel): ReactJSONValue { + // $FlowFixMe[missing-this-annot] + toJSON: function (key: string, value: ReactClientValue): ReactJSONValue { return resolveModelToJSON(request, this, key, value); }, }; @@ -212,10 +240,85 @@ function createRootContext( const POP = {}; -// Used for DEV messages to keep track of which parent rendered some props, -// in case they error. -const jsxPropsParents: WeakMap = new WeakMap(); -const jsxChildrenParents: WeakMap = new WeakMap(); +function serializeThenable(request: Request, thenable: Thenable): number { + request.pendingChunks++; + const newTask = createTask( + request, + null, + getActiveContext(), + request.abortableTasks, + ); + + switch (thenable.status) { + case 'fulfilled': { + // We have the resolved value, we can go ahead and schedule it for serialization. + newTask.model = thenable.value; + pingTask(request, newTask); + return newTask.id; + } + case 'rejected': { + const x = thenable.reason; + const digest = logRecoverableError(request, x); + if (__DEV__) { + const {message, stack} = getErrorMessageAndStackDev(x); + emitErrorChunkDev(request, newTask.id, digest, message, stack); + } else { + emitErrorChunkProd(request, newTask.id, digest); + } + return newTask.id; + } + default: { + if (typeof thenable.status === 'string') { + // Only instrument the thenable if the status if not defined. If + // it's defined, but an unknown value, assume it's been instrumented by + // some custom userspace implementation. We treat it as "pending". + break; + } + const pendingThenable: PendingThenable = (thenable: any); + pendingThenable.status = 'pending'; + pendingThenable.then( + fulfilledValue => { + if (thenable.status === 'pending') { + const fulfilledThenable: FulfilledThenable = (thenable: any); + fulfilledThenable.status = 'fulfilled'; + fulfilledThenable.value = fulfilledValue; + } + }, + (error: mixed) => { + if (thenable.status === 'pending') { + const rejectedThenable: RejectedThenable = (thenable: any); + rejectedThenable.status = 'rejected'; + rejectedThenable.reason = error; + } + }, + ); + break; + } + } + + thenable.then( + value => { + newTask.model = value; + pingTask(request, newTask); + }, + reason => { + newTask.status = ERRORED; + // TODO: We should ideally do this inside performWork so it's scheduled + const digest = logRecoverableError(request, reason); + if (__DEV__) { + const {message, stack} = getErrorMessageAndStackDev(reason); + emitErrorChunkDev(request, newTask.id, digest, message, stack); + } else { + emitErrorChunkProd(request, newTask.id, digest); + } + if (request.destination !== null) { + flushCompletedChunks(request, request.destination); + } + }, + ); + + return newTask.id; +} function readThenable(thenable: Thenable): T { if (thenable.status === 'fulfilled') { @@ -271,12 +374,13 @@ function createLazyWrapperAroundWakeable(wakeable: Wakeable) { } function attemptResolveElement( + request: Request, type: any, key: null | React$Key, ref: mixed, props: any, prevThenableState: ThenableState | null, -): ReactModel { +): ReactClientValue { if (ref !== null && ref !== undefined) { // When the ref moves to the regular props object this will implicitly // throw for functions. We could probably relax it to a DEV warning for other @@ -292,7 +396,7 @@ function attemptResolveElement( } } if (typeof type === 'function') { - if (isModuleReference(type)) { + if (isClientReference(type)) { // This is a reference to a Client Component. return [REACT_ELEMENT_TYPE, type, key, props]; } @@ -304,6 +408,14 @@ function attemptResolveElement( result !== null && typeof result.then === 'function' ) { + // When the return value is in children position we can resolve it immediately, + // to its value without a wrapper if it's synchronously available. + const thenable: Thenable = result; + if (thenable.status === 'fulfilled') { + return thenable.value; + } + // TODO: Once we accept Promises as children on the client, we can just return + // the thenable here. return createLazyWrapperAroundWakeable(result); } return result; @@ -322,7 +434,7 @@ function attemptResolveElement( // Any built-in works as long as its props are serializable. return [REACT_ELEMENT_TYPE, type, key, props]; } else if (type != null && typeof type === 'object') { - if (isModuleReference(type)) { + if (isClientReference(type)) { // This is a reference to a Client Component. return [REACT_ELEMENT_TYPE, type, key, props]; } @@ -332,6 +444,7 @@ function attemptResolveElement( const init = type._init; const wrappedType = init(payload); return attemptResolveElement( + request, wrappedType, key, ref, @@ -346,6 +459,7 @@ function attemptResolveElement( } case REACT_MEMO_TYPE: { return attemptResolveElement( + request, type.type, key, ref, @@ -394,12 +508,12 @@ function pingTask(request: Request, task: Task): void { function createTask( request: Request, - model: ReactModel, + model: ReactClientValue, context: ContextSnapshot, abortSet: Set, ): Task { const id = request.nextChunkId++; - const task = { + const task: Task = { id, status: PENDING, model, @@ -415,19 +529,46 @@ function serializeByValueID(id: number): string { return '$' + id.toString(16); } -function serializeByRefID(id: number): string { - return '@' + id.toString(16); +function serializeLazyID(id: number): string { + return '$L' + id.toString(16); +} + +function serializePromiseID(id: number): string { + return '$@' + id.toString(16); +} + +function serializeServerReferenceID(id: number): string { + return '$F' + id.toString(16); +} + +function serializeSymbolReference(name: string): string { + return '$S' + name; +} + +function serializeProviderReference(name: string): string { + return '$P' + name; } -function serializeModuleReference( +function serializeUndefined(): string { + return '$undefined'; +} + +function serializeBigInt(n: bigint): string { + return '$n' + n.toString(10); +} + +function serializeClientReference( request: Request, - parent: {+[key: string | number]: ReactModel} | $ReadOnlyArray, + parent: + | {+[key: string | number]: ReactClientValue} + | $ReadOnlyArray, key: string, - moduleReference: ModuleReference, + clientReference: ClientReference, ): string { - const moduleKey: ModuleKey = getModuleKey(moduleReference); - const writtenModules = request.writtenModules; - const existingId = writtenModules.get(moduleKey); + const clientReferenceKey: ClientReferenceKey = + getClientReferenceKey(clientReference); + const writtenClientReferences = request.writtenClientReferences; + const existingId = writtenClientReferences.get(clientReferenceKey); if (existingId !== undefined) { if (parent[0] === REACT_ELEMENT_TYPE && key === '1') { // If we're encoding the "type" of an element, we can refer @@ -435,28 +576,26 @@ function serializeModuleReference( // knows how to deal with lazy values. This lets us suspend // on this component rather than its parent until the code has // loaded. - return serializeByRefID(existingId); + return serializeLazyID(existingId); } return serializeByValueID(existingId); } try { - const moduleMetaData: ModuleMetaData = resolveModuleMetaData( - request.bundlerConfig, - moduleReference, - ); + const clientReferenceMetadata: ClientReferenceMetadata = + resolveClientReferenceMetadata(request.bundlerConfig, clientReference); request.pendingChunks++; - const moduleId = request.nextChunkId++; - emitModuleChunk(request, moduleId, moduleMetaData); - writtenModules.set(moduleKey, moduleId); + const importId = request.nextChunkId++; + emitImportChunk(request, importId, clientReferenceMetadata); + writtenClientReferences.set(clientReferenceKey, importId); if (parent[0] === REACT_ELEMENT_TYPE && key === '1') { // If we're encoding the "type" of an element, we can refer // to that by a lazy reference instead of directly since React // knows how to deal with lazy values. This lets us suspend // on this component rather than its parent until the code has // loaded. - return serializeByRefID(moduleId); + return serializeLazyID(importId); } - return serializeByValueID(moduleId); + return serializeByValueID(importId); } catch (x) { request.pendingChunks++; const errorId = request.nextChunkId++; @@ -471,280 +610,52 @@ function serializeModuleReference( } } -function escapeStringValue(value: string): string { - if (value[0] === '$' || value[0] === '@') { - // We need to escape $ or @ prefixed strings since we use those to encode - // references to IDs and as special symbol values. - return '$' + value; - } else { - return value; - } -} - -function isObjectPrototype(object): boolean { - if (!object) { - return false; - } - const ObjectPrototype = Object.prototype; - if (object === ObjectPrototype) { - return true; - } - // It might be an object from a different Realm which is - // still just a plain simple object. - if (Object.getPrototypeOf(object)) { - return false; - } - const names = Object.getOwnPropertyNames(object); - for (let i = 0; i < names.length; i++) { - if (!(names[i] in ObjectPrototype)) { - return false; - } - } - return true; -} - -function isSimpleObject(object): boolean { - if (!isObjectPrototype(Object.getPrototypeOf(object))) { - return false; - } - const names = Object.getOwnPropertyNames(object); - for (let i = 0; i < names.length; i++) { - const descriptor = Object.getOwnPropertyDescriptor(object, names[i]); - if (!descriptor) { - return false; - } - if (!descriptor.enumerable) { - if ( - (names[i] === 'key' || names[i] === 'ref') && - typeof descriptor.get === 'function' - ) { - // React adds key and ref getters to props objects to issue warnings. - // Those getters will not be transferred to the client, but that's ok, - // so we'll special case them. - continue; - } - return false; - } - } - return true; -} - -function objectName(object): string { - // $FlowFixMe[method-unbinding] - const name = Object.prototype.toString.call(object); - return name.replace(/^\[object (.*)\]$/, function(m, p0) { - return p0; - }); -} - -function describeKeyForErrorMessage(key: string): string { - const encodedKey = JSON.stringify(key); - return '"' + key + '"' === encodedKey ? key : encodedKey; -} - -function describeValueForErrorMessage(value: ReactModel): string { - switch (typeof value) { - case 'string': { - return JSON.stringify( - value.length <= 10 ? value : value.substr(0, 10) + '...', - ); - } - case 'object': { - if (isArray(value)) { - return '[...]'; - } - const name = objectName(value); - if (name === 'Object') { - return '{...}'; - } - return name; - } - case 'function': - return 'function'; - default: - // eslint-disable-next-line react-internal/safe-string-coercion - return String(value); +function serializeServerReference( + request: Request, + parent: + | {+[key: string | number]: ReactClientValue} + | $ReadOnlyArray, + key: string, + serverReference: ServerReference, +): string { + const writtenServerReferences = request.writtenServerReferences; + const existingId = writtenServerReferences.get(serverReference); + if (existingId !== undefined) { + return serializeServerReferenceID(existingId); } -} -function describeElementType(type: any): string { - if (typeof type === 'string') { - return type; - } - switch (type) { - case REACT_SUSPENSE_TYPE: - return 'Suspense'; - case REACT_SUSPENSE_LIST_TYPE: - return 'SuspenseList'; - } - if (typeof type === 'object') { - switch (type.$$typeof) { - case REACT_FORWARD_REF_TYPE: - return describeElementType(type.render); - case REACT_MEMO_TYPE: - return describeElementType(type.type); - case REACT_LAZY_TYPE: { - const lazyComponent: LazyComponent = (type: any); - const payload = lazyComponent._payload; - const init = lazyComponent._init; - try { - // Lazy may contain any component type so we recursively resolve it. - return describeElementType(init(payload)); - } catch (x) {} - } - } - } - return ''; + const bound: null | Array = getServerReferenceBoundArguments( + request.bundlerConfig, + serverReference, + ); + const serverReferenceMetadata: { + id: ServerReferenceId, + bound: null | Promise>, + } = { + id: getServerReferenceId(request.bundlerConfig, serverReference), + bound: bound ? Promise.resolve(bound) : null, + }; + request.pendingChunks++; + const metadataId = request.nextChunkId++; + // We assume that this object doesn't suspend. + const processedChunk = processModelChunk( + request, + metadataId, + serverReferenceMetadata, + ); + request.completedJSONChunks.push(processedChunk); + writtenServerReferences.set(serverReference, metadataId); + return serializeServerReferenceID(metadataId); } -function describeObjectForErrorMessage( - objectOrArray: - | {+[key: string | number]: ReactModel, ...} - | $ReadOnlyArray, - expandedName?: string, -): string { - const objKind = objectName(objectOrArray); - if (objKind !== 'Object' && objKind !== 'Array') { - return objKind; - } - let str = ''; - let start = -1; - let length = 0; - if (isArray(objectOrArray)) { - if (__DEV__ && jsxChildrenParents.has(objectOrArray)) { - // Print JSX Children - const type = jsxChildrenParents.get(objectOrArray); - str = '<' + describeElementType(type) + '>'; - const array: $ReadOnlyArray = objectOrArray; - for (let i = 0; i < array.length; i++) { - const value = array[i]; - let substr; - if (typeof value === 'string') { - substr = value; - } else if (typeof value === 'object' && value !== null) { - // $FlowFixMe[incompatible-call] found when upgrading Flow - substr = '{' + describeObjectForErrorMessage(value) + '}'; - } else { - substr = '{' + describeValueForErrorMessage(value) + '}'; - } - if ('' + i === expandedName) { - start = str.length; - length = substr.length; - str += substr; - } else if (substr.length < 15 && str.length + substr.length < 40) { - str += substr; - } else { - str += '{...}'; - } - } - str += ''; - } else { - // Print Array - str = '['; - const array: $ReadOnlyArray = objectOrArray; - for (let i = 0; i < array.length; i++) { - if (i > 0) { - str += ', '; - } - const value = array[i]; - let substr; - if (typeof value === 'object' && value !== null) { - // $FlowFixMe[incompatible-call] found when upgrading Flow - substr = describeObjectForErrorMessage(value); - } else { - substr = describeValueForErrorMessage(value); - } - if ('' + i === expandedName) { - start = str.length; - length = substr.length; - str += substr; - } else if (substr.length < 10 && str.length + substr.length < 40) { - str += substr; - } else { - str += '...'; - } - } - str += ']'; - } +function escapeStringValue(value: string): string { + if (value[0] === '$') { + // We need to escape $ prefixed strings since we use those to encode + // references to IDs and as special symbol values. + return '$' + value; } else { - if (objectOrArray.$$typeof === REACT_ELEMENT_TYPE) { - str = '<' + describeElementType(objectOrArray.type) + '/>'; - } else if (__DEV__ && jsxPropsParents.has(objectOrArray)) { - // Print JSX - const type = jsxPropsParents.get(objectOrArray); - str = '<' + (describeElementType(type) || '...'); - const object: {+[key: string | number]: ReactModel, ...} = objectOrArray; - const names = Object.keys(object); - for (let i = 0; i < names.length; i++) { - str += ' '; - const name = names[i]; - str += describeKeyForErrorMessage(name) + '='; - const value = object[name]; - let substr; - if ( - name === expandedName && - typeof value === 'object' && - value !== null - ) { - // $FlowFixMe[incompatible-call] found when upgrading Flow - substr = describeObjectForErrorMessage(value); - } else { - substr = describeValueForErrorMessage(value); - } - if (typeof value !== 'string') { - substr = '{' + substr + '}'; - } - if (name === expandedName) { - start = str.length; - length = substr.length; - str += substr; - } else if (substr.length < 10 && str.length + substr.length < 40) { - str += substr; - } else { - str += '...'; - } - } - str += '>'; - } else { - // Print Object - str = '{'; - const object: {+[key: string | number]: ReactModel, ...} = objectOrArray; - const names = Object.keys(object); - for (let i = 0; i < names.length; i++) { - if (i > 0) { - str += ', '; - } - const name = names[i]; - str += describeKeyForErrorMessage(name) + ': '; - const value = object[name]; - let substr; - if (typeof value === 'object' && value !== null) { - // $FlowFixMe[incompatible-call] found when upgrading Flow - substr = describeObjectForErrorMessage(value); - } else { - substr = describeValueForErrorMessage(value); - } - if (name === expandedName) { - start = str.length; - length = substr.length; - str += substr; - } else if (substr.length < 10 && str.length + substr.length < 40) { - str += substr; - } else { - str += '...'; - } - } - str += '}'; - } - } - if (expandedName === undefined) { - return str; - } - if (start > -1 && length > 0) { - const highlight = ' '.repeat(start) + '^'.repeat(length); - return '\n ' + str + '\n ' + highlight; + return value; } - return '\n ' + str; } let insideContextProps = null; @@ -752,12 +663,14 @@ let isInsideContextValue = false; export function resolveModelToJSON( request: Request, - parent: {+[key: string | number]: ReactModel} | $ReadOnlyArray, + parent: + | {+[key: string | number]: ReactClientValue} + | $ReadOnlyArray, key: string, - value: ReactModel, + value: ReactClientValue, ): ReactJSONValue { if (__DEV__) { - // $FlowFixMe + // $FlowFixMe[incompatible-use] const originalValue = parent[key]; if (typeof originalValue === 'object' && originalValue !== value) { if (objectName(originalValue) !== 'Object') { @@ -797,7 +710,7 @@ export function resolveModelToJSON( if ( parent[0] === REACT_ELEMENT_TYPE && parent[1] && - parent[1].$$typeof === REACT_PROVIDER_TYPE && + (parent[1]: any).$$typeof === REACT_PROVIDER_TYPE && key === '3' ) { insideContextProps = value; @@ -828,6 +741,7 @@ export function resolveModelToJSON( const element: React$Element = (value: any); // Attempt to render the Server Component. value = attemptResolveElement( + request, element.type, element.key, element.ref, @@ -866,7 +780,7 @@ export function resolveModelToJSON( const ping = newTask.ping; x.then(ping, ping); newTask.thenableState = getThenableStateAfterSuspending(); - return serializeByRefID(newTask.id); + return serializeLazyID(newTask.id); } else { // Something errored. We'll still send everything we have up until this point. // We'll replace this element with a lazy reference that throws on the client @@ -880,7 +794,7 @@ export function resolveModelToJSON( } else { emitErrorChunkProd(request, errorId, digest); } - return serializeByRefID(errorId); + return serializeLazyID(errorId); } } } @@ -890,8 +804,14 @@ export function resolveModelToJSON( } if (typeof value === 'object') { - if (isModuleReference(value)) { - return serializeModuleReference(request, parent, key, (value: any)); + if (isClientReference(value)) { + return serializeClientReference(request, parent, key, (value: any)); + // $FlowFixMe[method-unbinding] + } else if (typeof value.then === 'function') { + // We assume that any object with a .then property is a "Thenable" type, + // or a Promise type. Either of which can be represented by a Promise. + const promiseId = serializeThenable(request, (value: any)); + return serializePromiseID(promiseId); } else if ((value: any).$$typeof === REACT_PROVIDER_TYPE) { const providerKey = ((value: any): ReactProviderType)._context ._globalName; @@ -912,6 +832,12 @@ export function resolveModelToJSON( } return (undefined: any); } + if (!isArray(value)) { + const iteratorFn = getIteratorFn(value); + if (iteratorFn) { + return Array.from((value: any)); + } + } if (__DEV__) { if (value !== null && !isArray(value)) { @@ -943,7 +869,7 @@ export function resolveModelToJSON( } } - // $FlowFixMe + // $FlowFixMe[incompatible-return] return value; } @@ -951,17 +877,20 @@ export function resolveModelToJSON( return escapeStringValue(value); } - if ( - typeof value === 'boolean' || - typeof value === 'number' || - typeof value === 'undefined' - ) { + if (typeof value === 'boolean' || typeof value === 'number') { return value; } + if (typeof value === 'undefined') { + return serializeUndefined(); + } + if (typeof value === 'function') { - if (isModuleReference(value)) { - return serializeModuleReference(request, parent, key, (value: any)); + if (isClientReference(value)) { + return serializeClientReference(request, parent, key, (value: any)); + } + if (isServerReference(value)) { + return serializeServerReference(request, parent, key, (value: any)); } if (/^on[A-Z]/.test(key)) { throw new Error( @@ -972,7 +901,7 @@ export function resolveModelToJSON( } else { throw new Error( 'Functions cannot be passed directly to Client Components ' + - "because they're not serializable." + + 'unless you explicitly expose it by marking it with "use server".' + describeObjectForErrorMessage(parent, key), ); } @@ -984,14 +913,14 @@ export function resolveModelToJSON( if (existingId !== undefined) { return serializeByValueID(existingId); } - // $FlowFixMe `description` might be undefined + // $FlowFixMe[incompatible-type] `description` might be undefined const name: string = value.description; if (Symbol.for(name) !== value) { throw new Error( 'Only global symbols received from Symbol.for(...) can be passed to Client Components. ' + `The symbol Symbol.for(${ - // $FlowFixMe `description` might be undefined + // $FlowFixMe[incompatible-type] `description` might be undefined value.description }) cannot be found among global symbols.` + describeObjectForErrorMessage(parent, key), @@ -1005,12 +934,8 @@ export function resolveModelToJSON( return serializeByValueID(symbolId); } - // $FlowFixMe: bigint isn't added to Flow yet. if (typeof value === 'bigint') { - throw new Error( - `BigInt (${value}) is not yet supported in Client Component props.` + - describeObjectForErrorMessage(parent, key), - ); + return serializeBigInt(value); } throw new Error( @@ -1031,9 +956,10 @@ function logRecoverableError(request: Request, error: mixed): string { return errorDigest || ''; } -function getErrorMessageAndStackDev( - error: mixed, -): {message: string, stack: string} { +function getErrorMessageAndStackDev(error: mixed): { + message: string, + stack: string, +} { if (__DEV__) { let message; let stack = ''; @@ -1099,18 +1025,23 @@ function emitErrorChunkDev( request.completedErrorChunks.push(processedChunk); } -function emitModuleChunk( +function emitImportChunk( request: Request, id: number, - moduleMetaData: ModuleMetaData, + clientReferenceMetadata: ClientReferenceMetadata, ): void { - const processedChunk = processModuleChunk(request, id, moduleMetaData); - request.completedModuleChunks.push(processedChunk); + const processedChunk = processImportChunk( + request, + id, + clientReferenceMetadata, + ); + request.completedImportChunks.push(processedChunk); } function emitSymbolChunk(request: Request, id: number, name: string): void { - const processedChunk = processSymbolChunk(request, id, name); - request.completedModuleChunks.push(processedChunk); + const symbolReference = serializeSymbolReference(name); + const processedChunk = processReferenceChunk(request, id, symbolReference); + request.completedImportChunks.push(processedChunk); } function emitProviderChunk( @@ -1118,7 +1049,8 @@ function emitProviderChunk( id: number, contextName: string, ): void { - const processedChunk = processProviderChunk(request, id, contextName); + const contextReference = serializeProviderReference(contextName); + const processedChunk = processReferenceChunk(request, id, contextReference); request.completedJSONChunks.push(processedChunk); } @@ -1148,6 +1080,7 @@ function retryTask(request: Request, task: Task): void { // also suspends. task.model = value; value = attemptResolveElement( + request, element.type, element.key, element.ref, @@ -1171,6 +1104,7 @@ function retryTask(request: Request, task: Task): void { const nextElement: React$Element = (value: any); task.model = value; value = attemptResolveElement( + request, nextElement.type, nextElement.key, nextElement.ref, @@ -1259,11 +1193,11 @@ function flushCompletedChunks( try { // We emit module chunks first in the stream so that // they can be preloaded as early as possible. - const moduleChunks = request.completedModuleChunks; + const importsChunks = request.completedImportChunks; let i = 0; - for (; i < moduleChunks.length; i++) { + for (; i < importsChunks.length; i++) { request.pendingChunks--; - const chunk = moduleChunks[i]; + const chunk = importsChunks[i]; const keepWriting: boolean = writeChunkAndReturn(destination, chunk); if (!keepWriting) { request.destination = null; @@ -1271,7 +1205,7 @@ function flushCompletedChunks( break; } } - moduleChunks.splice(0, i); + importsChunks.splice(0, i); // Next comes model data. const jsonChunks = request.completedJSONChunks; i = 0; diff --git a/packages/react-server/src/ReactFlightServerBundlerConfigCustom.js b/packages/react-server/src/ReactFlightServerBundlerConfigCustom.js index fa0815cca2f2..b8254bb51d3c 100644 --- a/packages/react-server/src/ReactFlightServerBundlerConfigCustom.js +++ b/packages/react-server/src/ReactFlightServerBundlerConfigCustom.js @@ -9,10 +9,17 @@ declare var $$$hostConfig: any; -export opaque type BundlerConfig = mixed; // eslint-disable-line no-undef -export opaque type ModuleReference = mixed; // eslint-disable-line no-undef -export opaque type ModuleMetaData: any = mixed; // eslint-disable-line no-undef -export opaque type ModuleKey: any = mixed; // eslint-disable-line no-undef -export const isModuleReference = $$$hostConfig.isModuleReference; -export const getModuleKey = $$$hostConfig.getModuleKey; -export const resolveModuleMetaData = $$$hostConfig.resolveModuleMetaData; +export opaque type ClientManifest = mixed; +export opaque type ClientReference = mixed; // eslint-disable-line no-unused-vars +export opaque type ServerReference = mixed; // eslint-disable-line no-unused-vars +export opaque type ClientReferenceMetadata: any = mixed; +export opaque type ServerReferenceId: any = mixed; +export opaque type ClientReferenceKey: any = mixed; +export const isClientReference = $$$hostConfig.isClientReference; +export const isServerReference = $$$hostConfig.isServerReference; +export const getClientReferenceKey = $$$hostConfig.getClientReferenceKey; +export const resolveClientReferenceMetadata = + $$$hostConfig.resolveClientReferenceMetadata; +export const getServerReferenceId = $$$hostConfig.getServerReferenceId; +export const getServerReferenceBoundArguments = + $$$hostConfig.getServerReferenceBoundArguments; diff --git a/packages/react-server/src/ReactFlightServerConfigStream.js b/packages/react-server/src/ReactFlightServerConfigStream.js index d5e4ec6f6371..4377a313b374 100644 --- a/packages/react-server/src/ReactFlightServerConfigStream.js +++ b/packages/react-server/src/ReactFlightServerConfigStream.js @@ -64,7 +64,10 @@ ByteSize // TODO: Implement HTMLData, BlobData and URLData. -import type {Request, ReactModel} from 'react-server/src/ReactFlightServer'; +import type { + Request, + ReactClientValue, +} from 'react-server/src/ReactFlightServer'; import {stringToChunk} from './ReactServerStreamConfig'; @@ -80,7 +83,7 @@ export { const stringify = JSON.stringify; function serializeRowHeader(tag: string, id: number) { - return tag + id.toString(16) + ':'; + return id.toString(16) + ':' + tag; } export function processErrorChunkProd( @@ -124,10 +127,11 @@ export function processErrorChunkDev( export function processModelChunk( request: Request, id: number, - model: ReactModel, + model: ReactClientValue, ): Chunk { + // $FlowFixMe[incompatible-type] stringify can return null const json: string = stringify(model, request.toJSON); - const row = serializeRowHeader('J', id) + json + '\n'; + const row = id.toString(16) + ':' + json + '\n'; return stringToChunk(row); } @@ -137,36 +141,18 @@ export function processReferenceChunk( reference: string, ): Chunk { const json = stringify(reference); - const row = serializeRowHeader('J', id) + json + '\n'; + const row = id.toString(16) + ':' + json + '\n'; return stringToChunk(row); } -export function processModuleChunk( +export function processImportChunk( request: Request, id: number, - moduleMetaData: ReactModel, + clientReferenceMetadata: ReactClientValue, ): Chunk { - const json: string = stringify(moduleMetaData); - const row = serializeRowHeader('M', id) + json + '\n'; - return stringToChunk(row); -} - -export function processProviderChunk( - request: Request, - id: number, - contextName: string, -): Chunk { - const row = serializeRowHeader('P', id) + contextName + '\n'; - return stringToChunk(row); -} - -export function processSymbolChunk( - request: Request, - id: number, - name: string, -): Chunk { - const json = stringify(name); - const row = serializeRowHeader('S', id) + json + '\n'; + // $FlowFixMe[incompatible-type] stringify can return null + const json: string = stringify(clientReferenceMetadata); + const row = serializeRowHeader('I', id) + json + '\n'; return stringToChunk(row); } diff --git a/packages/react-server/src/ReactServerStreamConfigBrowser.js b/packages/react-server/src/ReactServerStreamConfigBrowser.js index 2d9346efe54d..aa9cac7b2c37 100644 --- a/packages/react-server/src/ReactServerStreamConfigBrowser.js +++ b/packages/react-server/src/ReactServerStreamConfigBrowser.js @@ -21,12 +21,9 @@ export function flushBuffered(destination: Destination) { // transform streams. https://github.com/whatwg/streams/issues/960 } -// For now we support AsyncLocalStorage as a global for the "browser" builds -// TODO: Move this to some special WinterCG build. -export const supportsRequestStorage = typeof AsyncLocalStorage === 'function'; -export const requestStorage: AsyncLocalStorage< - Map, -> = supportsRequestStorage ? new AsyncLocalStorage() : (null: any); +export const supportsRequestStorage = false; +export const requestStorage: AsyncLocalStorage> = + (null: any); const VIEW_SIZE = 512; let currentView = null; @@ -149,7 +146,7 @@ export function clonePrecomputedChunk( export function closeWithError(destination: Destination, error: mixed): void { // $FlowFixMe[method-unbinding] if (typeof destination.error === 'function') { - // $FlowFixMe: This is an Error object or the destination accepts other types. + // $FlowFixMe[incompatible-call]: This is an Error object or the destination accepts other types. destination.error(error); } else { // Earlier implementations doesn't support this method. In that environment you're diff --git a/packages/react-server/src/ReactServerStreamConfigBun.js b/packages/react-server/src/ReactServerStreamConfigBun.js index fd90c17a3d1e..9cc88c408647 100644 --- a/packages/react-server/src/ReactServerStreamConfigBun.js +++ b/packages/react-server/src/ReactServerStreamConfigBun.js @@ -71,9 +71,8 @@ export function clonePrecomputedChunk( } export function closeWithError(destination: Destination, error: mixed): void { - // $FlowFixMe[method-unbinding] if (typeof destination.error === 'function') { - // $FlowFixMe: This is an Error object or the destination accepts other types. + // $FlowFixMe[incompatible-call]: This is an Error object or the destination accepts other types. destination.error(error); } else { // Earlier implementations doesn't support this method. In that environment you're diff --git a/packages/react-server/src/ReactServerStreamConfigEdge.js b/packages/react-server/src/ReactServerStreamConfigEdge.js new file mode 100644 index 000000000000..db6bfb14fee8 --- /dev/null +++ b/packages/react-server/src/ReactServerStreamConfigEdge.js @@ -0,0 +1,161 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +export type Destination = ReadableStreamController; + +export type PrecomputedChunk = Uint8Array; +export opaque type Chunk = Uint8Array; + +export function scheduleWork(callback: () => void) { + setTimeout(callback, 0); +} + +export function flushBuffered(destination: Destination) { + // WHATWG Streams do not yet have a way to flush the underlying + // transform streams. https://github.com/whatwg/streams/issues/960 +} + +// For now, we get this from the global scope, but this will likely move to a module. +export const supportsRequestStorage = typeof AsyncLocalStorage === 'function'; +export const requestStorage: AsyncLocalStorage> = + supportsRequestStorage ? new AsyncLocalStorage() : (null: any); + +const VIEW_SIZE = 512; +let currentView = null; +let writtenBytes = 0; + +export function beginWriting(destination: Destination) { + currentView = new Uint8Array(VIEW_SIZE); + writtenBytes = 0; +} + +export function writeChunk( + destination: Destination, + chunk: PrecomputedChunk | Chunk, +): void { + if (chunk.length === 0) { + return; + } + + if (chunk.length > VIEW_SIZE) { + if (__DEV__) { + if (precomputedChunkSet.has(chunk)) { + console.error( + 'A large precomputed chunk was passed to writeChunk without being copied.' + + ' Large chunks get enqueued directly and are not copied. This is incompatible with precomputed chunks because you cannot enqueue the same precomputed chunk twice.' + + ' Use "cloneChunk" to make a copy of this large precomputed chunk before writing it. This is a bug in React.', + ); + } + } + // this chunk may overflow a single view which implies it was not + // one that is cached by the streaming renderer. We will enqueu + // it directly and expect it is not re-used + if (writtenBytes > 0) { + destination.enqueue( + new Uint8Array( + ((currentView: any): Uint8Array).buffer, + 0, + writtenBytes, + ), + ); + currentView = new Uint8Array(VIEW_SIZE); + writtenBytes = 0; + } + destination.enqueue(chunk); + return; + } + + let bytesToWrite = chunk; + const allowableBytes = ((currentView: any): Uint8Array).length - writtenBytes; + if (allowableBytes < bytesToWrite.length) { + // this chunk would overflow the current view. We enqueue a full view + // and start a new view with the remaining chunk + if (allowableBytes === 0) { + // the current view is already full, send it + destination.enqueue(currentView); + } else { + // fill up the current view and apply the remaining chunk bytes + // to a new view. + ((currentView: any): Uint8Array).set( + bytesToWrite.subarray(0, allowableBytes), + writtenBytes, + ); + // writtenBytes += allowableBytes; // this can be skipped because we are going to immediately reset the view + destination.enqueue(currentView); + bytesToWrite = bytesToWrite.subarray(allowableBytes); + } + currentView = new Uint8Array(VIEW_SIZE); + writtenBytes = 0; + } + ((currentView: any): Uint8Array).set(bytesToWrite, writtenBytes); + writtenBytes += bytesToWrite.length; +} + +export function writeChunkAndReturn( + destination: Destination, + chunk: PrecomputedChunk | Chunk, +): boolean { + writeChunk(destination, chunk); + // in web streams there is no backpressure so we can alwas write more + return true; +} + +export function completeWriting(destination: Destination) { + if (currentView && writtenBytes > 0) { + destination.enqueue(new Uint8Array(currentView.buffer, 0, writtenBytes)); + currentView = null; + writtenBytes = 0; + } +} + +export function close(destination: Destination) { + destination.close(); +} + +const textEncoder = new TextEncoder(); + +export function stringToChunk(content: string): Chunk { + return textEncoder.encode(content); +} + +const precomputedChunkSet: Set = __DEV__ ? new Set() : (null: any); + +export function stringToPrecomputedChunk(content: string): PrecomputedChunk { + const precomputedChunk = textEncoder.encode(content); + + if (__DEV__) { + precomputedChunkSet.add(precomputedChunk); + } + + return precomputedChunk; +} + +export function clonePrecomputedChunk( + precomputedChunk: PrecomputedChunk, +): PrecomputedChunk { + return precomputedChunk.length > VIEW_SIZE + ? precomputedChunk.slice() + : precomputedChunk; +} + +export function closeWithError(destination: Destination, error: mixed): void { + // $FlowFixMe[method-unbinding] + if (typeof destination.error === 'function') { + // $FlowFixMe[incompatible-call]: This is an Error object or the destination accepts other types. + destination.error(error); + } else { + // Earlier implementations doesn't support this method. In that environment you're + // supposed to throw from a promise returned but we don't return a promise in our + // approach. We could fork this implementation but this is environment is an edge + // case to begin with. It's even less common to run this in an older environment. + // Even then, this is not where errors are supposed to happen and they get reported + // to a global callback in addition to this anyway. So it's fine just to close this. + destination.close(); + } +} diff --git a/packages/react-server/src/ReactServerStreamConfigNode.js b/packages/react-server/src/ReactServerStreamConfigNode.js index 458c84e076d8..5d33ce7d6576 100644 --- a/packages/react-server/src/ReactServerStreamConfigNode.js +++ b/packages/react-server/src/ReactServerStreamConfigNode.js @@ -35,9 +35,8 @@ export function flushBuffered(destination: Destination) { } export const supportsRequestStorage = true; -export const requestStorage: AsyncLocalStorage< - Map, -> = new AsyncLocalStorage(); +export const requestStorage: AsyncLocalStorage> = + new AsyncLocalStorage(); const VIEW_SIZE = 2048; let currentView = null; @@ -76,11 +75,15 @@ function writeStringChunk(destination: Destination, stringChunk: string) { writtenBytes += written; if (read < stringChunk.length) { - writeToDestination(destination, (currentView: any)); + writeToDestination( + destination, + (currentView: any).subarray(0, writtenBytes), + ); currentView = new Uint8Array(VIEW_SIZE); - // $FlowFixMe[incompatible-call] found when upgrading Flow - writtenBytes = textEncoder.encodeInto(stringChunk.slice(read), currentView) - .written; + writtenBytes = textEncoder.encodeInto( + stringChunk.slice(read), + (currentView: any), + ).written; } if (writtenBytes === VIEW_SIZE) { @@ -194,7 +197,7 @@ export function stringToChunk(content: string): Chunk { return content; } -const precomputedChunkSet = __DEV__ ? new Set() : null; +const precomputedChunkSet = __DEV__ ? new Set() : null; export function stringToPrecomputedChunk(content: string): PrecomputedChunk { const precomputedChunk = textEncoder.encode(content); @@ -217,6 +220,6 @@ export function clonePrecomputedChunk( } export function closeWithError(destination: Destination, error: mixed): void { - // $FlowFixMe: This is an Error object or the destination accepts other types. + // $FlowFixMe[incompatible-call]: This is an Error object or the destination accepts other types. destination.destroy(error); } diff --git a/packages/react-server/src/forks/ReactFlightServerConfig.dom-bun.js b/packages/react-server/src/forks/ReactFlightServerConfig.dom-bun.js new file mode 100644 index 000000000000..f85cfb5f78b2 --- /dev/null +++ b/packages/react-server/src/forks/ReactFlightServerConfig.dom-bun.js @@ -0,0 +1,11 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +export * from '../ReactFlightServerConfigStream'; +export * from '../ReactFlightServerBundlerConfigCustom'; diff --git a/packages/react-server/src/forks/ReactFlightServerConfig.bun.js b/packages/react-server/src/forks/ReactFlightServerConfig.dom-edge-webpack.js similarity index 100% rename from packages/react-server/src/forks/ReactFlightServerConfig.bun.js rename to packages/react-server/src/forks/ReactFlightServerConfig.dom-edge-webpack.js diff --git a/packages/react-server/src/forks/ReactFlightServerConfig.dom.js b/packages/react-server/src/forks/ReactFlightServerConfig.dom-node-webpack.js similarity index 100% rename from packages/react-server/src/forks/ReactFlightServerConfig.dom.js rename to packages/react-server/src/forks/ReactFlightServerConfig.dom-node-webpack.js diff --git a/packages/react-server/src/forks/ReactFlightServerConfig.dom-node.js b/packages/react-server/src/forks/ReactFlightServerConfig.dom-node.js new file mode 100644 index 000000000000..99c541a937d6 --- /dev/null +++ b/packages/react-server/src/forks/ReactFlightServerConfig.dom-node.js @@ -0,0 +1,11 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +export * from '../ReactFlightServerConfigStream'; +export * from 'react-server-dom-webpack/src/ReactFlightServerWebpackBundlerConfig'; diff --git a/packages/react-server/src/forks/ReactServerFormatConfig.custom.js b/packages/react-server/src/forks/ReactServerFormatConfig.custom.js index 26455b9ee493..33f6c5aa65dd 100644 --- a/packages/react-server/src/forks/ReactServerFormatConfig.custom.js +++ b/packages/react-server/src/forks/ReactServerFormatConfig.custom.js @@ -74,11 +74,13 @@ export const cleanupAfterRender = $$$hostConfig.cleanupAfterRender; // ------------------------- // Resources // ------------------------- -export const writeInitialResources = $$$hostConfig.writeInitialResources; -export const writeImmediateResources = $$$hostConfig.writeImmediateResources; +export const writePreamble = $$$hostConfig.writePreamble; +export const writeHoistables = $$$hostConfig.writeHoistables; +export const writePostamble = $$$hostConfig.writePostamble; export const hoistResources = $$$hostConfig.hoistResources; -export const hoistResourcesToRoot = $$$hostConfig.hoistResourcesToRoot; export const createResources = $$$hostConfig.createResources; export const createBoundaryResources = $$$hostConfig.createBoundaryResources; export const setCurrentlyRenderingBoundaryResourcesTarget = $$$hostConfig.setCurrentlyRenderingBoundaryResourcesTarget; +export const writeResourcesForBoundary = + $$$hostConfig.writeResourcesForBoundary; diff --git a/packages/react-server/src/forks/ReactServerFormatConfig.bun.js b/packages/react-server/src/forks/ReactServerFormatConfig.dom-bun.js similarity index 100% rename from packages/react-server/src/forks/ReactServerFormatConfig.bun.js rename to packages/react-server/src/forks/ReactServerFormatConfig.dom-bun.js diff --git a/packages/react-server/src/forks/ReactServerFormatConfig.dom.js b/packages/react-server/src/forks/ReactServerFormatConfig.dom-edge-webpack.js similarity index 100% rename from packages/react-server/src/forks/ReactServerFormatConfig.dom.js rename to packages/react-server/src/forks/ReactServerFormatConfig.dom-edge-webpack.js diff --git a/packages/react-server/src/forks/ReactServerFormatConfig.dom-node-webpack.js b/packages/react-server/src/forks/ReactServerFormatConfig.dom-node-webpack.js new file mode 100644 index 000000000000..485793a6893e --- /dev/null +++ b/packages/react-server/src/forks/ReactServerFormatConfig.dom-node-webpack.js @@ -0,0 +1,10 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +export * from 'react-dom-bindings/src/server/ReactDOMServerFormatConfig'; diff --git a/packages/react-server/src/forks/ReactServerFormatConfig.dom-node.js b/packages/react-server/src/forks/ReactServerFormatConfig.dom-node.js new file mode 100644 index 000000000000..485793a6893e --- /dev/null +++ b/packages/react-server/src/forks/ReactServerFormatConfig.dom-node.js @@ -0,0 +1,10 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +export * from 'react-dom-bindings/src/server/ReactDOMServerFormatConfig'; diff --git a/packages/react-server/src/forks/ReactServerStreamConfig.bun.js b/packages/react-server/src/forks/ReactServerStreamConfig.dom-bun.js similarity index 100% rename from packages/react-server/src/forks/ReactServerStreamConfig.bun.js rename to packages/react-server/src/forks/ReactServerStreamConfig.dom-bun.js diff --git a/packages/react-server/src/forks/ReactServerStreamConfig.dom-edge-webpack.js b/packages/react-server/src/forks/ReactServerStreamConfig.dom-edge-webpack.js new file mode 100644 index 000000000000..a594d19afc74 --- /dev/null +++ b/packages/react-server/src/forks/ReactServerStreamConfig.dom-edge-webpack.js @@ -0,0 +1,10 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +export * from '../ReactServerStreamConfigEdge'; diff --git a/packages/react-server/src/forks/ReactServerStreamConfig.dom.js b/packages/react-server/src/forks/ReactServerStreamConfig.dom-node-webpack.js similarity index 100% rename from packages/react-server/src/forks/ReactServerStreamConfig.dom.js rename to packages/react-server/src/forks/ReactServerStreamConfig.dom-node-webpack.js diff --git a/packages/react-server/src/forks/ReactServerStreamConfig.dom-node.js b/packages/react-server/src/forks/ReactServerStreamConfig.dom-node.js new file mode 100644 index 000000000000..1a3871aef2e8 --- /dev/null +++ b/packages/react-server/src/forks/ReactServerStreamConfig.dom-node.js @@ -0,0 +1,10 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +export * from '../ReactServerStreamConfigNode'; diff --git a/packages/react-test-renderer/src/ReactTestHostConfig.js b/packages/react-test-renderer/src/ReactTestHostConfig.js index a3230a541aa3..e306f9e1a0b0 100644 --- a/packages/react-test-renderer/src/ReactTestHostConfig.js +++ b/packages/react-test-renderer/src/ReactTestHostConfig.js @@ -51,7 +51,7 @@ export * from 'react-reconciler/src/ReactFiberHostConfigWithNoSingletons'; const NO_CONTEXT = {}; const UPDATE_SIGNAL = {}; -const nodeToInstanceMap = new WeakMap(); +const nodeToInstanceMap = new WeakMap(); if (__DEV__) { Object.freeze(NO_CONTEXT); @@ -324,6 +324,23 @@ export function requestPostPaintCallback(callback: (time: number) => void) { // noop } +export function maySuspendCommit(type: Type, props: Props): boolean { + return false; +} + +export function preloadInstance(type: Type, props: Props): boolean { + // Return true to indicate it's already loaded + return true; +} + +export function startSuspendingCommit(): void {} + +export function suspendInstance(type: Type, props: Props): void {} + +export function waitForCommitToBeReady(): null { + return null; +} + export function prepareRendererToRender(container: Container): void { // noop } diff --git a/packages/react-test-renderer/src/ReactTestRenderer.js b/packages/react-test-renderer/src/ReactTestRenderer.js index 464504797711..2080c287a86f 100644 --- a/packages/react-test-renderer/src/ReactTestRenderer.js +++ b/packages/react-test-renderer/src/ReactTestRenderer.js @@ -30,7 +30,7 @@ import { FunctionComponent, ClassComponent, HostComponent, - HostResource, + HostHoistable, HostSingleton, HostPortal, HostText, @@ -84,7 +84,7 @@ type FindOptions = $Shape<{ export type Predicate = (node: ReactTestInstance) => ?boolean; const defaultTestOptions = { - createNodeMock: function() { + createNodeMock: function () { return null; }, }; @@ -133,7 +133,7 @@ function toJSON(inst: Instance | TextInstance): ReactTestRendererNode | null { } } -function childrenToTree(node) { +function childrenToTree(node: null | Fiber) { if (!node) { return null; } @@ -146,6 +146,7 @@ function childrenToTree(node) { return flatten(children.map(toTree)); } +// $FlowFixMe[missing-local-annot] function nodeAndSiblingsArray(nodeWithSibling) { const array = []; let node = nodeWithSibling; @@ -156,6 +157,7 @@ function nodeAndSiblingsArray(nodeWithSibling) { return array; } +// $FlowFixMe[missing-local-annot] function flatten(arr) { const result = []; const stack = [{i: 0, array: arr}]; @@ -175,7 +177,7 @@ function flatten(arr) { return result; } -function toTree(node: ?Fiber) { +function toTree(node: null | Fiber): $FlowFixMe { if (node == null) { return null; } @@ -201,7 +203,7 @@ function toTree(node: ?Fiber) { instance: null, rendered: childrenToTree(node.child), }; - case HostResource: + case HostHoistable: case HostSingleton: case HostComponent: { return { @@ -312,7 +314,7 @@ class ReactTestInstance { const tag = this._fiber.tag; if ( tag === HostComponent || - tag === HostResource || + tag === HostHoistable || tag === HostSingleton ) { return getPublicInstance(this._fiber.stateNode); @@ -449,6 +451,7 @@ function propsMatch(props: Object, filter: Object): boolean { return true; } +// $FlowFixMe[missing-local-annot] function onRecoverableError(error) { // TODO: Expose onRecoverableError option to userspace // eslint-disable-next-line react-internal/no-production-logging, react-internal/warning-args @@ -491,7 +494,7 @@ function create( } } let container = { - children: [], + children: ([]: Array), createNodeMock, tag: 'CONTAINER', }; @@ -588,7 +591,7 @@ function create( ({ configurable: true, enumerable: true, - get: function() { + get: function () { if (root === null) { throw new Error("Can't access .root on unmounted test renderer"); } @@ -611,7 +614,7 @@ function create( return entry; } -const fiberToWrapper = new WeakMap(); +const fiberToWrapper = new WeakMap(); function wrapFiber(fiber: Fiber): ReactTestInstance { let wrapper = fiberToWrapper.get(fiber); if (wrapper === undefined && fiber.alternate !== null) { diff --git a/packages/react-test-renderer/src/__tests__/ReactTestRenderer-test.internal.js b/packages/react-test-renderer/src/__tests__/ReactTestRenderer-test.internal.js index a2c458f6aafe..c8095b823962 100644 --- a/packages/react-test-renderer/src/__tests__/ReactTestRenderer-test.internal.js +++ b/packages/react-test-renderer/src/__tests__/ReactTestRenderer-test.internal.js @@ -14,12 +14,14 @@ const ReactFeatureFlags = require('shared/ReactFeatureFlags'); ReactFeatureFlags.replayFailedUnitOfWorkWithInvokeGuardedCallback = false; const React = require('react'); const ReactTestRenderer = require('react-test-renderer'); -const prettyFormat = require('pretty-format'); +const {format: prettyFormat} = require('pretty-format'); // Isolate noop renderer jest.resetModules(); const ReactNoop = require('react-noop-renderer'); -const Scheduler = require('scheduler'); + +const InternalTestUtils = require('internal-test-utils'); +const waitForAll = InternalTestUtils.waitForAll; // Kind of hacky, but we nullify all the instances to test the tree structure // with jasmine's deep equality function, and test the instances separate. We @@ -1015,7 +1017,7 @@ describe('ReactTestRenderer', () => { ); }); - it('can concurrently render context with a "primary" renderer', () => { + it('can concurrently render context with a "primary" renderer', async () => { const Context = React.createContext(null); const Indirection = React.Fragment; const App = () => ( @@ -1026,7 +1028,7 @@ describe('ReactTestRenderer', () => { ); ReactNoop.render(); - expect(Scheduler).toFlushWithoutYielding(); + await waitForAll([]); ReactTestRenderer.create(); }); diff --git a/packages/react-test-renderer/src/__tests__/ReactTestRenderer-test.js b/packages/react-test-renderer/src/__tests__/ReactTestRenderer-test.js index a55b046fee98..0118fa53f3ad 100644 --- a/packages/react-test-renderer/src/__tests__/ReactTestRenderer-test.js +++ b/packages/react-test-renderer/src/__tests__/ReactTestRenderer-test.js @@ -13,7 +13,7 @@ let ReactDOM; let React; let ReactCache; let ReactTestRenderer; -let Scheduler; +let waitForAll; describe('ReactTestRenderer', () => { beforeEach(() => { @@ -25,15 +25,30 @@ describe('ReactTestRenderer', () => { React = require('react'); ReactCache = require('react-cache'); ReactTestRenderer = require('react-test-renderer'); - Scheduler = require('scheduler'); + const InternalTestUtils = require('internal-test-utils'); + waitForAll = InternalTestUtils.waitForAll; }); it('should warn if used to render a ReactDOM portal', () => { const container = document.createElement('div'); expect(() => { - expect(() => { + let error; + try { ReactTestRenderer.create(ReactDOM.createPortal('foo', container)); - }).toThrow(); + } catch (e) { + error = e; + } + // After the update throws, a subsequent render is scheduled to + // unmount the whole tree. This update also causes an error, so React + // throws an AggregateError. + const errors = error.errors; + expect(errors.length).toBe(2); + expect(errors[0].message.includes('indexOf is not a function')).toBe( + true, + ); + expect(errors[1].message.includes('indexOf is not a function')).toBe( + true, + ); }).toErrorDev('An invalid container has been provided.', { withoutStack: true, }); @@ -71,16 +86,14 @@ describe('ReactTestRenderer', () => { const root = ReactTestRenderer.create(); PendingResources.initial('initial'); - await Promise.resolve(); - Scheduler.unstable_flushAll(); + await waitForAll([]); expect(root.toJSON()).toEqual('initial'); root.update(); expect(root.toJSON()).toEqual('fallback'); PendingResources.dynamic('dynamic'); - await Promise.resolve(); - Scheduler.unstable_flushAll(); + await waitForAll([]); expect(root.toJSON()).toEqual('dynamic'); }); @@ -97,16 +110,14 @@ describe('ReactTestRenderer', () => { const root = ReactTestRenderer.create(); PendingResources.initial('initial'); - await Promise.resolve(); - Scheduler.unstable_flushAll(); + await waitForAll([]); expect(root.toJSON().children).toEqual(['initial']); root.update(); expect(root.toJSON().children).toEqual(['fallback']); PendingResources.dynamic('dynamic'); - await Promise.resolve(); - Scheduler.unstable_flushAll(); + await waitForAll([]); expect(root.toJSON().children).toEqual(['dynamic']); }); }); diff --git a/packages/react-test-renderer/src/__tests__/ReactTestRendererAct-test.js b/packages/react-test-renderer/src/__tests__/ReactTestRendererAct-test.js index 4fe1afea6d62..2306a895b785 100644 --- a/packages/react-test-renderer/src/__tests__/ReactTestRendererAct-test.js +++ b/packages/react-test-renderer/src/__tests__/ReactTestRendererAct-test.js @@ -4,6 +4,7 @@ let React; let ReactTestRenderer; let Scheduler; let act; +let assertLog; describe('ReactTestRenderer.act()', () => { beforeEach(() => { @@ -12,6 +13,9 @@ describe('ReactTestRenderer.act()', () => { ReactTestRenderer = require('react-test-renderer'); Scheduler = require('scheduler'); act = ReactTestRenderer.act; + + const InternalTestUtils = require('internal-test-utils'); + assertLog = InternalTestUtils.assertLog; }); // @gate __DEV__ @@ -79,9 +83,9 @@ describe('ReactTestRenderer.act()', () => { // This component will keep updating itself until step === 3 const [step, proceed] = useReducer(s => (s === 3 ? 3 : s + 1), 1); useEffect(() => { - Scheduler.unstable_yieldValue('Effect'); + Scheduler.log('Effect'); alreadyResolvedPromise.then(() => { - Scheduler.unstable_yieldValue('Microtask'); + Scheduler.log('Microtask'); proceed(); }); }); @@ -91,7 +95,7 @@ describe('ReactTestRenderer.act()', () => { await act(async () => { root.update(); }); - expect(Scheduler).toHaveYielded([ + assertLog([ // Should not flush effects without also flushing microtasks // First render: 'Effect', diff --git a/packages/react-test-renderer/src/__tests__/ReactTestRendererAsync-test.js b/packages/react-test-renderer/src/__tests__/ReactTestRendererAsync-test.js index 03379563c7e5..468f62d39a72 100644 --- a/packages/react-test-renderer/src/__tests__/ReactTestRendererAsync-test.js +++ b/packages/react-test-renderer/src/__tests__/ReactTestRendererAsync-test.js @@ -13,6 +13,8 @@ let React; let ReactTestRenderer; let Scheduler; +let waitForAll; +let waitFor; describe('ReactTestRendererAsync', () => { beforeEach(() => { @@ -21,9 +23,13 @@ describe('ReactTestRendererAsync', () => { React = require('react'); ReactTestRenderer = require('react-test-renderer'); Scheduler = require('scheduler'); + + const InternalTestUtils = require('internal-test-utils'); + waitForAll = InternalTestUtils.waitForAll; + waitFor = InternalTestUtils.waitFor; }); - it('flushAll flushes all work', () => { + it('flushAll flushes all work', async () => { function Foo(props) { return props.children; } @@ -35,7 +41,7 @@ describe('ReactTestRendererAsync', () => { expect(renderer.toJSON()).toEqual(null); // Flush initial mount. - expect(Scheduler).toFlushWithoutYielding(); + await waitForAll([]); expect(renderer.toJSON()).toEqual('Hi'); // Update @@ -43,13 +49,13 @@ describe('ReactTestRendererAsync', () => { // Not yet updated. expect(renderer.toJSON()).toEqual('Hi'); // Flush update. - expect(Scheduler).toFlushWithoutYielding(); + await waitForAll([]); expect(renderer.toJSON()).toEqual('Bye'); }); - it('flushAll returns array of yielded values', () => { + it('flushAll returns array of yielded values', async () => { function Child(props) { - Scheduler.unstable_yieldValue(props.children); + Scheduler.log(props.children); return props.children; } function Parent(props) { @@ -65,17 +71,17 @@ describe('ReactTestRendererAsync', () => { unstable_isConcurrent: true, }); - expect(Scheduler).toFlushAndYield(['A:1', 'B:1', 'C:1']); + await waitForAll(['A:1', 'B:1', 'C:1']); expect(renderer.toJSON()).toEqual(['A:1', 'B:1', 'C:1']); renderer.update(); - expect(Scheduler).toFlushAndYield(['A:2', 'B:2', 'C:2']); + await waitForAll(['A:2', 'B:2', 'C:2']); expect(renderer.toJSON()).toEqual(['A:2', 'B:2', 'C:2']); }); - it('flushThrough flushes until the expected values is yielded', () => { + it('flushThrough flushes until the expected values is yielded', async () => { function Child(props) { - Scheduler.unstable_yieldValue(props.children); + Scheduler.log(props.children); return props.children; } function Parent(props) { @@ -89,31 +95,25 @@ describe('ReactTestRendererAsync', () => { } let renderer; - if (gate(flags => flags.enableSyncDefaultUpdates)) { - React.startTransition(() => { - renderer = ReactTestRenderer.create(, { - unstable_isConcurrent: true, - }); - }); - } else { + React.startTransition(() => { renderer = ReactTestRenderer.create(, { unstable_isConcurrent: true, }); - } + }); // Flush the first two siblings - expect(Scheduler).toFlushAndYieldThrough(['A:1', 'B:1']); + await waitFor(['A:1', 'B:1']); // Did not commit yet. expect(renderer.toJSON()).toEqual(null); // Flush the remaining work - expect(Scheduler).toFlushAndYield(['C:1']); + await waitForAll(['C:1']); expect(renderer.toJSON()).toEqual(['A:1', 'B:1', 'C:1']); }); - it('supports high priority interruptions', () => { + it('supports high priority interruptions', async () => { function Child(props) { - Scheduler.unstable_yieldValue(props.children); + Scheduler.log(props.children); return props.children; } @@ -135,20 +135,14 @@ describe('ReactTestRendererAsync', () => { } let renderer; - if (gate(flags => flags.enableSyncDefaultUpdates)) { - React.startTransition(() => { - renderer = ReactTestRenderer.create(, { - unstable_isConcurrent: true, - }); - }); - } else { + React.startTransition(() => { renderer = ReactTestRenderer.create(, { unstable_isConcurrent: true, }); - } + }); // Flush the some of the changes, but don't commit - expect(Scheduler).toFlushAndYieldThrough(['A:1']); + await waitFor(['A:1']); expect(renderer.toJSON()).toEqual(null); // Interrupt with higher priority properties @@ -159,132 +153,4 @@ describe('ReactTestRendererAsync', () => { // Only the higher priority properties have been committed expect(renderer.toJSON()).toEqual(['A:2', 'B:2']); }); - - describe('Jest matchers', () => { - it('toFlushAndYieldThrough', () => { - const Yield = ({id}) => { - Scheduler.unstable_yieldValue(id); - return id; - }; - - ReactTestRenderer.create( -
    - - - -
    , - { - unstable_isConcurrent: true, - }, - ); - - expect(() => - expect(Scheduler).toFlushAndYieldThrough(['foo', 'baz']), - ).toThrow('// deep equality'); - }); - - it('toFlushAndYield', () => { - const Yield = ({id}) => { - Scheduler.unstable_yieldValue(id); - return id; - }; - - const renderer = ReactTestRenderer.create( -
    - - - -
    , - { - unstable_isConcurrent: true, - }, - ); - - expect(() => expect(Scheduler).toFlushWithoutYielding()).toThrowError( - '// deep equality', - ); - - renderer.update( -
    - - - -
    , - ); - - expect(() => expect(Scheduler).toFlushAndYield(['foo', 'baz'])).toThrow( - '// deep equality', - ); - }); - - it('toFlushAndThrow', () => { - const Yield = ({id}) => { - Scheduler.unstable_yieldValue(id); - return id; - }; - - function BadRender() { - throw new Error('Oh no!'); - } - - function App() { - return ( -
    - - - - - -
    - ); - } - - const renderer = ReactTestRenderer.create(, { - unstable_isConcurrent: true, - }); - - expect(Scheduler).toFlushAndThrow('Oh no!'); - expect(Scheduler).toHaveYielded(['A', 'B', 'C', 'D', 'A', 'B', 'C', 'D']); - - renderer.update(); - - expect(Scheduler).toFlushAndThrow('Oh no!'); - expect(Scheduler).toHaveYielded(['A', 'B', 'C', 'D', 'A', 'B', 'C', 'D']); - - renderer.update(); - expect(Scheduler).toFlushAndThrow('Oh no!'); - }); - }); - - it('toHaveYielded', () => { - const Yield = ({id}) => { - Scheduler.unstable_yieldValue(id); - return id; - }; - - function App() { - return ( -
    - - - -
    - ); - } - - ReactTestRenderer.create(); - expect(() => expect(Scheduler).toHaveYielded(['A', 'B'])).toThrow( - '// deep equality', - ); - }); - - it('flush methods throw if log is not empty', () => { - ReactTestRenderer.create(
    , { - unstable_isConcurrent: true, - }); - Scheduler.unstable_yieldValue('Something'); - expect(() => expect(Scheduler).toFlushWithoutYielding()).toThrow( - 'Log of yielded values is not empty.', - ); - }); }); diff --git a/packages/react/README.md b/packages/react/README.md index a4aa151da728..20a855efd3c0 100644 --- a/packages/react/README.md +++ b/packages/react/README.md @@ -30,8 +30,8 @@ root.render(); ## Documentation -See https://reactjs.org/ +See https://react.dev/ ## API -See https://reactjs.org/docs/react-api.html +See https://react.dev/reference/react diff --git a/packages/react/index.classic.fb.js b/packages/react/index.classic.fb.js index 04b905efa759..48caeb5e1389 100644 --- a/packages/react/index.classic.fb.js +++ b/packages/react/index.classic.fb.js @@ -52,7 +52,7 @@ export { useDeferredValue, useDeferredValue as unstable_useDeferredValue, // TODO: Remove once call sights updated to useDeferredValue useEffect, - experimental_useEvent, + experimental_useEffectEvent, useImperativeHandle, useLayoutEffect, useInsertionEffect, diff --git a/packages/react/index.experimental.js b/packages/react/index.experimental.js index a5b16fcadfc7..876aa3a76175 100644 --- a/packages/react/index.experimental.js +++ b/packages/react/index.experimental.js @@ -44,7 +44,7 @@ export { useDebugValue, useDeferredValue, useEffect, - experimental_useEvent, + experimental_useEffectEvent, useImperativeHandle, useInsertionEffect, useLayoutEffect, diff --git a/packages/react/index.js b/packages/react/index.js index bee9b71e48fe..2ed91ec25b7e 100644 --- a/packages/react/index.js +++ b/packages/react/index.js @@ -8,9 +8,8 @@ */ // Keep in sync with https://github.com/facebook/flow/blob/main/lib/react.js -export type StatelessFunctionalComponent< - P, -> = React$StatelessFunctionalComponent

    ; +export type StatelessFunctionalComponent

    = + React$StatelessFunctionalComponent

    ; export type ComponentType<-P> = React$ComponentType

    ; export type AbstractComponent< -Config, @@ -72,7 +71,7 @@ export { useDebugValue, useDeferredValue, useEffect, - experimental_useEvent, + experimental_useEffectEvent, useImperativeHandle, useInsertionEffect, useLayoutEffect, diff --git a/packages/react/index.modern.fb.js b/packages/react/index.modern.fb.js index 2c446d7bd7f9..68adc9a22464 100644 --- a/packages/react/index.modern.fb.js +++ b/packages/react/index.modern.fb.js @@ -50,7 +50,7 @@ export { useDeferredValue, useDeferredValue as unstable_useDeferredValue, // TODO: Remove once call sights updated to useDeferredValue useEffect, - experimental_useEvent, + experimental_useEffectEvent, useImperativeHandle, useInsertionEffect, useLayoutEffect, diff --git a/packages/react/src/React.js b/packages/react/src/React.js index 399c62f0dc58..38d40ece0503 100644 --- a/packages/react/src/React.js +++ b/packages/react/src/React.js @@ -42,7 +42,7 @@ import { useCallback, useContext, useEffect, - useEvent, + useEffectEvent, useImperativeHandle, useDebugValue, useInsertionEffect, @@ -105,7 +105,7 @@ export { useCallback, useContext, useEffect, - useEvent as experimental_useEvent, + useEffectEvent as experimental_useEffectEvent, useImperativeHandle, useDebugValue, useInsertionEffect, diff --git a/packages/react/src/ReactAct.js b/packages/react/src/ReactAct.js index e6a1bd8e3449..e28ecef8ad7d 100644 --- a/packages/react/src/ReactAct.js +++ b/packages/react/src/ReactAct.js @@ -106,7 +106,7 @@ export function act(callback: () => T | Thenable): Thenable { }); return { - then(resolve, reject) { + then(resolve: T => mixed, reject: mixed => mixed) { didAwaitActCall = true; thenable.then( returnValue => { @@ -184,7 +184,7 @@ export function act(callback: () => T | Thenable): Thenable { ReactCurrentActQueue.current = null; } return { - then(resolve, reject) { + then(resolve: T => mixed, reject: mixed => mixed) { didAwaitActCall = true; if (prevActScopeDepth === 0) { // If the `act` call is awaited, restore the queue we were @@ -205,7 +205,10 @@ export function act(callback: () => T | Thenable): Thenable { } } -function popActScope(prevActQueue, prevActScopeDepth) { +function popActScope( + prevActQueue: null | Array, + prevActScopeDepth: number, +) { if (__DEV__) { if (prevActScopeDepth !== actScopeDepth - 1) { console.error( @@ -252,7 +255,7 @@ function recursivelyFlushAsyncActWork( } let isFlushing = false; -function flushActQueue(queue) { +function flushActQueue(queue: Array) { if (__DEV__) { if (!isFlushing) { // Prevent re-entrance. @@ -304,7 +307,7 @@ function flushActQueue(queue) { // environment it may cause the warning to fire too late. const queueSeveralMicrotasks = typeof queueMicrotask === 'function' - ? callback => { + ? (callback: () => void) => { queueMicrotask(() => queueMicrotask(callback)); } : queueMacrotask; diff --git a/packages/react/src/ReactBaseClasses.js b/packages/react/src/ReactBaseClasses.js index e491ed5c4ca9..12a362797a3d 100644 --- a/packages/react/src/ReactBaseClasses.js +++ b/packages/react/src/ReactBaseClasses.js @@ -53,7 +53,7 @@ Component.prototype.isReactComponent = {}; * @final * @protected */ -Component.prototype.setState = function(partialState, callback) { +Component.prototype.setState = function (partialState, callback) { if ( typeof partialState !== 'object' && typeof partialState !== 'function' && @@ -82,7 +82,7 @@ Component.prototype.setState = function(partialState, callback) { * @final * @protected */ -Component.prototype.forceUpdate = function(callback) { +Component.prototype.forceUpdate = function (callback) { this.updater.enqueueForceUpdate(this, callback, 'forceUpdate'); }; @@ -104,9 +104,9 @@ if (__DEV__) { 'https://github.com/facebook/react/issues/3236).', ], }; - const defineDeprecationWarning = function(methodName, info) { + const defineDeprecationWarning = function (methodName, info) { Object.defineProperty(Component.prototype, methodName, { - get: function() { + get: function () { console.warn( '%s(...) is deprecated in plain JavaScript React classes. %s', info[0], diff --git a/packages/react/src/ReactCache.js b/packages/react/src/ReactCache.js index b308ad9e16d2..2aba19344695 100644 --- a/packages/react/src/ReactCache.js +++ b/packages/react/src/ReactCache.js @@ -53,14 +53,16 @@ function createCacheNode(): CacheNode { } export function cache, T>(fn: (...A) => T): (...A) => T { - return function() { + return function () { const dispatcher = ReactCurrentCache.current; if (!dispatcher) { // If there is no dispatcher, then we treat this as not being cached. - // $FlowFixMe: We don't want to use rest arguments since we transpile the code. + // $FlowFixMe[incompatible-call]: We don't want to use rest arguments since we transpile the code. return fn.apply(null, arguments); } - const fnMap = dispatcher.getCacheForType(createCacheRoot); + const fnMap: WeakMap> = dispatcher.getCacheForType( + createCacheRoot, + ); const fnNode = fnMap.get(fn); let cacheNode: CacheNode; if (fnNode === undefined) { @@ -109,7 +111,7 @@ export function cache, T>(fn: (...A) => T): (...A) => T { throw cacheNode.v; } try { - // $FlowFixMe: We don't want to use rest arguments since we transpile the code. + // $FlowFixMe[incompatible-call]: We don't want to use rest arguments since we transpile the code. const result = fn.apply(null, arguments); const terminatedNode: TerminatedCacheNode = (cacheNode: any); terminatedNode.s = TERMINATED; diff --git a/packages/react/src/ReactChildren.js b/packages/react/src/ReactChildren.js index 65c97c62cd7e..6ad83d60f12e 100644 --- a/packages/react/src/ReactChildren.js +++ b/packages/react/src/ReactChildren.js @@ -34,7 +34,7 @@ function escape(key: string): string { '=': '=0', ':': '=2', }; - const escapedString = key.replace(escapeRegex, function(match) { + const escapedString = key.replace(escapeRegex, function (match) { return escaperLookup[match]; }); @@ -125,7 +125,7 @@ function mapIntoArray( if (__DEV__) { // The `if` statement here prevents auto-disabling of the safe // coercion ESLint rule, so we must manually disable it below. - // $FlowFixMe Flow incorrectly thinks React.Portal doesn't have a key + // $FlowFixMe[incompatible-type] Flow incorrectly thinks React.Portal doesn't have a key if (mappedChild.key && (!child || child.key !== mappedChild.key)) { checkKeyStringCoercion(mappedChild.key); } @@ -135,13 +135,11 @@ function mapIntoArray( // Keep both the (mapped) and old keys if they differ, just as // traverseAllChildren used to do for objects as children escapedPrefix + - // $FlowFixMe Flow incorrectly thinks React.Portal doesn't have a key + // $FlowFixMe[incompatible-type] Flow incorrectly thinks React.Portal doesn't have a key (mappedChild.key && (!child || child.key !== mappedChild.key) ? escapeUserProvidedKey( - // eslint-disable-next-line react-internal/safe-string-coercion - '' + - // $FlowFixMe Flow incorrectly thinks existing element's key can be a number - mappedChild.key, + // $FlowFixMe[unsafe-addition] + '' + mappedChild.key, // eslint-disable-line react-internal/safe-string-coercion ) + '/' : '') + childKey, @@ -193,7 +191,7 @@ function mapIntoArray( const iterator = iteratorFn.call(iterableChildren); let step; let ii = 0; - // $FlowFixMe `iteratorFn` might return null according to typing. + // $FlowFixMe[incompatible-use] `iteratorFn` might return null according to typing. while (!(step = iterator.next()).done) { child = step.value; nextName = nextNamePrefix + getElementKey(child, ii++); @@ -249,9 +247,9 @@ function mapChildren( if (children == null) { return children; } - const result = []; + const result: Array = []; let count = 0; - mapIntoArray(children, result, '', '', function(child) { + mapIntoArray(children, result, '', '', function (child) { return func.call(context, child, count++); }); return result; @@ -296,7 +294,8 @@ function forEachChildren( ): void { mapChildren( children, - function() { + // $FlowFixMe[missing-this-annot] + function () { forEachFunc.apply(this, arguments); // Don't return anything. }, diff --git a/packages/react/src/ReactContext.js b/packages/react/src/ReactContext.js index 59bf7e11c766..d3a05fa94bdb 100644 --- a/packages/react/src/ReactContext.js +++ b/packages/react/src/ReactContext.js @@ -9,6 +9,7 @@ import {REACT_PROVIDER_TYPE, REACT_CONTEXT_TYPE} from 'shared/ReactSymbols'; +import type {ReactProviderType} from 'shared/ReactTypes'; import type {ReactContext} from 'shared/ReactTypes'; export function createContext(defaultValue: T): ReactContext { @@ -53,7 +54,7 @@ export function createContext(defaultValue: T): ReactContext { $$typeof: REACT_CONTEXT_TYPE, _context: context, }; - // $FlowFixMe: Flow complains about not setting a value, which is intentional here + // $FlowFixMe[prop-missing]: Flow complains about not setting a value, which is intentional here Object.defineProperties(Consumer, { Provider: { get() { @@ -66,7 +67,7 @@ export function createContext(defaultValue: T): ReactContext { } return context.Provider; }, - set(_Provider) { + set(_Provider: ReactProviderType) { context.Provider = _Provider; }, }, @@ -74,7 +75,7 @@ export function createContext(defaultValue: T): ReactContext { get() { return context._currentValue; }, - set(_currentValue) { + set(_currentValue: T) { context._currentValue = _currentValue; }, }, @@ -82,7 +83,7 @@ export function createContext(defaultValue: T): ReactContext { get() { return context._currentValue2; }, - set(_currentValue2) { + set(_currentValue2: T) { context._currentValue2 = _currentValue2; }, }, @@ -90,7 +91,7 @@ export function createContext(defaultValue: T): ReactContext { get() { return context._threadCount; }, - set(_threadCount) { + set(_threadCount: number) { context._threadCount = _threadCount; }, }, @@ -110,7 +111,7 @@ export function createContext(defaultValue: T): ReactContext { get() { return context.displayName; }, - set(displayName) { + set(displayName: void | string) { if (!hasWarnedAboutDisplayNameOnConsumer) { console.warn( 'Setting `displayName` on Context.Consumer has no effect. ' + @@ -122,7 +123,7 @@ export function createContext(defaultValue: T): ReactContext { }, }, }); - // $FlowFixMe: Flow complains about missing properties because it doesn't understand defineProperty + // $FlowFixMe[prop-missing]: Flow complains about missing properties because it doesn't understand defineProperty context.Consumer = Consumer; } else { context.Consumer = context; diff --git a/packages/react/src/ReactDebugCurrentFrame.js b/packages/react/src/ReactDebugCurrentFrame.js index a29ec8c93e29..c9b35594a34d 100644 --- a/packages/react/src/ReactDebugCurrentFrame.js +++ b/packages/react/src/ReactDebugCurrentFrame.js @@ -11,9 +11,7 @@ const ReactDebugCurrentFrame: { setExtraStackFrame?: (stack: null | string) => void, getCurrentStack?: null | (() => string), getStackAddendum?: () => string, -} = - // $FlowFixMe[incompatible-exact] - {}; +} = {}; let currentExtraStackFrame = (null: null | string); @@ -24,7 +22,7 @@ export function setExtraStackFrame(stack: null | string): void { } if (__DEV__) { - ReactDebugCurrentFrame.setExtraStackFrame = function(stack: null | string) { + ReactDebugCurrentFrame.setExtraStackFrame = function (stack: null | string) { if (__DEV__) { currentExtraStackFrame = stack; } @@ -32,7 +30,7 @@ if (__DEV__) { // Stack implementation injected by the current renderer. ReactDebugCurrentFrame.getCurrentStack = (null: null | (() => string)); - ReactDebugCurrentFrame.getStackAddendum = function(): string { + ReactDebugCurrentFrame.getStackAddendum = function (): string { let stack = ''; // Add an extra top frame while an element is being validated diff --git a/packages/react/src/ReactElement.js b/packages/react/src/ReactElement.js index bd15c85fdfd4..e9d721b92693 100644 --- a/packages/react/src/ReactElement.js +++ b/packages/react/src/ReactElement.js @@ -53,7 +53,7 @@ function hasValidKey(config) { } function defineKeyPropWarningGetter(props, displayName) { - const warnAboutAccessingKey = function() { + const warnAboutAccessingKey = function () { if (__DEV__) { if (!specialPropKeyWarningShown) { specialPropKeyWarningShown = true; @@ -75,7 +75,7 @@ function defineKeyPropWarningGetter(props, displayName) { } function defineRefPropWarningGetter(props, displayName) { - const warnAboutAccessingRef = function() { + const warnAboutAccessingRef = function () { if (__DEV__) { if (!specialPropRefWarningShown) { specialPropRefWarningShown = true; @@ -145,7 +145,7 @@ function warnIfStringRefCannotBeAutoConverted(config) { * indicating filename, line number, and/or other information. * @internal */ -const ReactElement = function(type, key, ref, self, source, owner, props) { +function ReactElement(type, key, ref, self, source, owner, props) { const element = { // This tag allows us to uniquely identify this as a React Element $$typeof: REACT_ELEMENT_TYPE, @@ -199,7 +199,7 @@ const ReactElement = function(type, key, ref, self, source, owner, props) { } return element; -}; +} /** * https://github.com/reactjs/rfcs/pull/107 diff --git a/packages/react/src/ReactElementValidator.js b/packages/react/src/ReactElementValidator.js index b67f52fe9f78..8281057750e9 100644 --- a/packages/react/src/ReactElementValidator.js +++ b/packages/react/src/ReactElementValidator.js @@ -6,7 +6,7 @@ */ /** - * ReactElementValidator provides a wrapper around a element factory + * ReactElementValidator provides a wrapper around an element factory * which validates the props passed to the element. This is intended to be * used only in DEV and could be replaced by a static type checker for languages * that support it. @@ -21,7 +21,6 @@ import { REACT_FRAGMENT_TYPE, REACT_ELEMENT_TYPE, } from 'shared/ReactSymbols'; -import {warnAboutSpreadingKeyToJSX} from 'shared/ReactFeatureFlags'; import checkPropTypes from 'shared/checkPropTypes'; import isArray from 'shared/isArray'; @@ -36,6 +35,8 @@ import {setExtraStackFrame} from './ReactDebugCurrentFrame'; import {describeUnknownElementTypeFrameInDEV} from 'shared/ReactComponentStackFrame'; import hasOwnProperty from 'shared/hasOwnProperty'; +const REACT_CLIENT_REFERENCE = Symbol.for('react.client.reference'); + function setCurrentlyValidatingElement(element) { if (__DEV__) { if (element) { @@ -166,10 +167,12 @@ function validateExplicitKey(element, parentType) { * @param {*} parentType node's parent's type. */ function validateChildKeys(node, parentType) { - if (typeof node !== 'object') { + if (typeof node !== 'object' || !node) { return; } - if (isArray(node)) { + if (node.$$typeof === REACT_CLIENT_REFERENCE) { + // This is a reference to a client component so it's unknown. + } else if (isArray(node)) { for (let i = 0; i < node.length; i++) { const child = node[i]; if (isValidElement(child)) { @@ -181,7 +184,7 @@ function validateChildKeys(node, parentType) { if (node._store) { node._store.validated = true; } - } else if (node) { + } else { const iteratorFn = getIteratorFn(node); if (typeof iteratorFn === 'function') { // Entry iterators used to provide implicit keys, @@ -211,6 +214,9 @@ function validatePropTypes(element) { if (type === null || type === undefined || typeof type === 'string') { return; } + if (type.$$typeof === REACT_CLIENT_REFERENCE) { + return; + } let propTypes; if (typeof type === 'function') { propTypes = type.propTypes; @@ -365,13 +371,11 @@ export function jsxWithValidation( Object.freeze(children); } } else { - if (__DEV__) { - console.error( - 'React.jsx: Static children should always be an array. ' + - 'You are likely explicitly calling React.jsxs or React.jsxDEV. ' + - 'Use the Babel transform instead.', - ); - } + console.error( + 'React.jsx: Static children should always be an array. ' + + 'You are likely explicitly calling React.jsxs or React.jsxDEV. ' + + 'Use the Babel transform instead.', + ); } } else { validateChildKeys(children, type); @@ -379,31 +383,29 @@ export function jsxWithValidation( } } - if (warnAboutSpreadingKeyToJSX) { - if (hasOwnProperty.call(props, 'key')) { - const componentName = getComponentNameFromType(type); - const keys = Object.keys(props).filter(k => k !== 'key'); - const beforeExample = - keys.length > 0 - ? '{key: someKey, ' + keys.join(': ..., ') + ': ...}' - : '{key: someKey}'; - if (!didWarnAboutKeySpread[componentName + beforeExample]) { - const afterExample = - keys.length > 0 ? '{' + keys.join(': ..., ') + ': ...}' : '{}'; - console.error( - 'A props object containing a "key" prop is being spread into JSX:\n' + - ' let props = %s;\n' + - ' <%s {...props} />\n' + - 'React keys must be passed directly to JSX without using spread:\n' + - ' let props = %s;\n' + - ' <%s key={someKey} {...props} />', - beforeExample, - componentName, - afterExample, - componentName, - ); - didWarnAboutKeySpread[componentName + beforeExample] = true; - } + if (hasOwnProperty.call(props, 'key')) { + const componentName = getComponentNameFromType(type); + const keys = Object.keys(props).filter(k => k !== 'key'); + const beforeExample = + keys.length > 0 + ? '{key: someKey, ' + keys.join(': ..., ') + ': ...}' + : '{key: someKey}'; + if (!didWarnAboutKeySpread[componentName + beforeExample]) { + const afterExample = + keys.length > 0 ? '{' + keys.join(': ..., ') + ': ...}' : '{}'; + console.error( + 'A props object containing a "key" prop is being spread into JSX:\n' + + ' let props = %s;\n' + + ' <%s {...props} />\n' + + 'React keys must be passed directly to JSX without using spread:\n' + + ' let props = %s;\n' + + ' <%s key={someKey} {...props} />', + beforeExample, + componentName, + afterExample, + componentName, + ); + didWarnAboutKeySpread[componentName + beforeExample] = true; } } @@ -523,7 +525,7 @@ export function createFactoryWithValidation(type) { // Legacy hook: remove it Object.defineProperty(validatedFactory, 'type', { enumerable: false, - get: function() { + get: function () { console.warn( 'Factory.type is deprecated. Access the class directly ' + 'before passing it to createFactory.', diff --git a/packages/react/src/ReactFetch.js b/packages/react/src/ReactFetch.js index befdcef4de85..7ceea672acd4 100644 --- a/packages/react/src/ReactFetch.js +++ b/packages/react/src/ReactFetch.js @@ -77,7 +77,7 @@ if (enableCache && enableFetchInstrumentation) { const request = new Request(resource, options); if ( (request.method !== 'GET' && request.method !== 'HEAD') || - // $FlowFixMe: keepalive is real + // $FlowFixMe[prop-missing]: keepalive is real request.keepalive ) { // We currently don't dedupe requests that might have side-effects. Those diff --git a/packages/react/src/ReactForwardRef.js b/packages/react/src/ReactForwardRef.js index bfd8fc9afd3a..4581fa9ca10a 100644 --- a/packages/react/src/ReactForwardRef.js +++ b/packages/react/src/ReactForwardRef.js @@ -3,6 +3,8 @@ * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. + * + * @noflow */ import {REACT_FORWARD_REF_TYPE, REACT_MEMO_TYPE} from 'shared/ReactSymbols'; @@ -52,10 +54,10 @@ export function forwardRef( Object.defineProperty(elementType, 'displayName', { enumerable: false, configurable: true, - get: function() { + get: function () { return ownName; }, - set: function(name) { + set: function (name) { ownName = name; // The inner component shouldn't inherit this display name in most cases, diff --git a/packages/react/src/ReactHooks.js b/packages/react/src/ReactHooks.js index a98fdb9c26cb..6d157cbc6028 100644 --- a/packages/react/src/ReactHooks.js +++ b/packages/react/src/ReactHooks.js @@ -57,7 +57,6 @@ export function getCacheSignal(): AbortSignal { 'This CacheSignal was requested outside React which means that it is ' + 'immediately aborted.', ); - // $FlowFixMe Flow doesn't yet know about this argument. controller.abort(reason); return controller.signal; } @@ -219,24 +218,26 @@ export function useSyncExternalStore( export function useCacheRefresh(): (?() => T, ?T) => void { const dispatcher = resolveDispatcher(); - // $FlowFixMe This is unstable, thus optional + // $FlowFixMe[not-a-function] This is unstable, thus optional return dispatcher.useCacheRefresh(); } export function use(usable: Usable): T { const dispatcher = resolveDispatcher(); - // $FlowFixMe This is unstable, thus optional + // $FlowFixMe[not-a-function] This is unstable, thus optional return dispatcher.use(usable); } export function useMemoCache(size: number): Array { const dispatcher = resolveDispatcher(); - // $FlowFixMe This is unstable, thus optional + // $FlowFixMe[not-a-function] This is unstable, thus optional return dispatcher.useMemoCache(size); } -export function useEvent(callback: T): void { +export function useEffectEvent) => mixed>( + callback: F, +): F { const dispatcher = resolveDispatcher(); - // $FlowFixMe This is unstable, thus optional - return dispatcher.useEvent(callback); + // $FlowFixMe[not-a-function] This is unstable, thus optional + return dispatcher.useEffectEvent(callback); } diff --git a/packages/react/src/ReactLazy.js b/packages/react/src/ReactLazy.js index 6537ef601e71..1debecda5557 100644 --- a/packages/react/src/ReactLazy.js +++ b/packages/react/src/ReactLazy.js @@ -137,13 +137,14 @@ export function lazy( // In production, this would just set it on the object. let defaultProps; let propTypes; - // $FlowFixMe + // $FlowFixMe[prop-missing] Object.defineProperties(lazyType, { defaultProps: { configurable: true, get() { return defaultProps; }, + // $FlowFixMe[missing-local-annot] set(newDefaultProps) { console.error( 'React.lazy(...): It is not supported to assign `defaultProps` to ' + @@ -152,7 +153,7 @@ export function lazy( ); defaultProps = newDefaultProps; // Match production behavior more closely: - // $FlowFixMe + // $FlowFixMe[prop-missing] Object.defineProperty(lazyType, 'defaultProps', { enumerable: true, }); @@ -163,6 +164,7 @@ export function lazy( get() { return propTypes; }, + // $FlowFixMe[missing-local-annot] set(newPropTypes) { console.error( 'React.lazy(...): It is not supported to assign `propTypes` to ' + @@ -171,7 +173,7 @@ export function lazy( ); propTypes = newPropTypes; // Match production behavior more closely: - // $FlowFixMe + // $FlowFixMe[prop-missing] Object.defineProperty(lazyType, 'propTypes', { enumerable: true, }); diff --git a/packages/react/src/ReactMemo.js b/packages/react/src/ReactMemo.js index 3f797de451d1..91f4864999d3 100644 --- a/packages/react/src/ReactMemo.js +++ b/packages/react/src/ReactMemo.js @@ -3,6 +3,8 @@ * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. + * + * @noflow */ import {REACT_MEMO_TYPE} from 'shared/ReactSymbols'; @@ -32,10 +34,10 @@ export function memo( Object.defineProperty(elementType, 'displayName', { enumerable: false, configurable: true, - get: function() { + get: function () { return ownName; }, - set: function(name) { + set: function (name) { ownName = name; // The inner component shouldn't inherit this display name in most cases, diff --git a/packages/react/src/ReactNoopUpdateQueue.js b/packages/react/src/ReactNoopUpdateQueue.js index de38664443f2..3e4e8bbeec93 100644 --- a/packages/react/src/ReactNoopUpdateQueue.js +++ b/packages/react/src/ReactNoopUpdateQueue.js @@ -40,7 +40,7 @@ const ReactNoopUpdateQueue = { * @protected * @final */ - isMounted: function(publicInstance) { + isMounted: function (publicInstance) { return false; }, @@ -59,7 +59,7 @@ const ReactNoopUpdateQueue = { * @param {?string} callerName name of the calling function in the public API. * @internal */ - enqueueForceUpdate: function(publicInstance, callback, callerName) { + enqueueForceUpdate: function (publicInstance, callback, callerName) { warnNoop(publicInstance, 'forceUpdate'); }, @@ -76,7 +76,7 @@ const ReactNoopUpdateQueue = { * @param {?string} callerName name of the calling function in the public API. * @internal */ - enqueueReplaceState: function( + enqueueReplaceState: function ( publicInstance, completeState, callback, @@ -97,7 +97,7 @@ const ReactNoopUpdateQueue = { * @param {?string} Name of the calling function in the public API. * @internal */ - enqueueSetState: function( + enqueueSetState: function ( publicInstance, partialState, callback, diff --git a/packages/react/src/ReactStartTransition.js b/packages/react/src/ReactStartTransition.js index ef180ddaef42..490caf5d319e 100644 --- a/packages/react/src/ReactStartTransition.js +++ b/packages/react/src/ReactStartTransition.js @@ -6,6 +6,7 @@ * * @flow */ +import type {BatchConfigTransition} from 'react-reconciler/src/ReactFiberTracingMarkerComponent'; import type {StartTransitionOptions} from 'shared/ReactTypes'; import ReactCurrentBatchConfig from './ReactCurrentBatchConfig'; @@ -16,7 +17,7 @@ export function startTransition( options?: StartTransitionOptions, ) { const prevTransition = ReactCurrentBatchConfig.transition; - ReactCurrentBatchConfig.transition = {}; + ReactCurrentBatchConfig.transition = ({}: BatchConfigTransition); const currentTransition = ReactCurrentBatchConfig.transition; if (__DEV__) { @@ -40,6 +41,7 @@ export function startTransition( if (__DEV__) { if (prevTransition === null && currentTransition._updatedFibers) { const updatedFibersCount = currentTransition._updatedFibers.size; + currentTransition._updatedFibers.clear(); if (updatedFibersCount > 10) { console.warn( 'Detected a large number of updates inside startTransition. ' + @@ -47,7 +49,6 @@ export function startTransition( 'Otherwise concurrent mode guarantees are off the table.', ); } - currentTransition._updatedFibers.clear(); } } } diff --git a/packages/react/src/__tests__/ReactChildren-test.js b/packages/react/src/__tests__/ReactChildren-test.js index 9dd52526da9c..32c17c64d567 100644 --- a/packages/react/src/__tests__/ReactChildren-test.js +++ b/packages/react/src/__tests__/ReactChildren-test.js @@ -21,7 +21,7 @@ describe('ReactChildren', () => { it('should support identity for simple', () => { const context = {}; - const callback = jest.fn().mockImplementation(function(kid, index) { + const callback = jest.fn().mockImplementation(function (kid, index) { expect(this).toBe(context); return kid; }); @@ -46,7 +46,7 @@ describe('ReactChildren', () => { it('should support Portal components', () => { const context = {}; - const callback = jest.fn().mockImplementation(function(kid, index) { + const callback = jest.fn().mockImplementation(function (kid, index) { expect(this).toBe(context); return kid; }); @@ -71,7 +71,7 @@ describe('ReactChildren', () => { it('should treat single arrayless child as being in array', () => { const context = {}; - const callback = jest.fn().mockImplementation(function(kid, index) { + const callback = jest.fn().mockImplementation(function (kid, index) { expect(this).toBe(context); return kid; }); @@ -92,7 +92,7 @@ describe('ReactChildren', () => { it('should treat single child in array as expected', () => { const context = {}; - const callback = jest.fn().mockImplementation(function(kid, index) { + const callback = jest.fn().mockImplementation(function (kid, index) { expect(this).toBe(context); return kid; }); @@ -119,7 +119,7 @@ describe('ReactChildren', () => { const four =

    ; const context = {}; - const callback = jest.fn().mockImplementation(function(kid) { + const callback = jest.fn().mockImplementation(function (kid) { expect(this).toBe(context); return kid; }); @@ -165,7 +165,7 @@ describe('ReactChildren', () => { const a = ; const context = {}; - const callback = jest.fn().mockImplementation(function(kid) { + const callback = jest.fn().mockImplementation(function (kid) { expect(this).toBe(context); return kid; }); @@ -225,7 +225,7 @@ describe('ReactChildren', () => { const five =
    ; const context = {}; - const callback = jest.fn().mockImplementation(function(kid) { + const callback = jest.fn().mockImplementation(function (kid) { return kid; }); @@ -263,7 +263,7 @@ describe('ReactChildren', () => { const zeroForceKey =
    ; const oneForceKey =
    ; const context = {}; - const callback = jest.fn().mockImplementation(function(kid) { + const callback = jest.fn().mockImplementation(function (kid) { expect(this).toBe(context); return kid; }); @@ -298,10 +298,10 @@ describe('ReactChildren', () => { it('should be called for each child in an iterable without keys', () => { const threeDivIterable = { - '@@iterator': function() { + '@@iterator': function () { let i = 0; return { - next: function() { + next: function () { if (i++ < 3) { return {value:
    , done: false}; } else { @@ -313,7 +313,7 @@ describe('ReactChildren', () => { }; const context = {}; - const callback = jest.fn().mockImplementation(function(kid) { + const callback = jest.fn().mockImplementation(function (kid) { expect(this).toBe(context); return kid; }); @@ -349,10 +349,10 @@ describe('ReactChildren', () => { it('should be called for each child in an iterable with keys', () => { const threeDivIterable = { - '@@iterator': function() { + '@@iterator': function () { let i = 0; return { - next: function() { + next: function () { if (i++ < 3) { return {value:
    , done: false}; } else { @@ -364,7 +364,7 @@ describe('ReactChildren', () => { }; const context = {}; - const callback = jest.fn().mockImplementation(function(kid) { + const callback = jest.fn().mockImplementation(function (kid) { expect(this).toBe(context); return kid; }); @@ -397,7 +397,7 @@ describe('ReactChildren', () => { it('should not enumerate enumerable numbers (#4776)', () => { /*eslint-disable no-extend-native */ - Number.prototype['@@iterator'] = function() { + Number.prototype['@@iterator'] = function () { throw new Error('number iterator called'); }; /*eslint-enable no-extend-native */ @@ -412,12 +412,12 @@ describe('ReactChildren', () => { ); const context = {}; - const callback = jest.fn().mockImplementation(function(kid) { + const callback = jest.fn().mockImplementation(function (kid) { expect(this).toBe(context); return kid; }); - const assertCalls = function() { + const assertCalls = function () { expect(callback).toHaveBeenCalledTimes(3); expect(callback).toHaveBeenCalledWith(5, 0); expect(callback).toHaveBeenCalledWith(12, 1); @@ -454,7 +454,7 @@ describe('ReactChildren', () => { ); const context = {}; - const callback = jest.fn().mockImplementation(function(kid) { + const callback = jest.fn().mockImplementation(function (kid) { expect(this).toBe(context); return kid; }); @@ -482,7 +482,7 @@ describe('ReactChildren', () => { }); it('should pass key to returned component', () => { - const mapFn = function(kid, index) { + const mapFn = function (kid, index) { return
    {kid}
    ; }; @@ -499,7 +499,7 @@ describe('ReactChildren', () => { it('should invoke callback with the right context', () => { let lastContext; - const callback = function(kid, index) { + const callback = function (kid, index) { lastContext = this; return this; }; @@ -536,7 +536,7 @@ describe('ReactChildren', () => { , // Map from null to something.
    , ]; - const callback = jest.fn().mockImplementation(function(kid, index) { + const callback = jest.fn().mockImplementation(function (kid, index) { return mapped[index]; }); @@ -597,7 +597,7 @@ describe('ReactChildren', () => { const fourMapped =
    ; const fiveMapped =
    ; - const callback = jest.fn().mockImplementation(function(kid) { + const callback = jest.fn().mockImplementation(function (kid) { switch (kid) { case zero: return zeroMapped; @@ -666,7 +666,7 @@ describe('ReactChildren', () => { // Key should be added even if we don't supply it! const oneForceKeyMapped =
    ; - const mapFn = function(kid, index) { + const mapFn = function (kid, index) { return index === 0 ? zeroForceKeyMapped : oneForceKeyMapped; }; @@ -702,7 +702,7 @@ describe('ReactChildren', () => { const zero =
    ; const one =
    ; - const mapFn = function() { + const mapFn = function () { return null; }; @@ -713,7 +713,7 @@ describe('ReactChildren', () => {
    ); - expect(function() { + expect(function () { React.Children.map(instance.props.children, mapFn); }).not.toThrow(); }); @@ -935,8 +935,8 @@ describe('ReactChildren', () => { }); it('should throw on object', () => { - expect(function() { - React.Children.forEach({a: 1, b: 2}, function() {}, null); + expect(function () { + React.Children.forEach({a: 1, b: 2}, function () {}, null); }).toThrowError( 'Objects are not valid as a React child (found: object with keys ' + '{a, b}).' + @@ -950,8 +950,8 @@ describe('ReactChildren', () => { it('should throw on regex', () => { // Really, we care about dates (#4840) but those have nondeterministic // serialization (timezones) so let's test a regex instead: - expect(function() { - React.Children.forEach(/abc/, function() {}, null); + expect(function () { + React.Children.forEach(/abc/, function () {}, null); }).toThrowError( 'Objects are not valid as a React child (found: /abc/).' + (__DEV__ diff --git a/packages/react/src/__tests__/ReactClassEquivalence-test.js b/packages/react/src/__tests__/ReactClassEquivalence-test.js index 912da27446cf..fa3d696e6a0d 100644 --- a/packages/react/src/__tests__/ReactClassEquivalence-test.js +++ b/packages/react/src/__tests__/ReactClassEquivalence-test.js @@ -29,21 +29,28 @@ function runJest(testFile) { const cwd = process.cwd(); const extension = process.platform === 'win32' ? '.cmd' : ''; const command = process.env.npm_lifecycle_event; + const defaultReporter = '--reporters=default'; + const equivalenceReporter = + '--reporters=/scripts/jest/spec-equivalence-reporter/equivalenceReporter.js'; if (!command.startsWith('test')) { throw new Error( 'Expected this test to run as a result of one of test commands.', ); } - const result = spawnSync('yarn' + extension, [command, testFile], { - cwd, - env: Object.assign({}, process.env, { - REACT_CLASS_EQUIVALENCE_TEST: 'true', - // Remove these so that the test file is not filtered out by the mechanism - // we use to parallelize tests in CI - CIRCLE_NODE_TOTAL: '', - CIRCLE_NODE_INDEX: '', - }), - }); + const result = spawnSync( + 'yarn' + extension, + [command, testFile, defaultReporter, equivalenceReporter], + { + cwd, + env: Object.assign({}, process.env, { + REACT_CLASS_EQUIVALENCE_TEST: 'true', + // Remove these so that the test file is not filtered out by the mechanism + // we use to parallelize tests in CI + CIRCLE_NODE_TOTAL: '', + CIRCLE_NODE_INDEX: '', + }), + }, + ); if (result.error) { throw result.error; diff --git a/packages/react/src/__tests__/ReactCoffeeScriptClass-test.coffee b/packages/react/src/__tests__/ReactCoffeeScriptClass-test.coffee index 0fd7e0b1e222..96bd3e0c24ca 100644 --- a/packages/react/src/__tests__/ReactCoffeeScriptClass-test.coffee +++ b/packages/react/src/__tests__/ReactCoffeeScriptClass-test.coffee @@ -9,9 +9,10 @@ PropTypes = null React = null ReactDOM = null ReactDOMClient = null -ReactFeatureFlags = null act = null +featureFlags = require 'shared/ReactFeatureFlags' + describe 'ReactCoffeeScriptClass', -> container = null root = null @@ -23,8 +24,6 @@ describe 'ReactCoffeeScriptClass', -> React = require 'react' ReactDOM = require 'react-dom' ReactDOMClient = require 'react-dom/client' - ReactFeatureFlags = require 'shared/ReactFeatureFlags' - act = require('jest-react').act PropTypes = require 'prop-types' container = document.createElement 'div' root = ReactDOMClient.createRoot container @@ -38,7 +37,7 @@ describe 'ReactCoffeeScriptClass', -> return React.createElement('div', className: this.props.name) test = (element, expectedTag, expectedClassName) -> - act -> + ReactDOM.flushSync -> root.render(element) expect(container.firstChild).not.toBeNull() expect(container.firstChild.tagName).toBe(expectedTag) @@ -52,7 +51,7 @@ describe 'ReactCoffeeScriptClass', -> class Foo extends React.Component expect(-> expect(-> - act -> + ReactDOM.flushSync -> root.render React.createElement(Foo) ).toThrow() ).toErrorDev([ @@ -105,7 +104,8 @@ describe 'ReactCoffeeScriptClass', -> ref = React.createRef() test React.createElement(Foo, initialValue: 'foo', ref: ref), 'DIV', 'foo' - ref.current.changeState() + ReactDOM.flushSync -> + ref.current.changeState() test React.createElement(Foo), 'SPAN', 'bar' it 'sets initial state with value returned by static getDerivedStateFromProps', -> @@ -131,7 +131,7 @@ describe 'ReactCoffeeScriptClass', -> getDerivedStateFromProps: -> {} expect(-> - act -> + ReactDOM.flushSync -> root.render React.createElement(Foo, foo: 'foo') return ).toErrorDev 'Foo: getDerivedStateFromProps() is defined as an instance method and will be ignored. Instead, declare it as a static method.' @@ -143,7 +143,7 @@ describe 'ReactCoffeeScriptClass', -> getDerivedStateFromError: -> {} expect(-> - act -> + ReactDOM.flushSync -> root.render React.createElement(Foo, foo: 'foo') return ).toErrorDev 'Foo: getDerivedStateFromError() is defined as an instance method and will be ignored. Instead, declare it as a static method.' @@ -155,7 +155,7 @@ describe 'ReactCoffeeScriptClass', -> Foo.getSnapshotBeforeUpdate = () -> {} expect(-> - act -> + ReactDOM.flushSync -> root.render React.createElement(Foo, foo: 'foo') return ).toErrorDev 'Foo: getSnapshotBeforeUpdate() is defined as a static method and will be ignored. Instead, declare it as an instance method.' @@ -172,7 +172,7 @@ describe 'ReactCoffeeScriptClass', -> bar: 'bar' } expect(-> - act -> + ReactDOM.flushSync -> root.render React.createElement(Foo, foo: 'foo') return ).toErrorDev ( @@ -218,36 +218,37 @@ describe 'ReactCoffeeScriptClass', -> test React.createElement(Foo, update: false), 'DIV', 'initial' test React.createElement(Foo, update: true), 'DIV', 'updated' - it 'renders based on context in the constructor', -> - class Foo extends React.Component - @contextTypes: - tag: PropTypes.string - className: PropTypes.string + if !featureFlags.disableLegacyContext + it 'renders based on context in the constructor', -> + class Foo extends React.Component + @contextTypes: + tag: PropTypes.string + className: PropTypes.string - constructor: (props, context) -> - super props, context - @state = - tag: context.tag - className: @context.className + constructor: (props, context) -> + super props, context + @state = + tag: context.tag + className: @context.className - render: -> - Tag = @state.tag - React.createElement Tag, - className: @state.className + render: -> + Tag = @state.tag + React.createElement Tag, + className: @state.className - class Outer extends React.Component - @childContextTypes: - tag: PropTypes.string - className: PropTypes.string + class Outer extends React.Component + @childContextTypes: + tag: PropTypes.string + className: PropTypes.string - getChildContext: -> - tag: 'span' - className: 'foo' + getChildContext: -> + tag: 'span' + className: 'foo' - render: -> - React.createElement Foo + render: -> + React.createElement Foo - test React.createElement(Outer), 'SPAN', 'foo' + test React.createElement(Outer), 'SPAN', 'foo' it 'renders only once when setting state in componentWillMount', -> renderCount = 0 @@ -263,9 +264,7 @@ describe 'ReactCoffeeScriptClass', -> React.createElement('span', className: @state.bar) test React.createElement(Foo, initialValue: 'foo'), 'SPAN', 'bar' - # This is broken with deferRenderPhaseUpdateToNextBatch flag on. - # We can't use the gate feature here because this test is also in CoffeeScript and TypeScript. - expect(renderCount).toBe(if global.__WWW__ and !global.__VARIANT__ then 2 else 1) + expect(renderCount).toBe(1) it 'should warn with non-object in the initial state property', -> [['an array'], 'a string', 1234].forEach (state) -> @@ -305,7 +304,7 @@ describe 'ReactCoffeeScriptClass', -> ) test React.createElement(Foo, initialValue: 'foo'), 'DIV', 'foo' - act -> + ReactDOM.flushSync -> attachedListener() expect(renderedName).toBe 'bar' @@ -342,7 +341,7 @@ describe 'ReactCoffeeScriptClass', -> ) test React.createElement(Foo, initialValue: 'foo'), 'DIV', 'foo' - act -> + ReactDOM.flushSync -> attachedListener() expect(renderedName).toBe 'bar' @@ -393,44 +392,45 @@ describe 'ReactCoffeeScriptClass', -> 'did-update', { value: 'foo' }, {} ] lifeCycles = [] # reset - act -> + ReactDOM.flushSync -> root.unmount() expect(lifeCycles).toEqual ['will-unmount'] - it 'warns when classic properties are defined on the instance, - but does not invoke them.', -> - getInitialStateWasCalled = false - getDefaultPropsWasCalled = false - class Foo extends React.Component - constructor: -> - @contextTypes = {} - @contextType = {} - @propTypes = {} + if !featureFlags.disableLegacyContext + it 'warns when classic properties are defined on the instance, + but does not invoke them.', -> + getInitialStateWasCalled = false + getDefaultPropsWasCalled = false + class Foo extends React.Component + constructor: -> + @contextTypes = {} + @contextType = {} + @propTypes = {} - getInitialState: -> - getInitialStateWasCalled = true - {} + getInitialState: -> + getInitialStateWasCalled = true + {} - getDefaultProps: -> - getDefaultPropsWasCalled = true - {} + getDefaultProps: -> + getDefaultPropsWasCalled = true + {} - render: -> - React.createElement('span', - className: 'foo' - ) + render: -> + React.createElement('span', + className: 'foo' + ) - expect(-> - test React.createElement(Foo), 'SPAN', 'foo' - ).toErrorDev([ - 'getInitialState was defined on Foo, a plain JavaScript class.', - 'getDefaultProps was defined on Foo, a plain JavaScript class.', - 'propTypes was defined as an instance property on Foo.', - 'contextTypes was defined as an instance property on Foo.', - 'contextType was defined as an instance property on Foo.', - ]) - expect(getInitialStateWasCalled).toBe false - expect(getDefaultPropsWasCalled).toBe false + expect(-> + test React.createElement(Foo), 'SPAN', 'foo' + ).toErrorDev([ + 'getInitialState was defined on Foo, a plain JavaScript class.', + 'getDefaultProps was defined on Foo, a plain JavaScript class.', + 'propTypes was defined as an instance property on Foo.', + 'contextTypes was defined as an instance property on Foo.', + 'contextType was defined as an instance property on Foo.', + ]) + expect(getInitialStateWasCalled).toBe false + expect(getDefaultPropsWasCalled).toBe false it 'does not warn about getInitialState() on class components if state is also defined.', -> @@ -517,22 +517,23 @@ describe 'ReactCoffeeScriptClass', -> {withoutStack: true} ) - it 'supports this.context passed via getChildContext', -> - class Bar extends React.Component - @contextTypes: - bar: PropTypes.string - render: -> - React.createElement('div', className: @context.bar) + if !featureFlags.disableLegacyContext + it 'supports this.context passed via getChildContext', -> + class Bar extends React.Component + @contextTypes: + bar: PropTypes.string + render: -> + React.createElement('div', className: @context.bar) - class Foo extends React.Component - @childContextTypes: - bar: PropTypes.string - getChildContext: -> - bar: 'bar-through-context' - render: -> - React.createElement Bar + class Foo extends React.Component + @childContextTypes: + bar: PropTypes.string + getChildContext: -> + bar: 'bar-through-context' + render: -> + React.createElement Bar - test React.createElement(Foo), 'DIV', 'bar-through-context' + test React.createElement(Foo), 'DIV', 'bar-through-context' it 'supports string refs', -> class Foo extends React.Component @@ -545,17 +546,13 @@ describe 'ReactCoffeeScriptClass', -> ref = React.createRef() expect(-> test(React.createElement(Foo, ref: ref), 'DIV', 'foo') - ).toErrorDev( - if ReactFeatureFlags.warnAboutStringRefs - then [ - 'Warning: Component "Foo" contains the string ref "inner". ' + - 'Support for string refs will be removed in a future major release. ' + - 'We recommend using useRef() or createRef() instead. ' + - 'Learn more about using refs safely here: https://reactjs.org/link/strict-mode-string-ref\n' + - ' in Foo (at **)' - ] - else [] - ); + ).toErrorDev([ + 'Warning: Component "Foo" contains the string ref "inner". ' + + 'Support for string refs will be removed in a future major release. ' + + 'We recommend using useRef() or createRef() instead. ' + + 'Learn more about using refs safely here: https://reactjs.org/link/strict-mode-string-ref\n' + + ' in Foo (at **)' + ]); expect(ref.current.refs.inner.getName()).toBe 'foo' it 'supports drilling through to the DOM using findDOMNode', -> diff --git a/packages/react/src/__tests__/ReactContextValidator-test.js b/packages/react/src/__tests__/ReactContextValidator-test.js index e4895d5c15bd..9bccbbc51f3d 100644 --- a/packages/react/src/__tests__/ReactContextValidator-test.js +++ b/packages/react/src/__tests__/ReactContextValidator-test.js @@ -35,6 +35,7 @@ describe('ReactContextValidator', () => { // TODO: This behavior creates a runtime dependency on propTypes. We should // ensure that this is not required for ES6 classes with Flow. + // @gate !disableLegacyContext it('should filter out context not in contextTypes', () => { class Component extends React.Component { render() { @@ -70,6 +71,7 @@ describe('ReactContextValidator', () => { expect(instance.childRef.current.context).toEqual({foo: 'abc'}); }); + // @gate !disableLegacyContext it('should pass next context to lifecycles', () => { let componentDidMountContext; let componentDidUpdateContext; @@ -148,6 +150,7 @@ describe('ReactContextValidator', () => { expect(componentDidUpdateContext).toEqual({foo: 'def'}); }); + // @gate !disableLegacyContext || !__DEV__ it('should check context types', () => { class Component extends React.Component { render() { @@ -213,6 +216,7 @@ describe('ReactContextValidator', () => { ); }); + // @gate !disableLegacyContext || !__DEV__ it('should check child context types', () => { class Component extends React.Component { getChildContext() { @@ -278,6 +282,7 @@ describe('ReactContextValidator', () => { // TODO (bvaughn) Remove this test and the associated behavior in the future. // It has only been added in Fiber to match the (unintentional) behavior in Stack. + // @gate !disableLegacyContext || !__DEV__ it('should warn (but not error) if getChildContext method is missing', () => { class ComponentA extends React.Component { static childContextTypes = { @@ -314,6 +319,7 @@ describe('ReactContextValidator', () => { // TODO (bvaughn) Remove this test and the associated behavior in the future. // It has only been added in Fiber to match the (unintentional) behavior in Stack. + // @gate !disableLegacyContext it('should pass parent context if getChildContext method is missing', () => { class ParentContextProvider extends React.Component { static childContextTypes = { @@ -474,6 +480,7 @@ describe('ReactContextValidator', () => { expect(renderedContext).toBe(secondContext); }); + // @gate !disableLegacyContext || !__DEV__ it('should warn if both contextType and contextTypes are defined', () => { const Context = React.createContext(); diff --git a/packages/react/src/__tests__/ReactES6Class-test.js b/packages/react/src/__tests__/ReactES6Class-test.js index 6c23038205ed..1f2d899f8820 100644 --- a/packages/react/src/__tests__/ReactES6Class-test.js +++ b/packages/react/src/__tests__/ReactES6Class-test.js @@ -13,13 +13,11 @@ let PropTypes; let React; let ReactDOM; let ReactDOMClient; -let ReactFeatureFlags; -let act; describe('ReactES6Class', () => { let container; let root; - const freeze = function(expectation) { + const freeze = function (expectation) { Object.freeze(expectation); return expectation; }; @@ -32,8 +30,6 @@ describe('ReactES6Class', () => { React = require('react'); ReactDOM = require('react-dom'); ReactDOMClient = require('react-dom/client'); - ReactFeatureFlags = require('shared/ReactFeatureFlags'); - act = require('jest-react').act; container = document.createElement('div'); root = ReactDOMClient.createRoot(container); attachedListener = null; @@ -51,7 +47,7 @@ describe('ReactES6Class', () => { }); function test(element, expectedTag, expectedClassName) { - act(() => root.render(element)); + ReactDOM.flushSync(() => root.render(element)); expect(container.firstChild).not.toBeNull(); expect(container.firstChild.tagName).toBe(expectedTag); expect(container.firstChild.className).toBe(expectedClassName); @@ -65,7 +61,7 @@ describe('ReactES6Class', () => { it('throws if no render function is defined', () => { class Foo extends React.Component {} expect(() => { - expect(() => act(() => root.render())).toThrow(); + expect(() => ReactDOM.flushSync(() => root.render())).toThrow(); }).toErrorDev([ // A failed component renders four times in DEV in concurrent mode 'Warning: Foo(...): No `render` method found on the returned component ' + @@ -120,7 +116,7 @@ describe('ReactES6Class', () => { } const ref = React.createRef(); test(, 'DIV', 'foo'); - act(() => ref.current.changeState()); + ReactDOM.flushSync(() => ref.current.changeState()); test(, 'SPAN', 'bar'); }); @@ -150,7 +146,7 @@ describe('ReactES6Class', () => { } } expect(() => { - act(() => root.render()); + ReactDOM.flushSync(() => root.render()); }).toErrorDev( 'Foo: getDerivedStateFromProps() is defined as an instance method ' + 'and will be ignored. Instead, declare it as a static method.', @@ -167,7 +163,7 @@ describe('ReactES6Class', () => { } } expect(() => { - act(() => root.render()); + ReactDOM.flushSync(() => root.render()); }).toErrorDev( 'Foo: getDerivedStateFromError() is defined as an instance method ' + 'and will be ignored. Instead, declare it as a static method.', @@ -182,7 +178,7 @@ describe('ReactES6Class', () => { } } expect(() => { - act(() => root.render()); + ReactDOM.flushSync(() => root.render()); }).toErrorDev( 'Foo: getSnapshotBeforeUpdate() is defined as a static method ' + 'and will be ignored. Instead, declare it as an instance method.', @@ -202,7 +198,7 @@ describe('ReactES6Class', () => { } } expect(() => { - act(() => root.render()); + ReactDOM.flushSync(() => root.render()); }).toErrorDev( '`Foo` uses `getDerivedStateFromProps` but its initial state is ' + 'undefined. This is not recommended. Instead, define the initial state by ' + @@ -250,36 +246,38 @@ describe('ReactES6Class', () => { test(, 'DIV', 'updated'); }); - it('renders based on context in the constructor', () => { - class Foo extends React.Component { - constructor(props, context) { - super(props, context); - this.state = {tag: context.tag, className: this.context.className}; - } - render() { - const Tag = this.state.tag; - return ; + if (!require('shared/ReactFeatureFlags').disableLegacyContext) { + it('renders based on context in the constructor', () => { + class Foo extends React.Component { + constructor(props, context) { + super(props, context); + this.state = {tag: context.tag, className: this.context.className}; + } + render() { + const Tag = this.state.tag; + return ; + } } - } - Foo.contextTypes = { - tag: PropTypes.string, - className: PropTypes.string, - }; + Foo.contextTypes = { + tag: PropTypes.string, + className: PropTypes.string, + }; - class Outer extends React.Component { - getChildContext() { - return {tag: 'span', className: 'foo'}; - } - render() { - return ; + class Outer extends React.Component { + getChildContext() { + return {tag: 'span', className: 'foo'}; + } + render() { + return ; + } } - } - Outer.childContextTypes = { - tag: PropTypes.string, - className: PropTypes.string, - }; - test(, 'SPAN', 'foo'); - }); + Outer.childContextTypes = { + tag: PropTypes.string, + className: PropTypes.string, + }; + test(, 'SPAN', 'foo'); + }); + } it('renders only once when setting state in componentWillMount', () => { let renderCount = 0; @@ -297,13 +295,11 @@ describe('ReactES6Class', () => { } } test(, 'SPAN', 'bar'); - // This is broken with deferRenderPhaseUpdateToNextBatch flag on. - // We can't use the gate feature here because this test is also in CoffeeScript and TypeScript. - expect(renderCount).toBe(global.__WWW__ && !global.__VARIANT__ ? 2 : 1); + expect(renderCount).toBe(1); }); it('should warn with non-object in the initial state property', () => { - [['an array'], 'a string', 1234].forEach(function(state) { + [['an array'], 'a string', 1234].forEach(function (state) { class Foo extends React.Component { constructor() { super(); @@ -349,7 +345,7 @@ describe('ReactES6Class', () => { } test(, 'DIV', 'foo'); - act(() => attachedListener()); + ReactDOM.flushSync(() => attachedListener()); expect(renderedName).toBe('bar'); }); @@ -390,7 +386,7 @@ describe('ReactES6Class', () => { } } test(, 'DIV', 'foo'); - act(() => attachedListener()); + ReactDOM.flushSync(() => attachedListener()); expect(renderedName).toBe('bar'); }); @@ -439,43 +435,45 @@ describe('ReactES6Class', () => { 'did-update', freeze({value: 'foo'}), {}, ]); lifeCycles = []; // reset - act(() => root.unmount()); + ReactDOM.flushSync(() => root.unmount()); expect(lifeCycles).toEqual(['will-unmount']); }); - it('warns when classic properties are defined on the instance, but does not invoke them.', () => { - let getDefaultPropsWasCalled = false; - let getInitialStateWasCalled = false; - class Foo extends React.Component { - constructor() { - super(); - this.contextTypes = {}; - this.contextType = {}; - this.propTypes = {}; - } - getInitialState() { - getInitialStateWasCalled = true; - return {}; - } - getDefaultProps() { - getDefaultPropsWasCalled = true; - return {}; - } - render() { - return ; + if (!require('shared/ReactFeatureFlags').disableLegacyContext) { + it('warns when classic properties are defined on the instance, but does not invoke them.', () => { + let getDefaultPropsWasCalled = false; + let getInitialStateWasCalled = false; + class Foo extends React.Component { + constructor() { + super(); + this.contextTypes = {}; + this.contextType = {}; + this.propTypes = {}; + } + getInitialState() { + getInitialStateWasCalled = true; + return {}; + } + getDefaultProps() { + getDefaultPropsWasCalled = true; + return {}; + } + render() { + return ; + } } - } - expect(() => test(, 'SPAN', 'foo')).toErrorDev([ - 'getInitialState was defined on Foo, a plain JavaScript class.', - 'getDefaultProps was defined on Foo, a plain JavaScript class.', - 'propTypes was defined as an instance property on Foo.', - 'contextType was defined as an instance property on Foo.', - 'contextTypes was defined as an instance property on Foo.', - ]); - expect(getInitialStateWasCalled).toBe(false); - expect(getDefaultPropsWasCalled).toBe(false); - }); + expect(() => test(, 'SPAN', 'foo')).toErrorDev([ + 'getInitialState was defined on Foo, a plain JavaScript class.', + 'getDefaultProps was defined on Foo, a plain JavaScript class.', + 'propTypes was defined as an instance property on Foo.', + 'contextType was defined as an instance property on Foo.', + 'contextTypes was defined as an instance property on Foo.', + ]); + expect(getInitialStateWasCalled).toBe(false); + expect(getDefaultPropsWasCalled).toBe(false); + }); + } it('does not warn about getInitialState() on class components if state is also defined.', () => { class Foo extends React.Component { @@ -551,32 +549,32 @@ describe('ReactES6Class', () => { 'replaceState(...) is deprecated in plain JavaScript React classes', {withoutStack: true}, ); - expect(() => - expect(() => ref.current.isMounted()).toThrow(), - ).toWarnDev( + expect(() => expect(() => ref.current.isMounted()).toThrow()).toWarnDev( 'isMounted(...) is deprecated in plain JavaScript React classes', {withoutStack: true}, ); }); - it('supports this.context passed via getChildContext', () => { - class Bar extends React.Component { - render() { - return
    ; - } - } - Bar.contextTypes = {bar: PropTypes.string}; - class Foo extends React.Component { - getChildContext() { - return {bar: 'bar-through-context'}; + if (!require('shared/ReactFeatureFlags').disableLegacyContext) { + it('supports this.context passed via getChildContext', () => { + class Bar extends React.Component { + render() { + return
    ; + } } - render() { - return ; + Bar.contextTypes = {bar: PropTypes.string}; + class Foo extends React.Component { + getChildContext() { + return {bar: 'bar-through-context'}; + } + render() { + return ; + } } - } - Foo.childContextTypes = {bar: PropTypes.string}; - test(, 'DIV', 'bar-through-context'); - }); + Foo.childContextTypes = {bar: PropTypes.string}; + test(, 'DIV', 'bar-through-context'); + }); + } it('supports string refs', () => { class Foo extends React.Component { @@ -587,17 +585,13 @@ describe('ReactES6Class', () => { const ref = React.createRef(); expect(() => { test(, 'DIV', 'foo'); - }).toErrorDev( - ReactFeatureFlags.warnAboutStringRefs - ? [ - 'Warning: Component "Foo" contains the string ref "inner". ' + - 'Support for string refs will be removed in a future major release. ' + - 'We recommend using useRef() or createRef() instead. ' + - 'Learn more about using refs safely here: https://reactjs.org/link/strict-mode-string-ref\n' + - ' in Foo (at **)', - ] - : [], - ); + }).toErrorDev([ + 'Warning: Component "Foo" contains the string ref "inner". ' + + 'Support for string refs will be removed in a future major release. ' + + 'We recommend using useRef() or createRef() instead. ' + + 'Learn more about using refs safely here: https://reactjs.org/link/strict-mode-string-ref\n' + + ' in Foo (at **)', + ]); expect(ref.current.refs.inner.getName()).toBe('foo'); }); diff --git a/packages/react/src/__tests__/ReactElement-test.js b/packages/react/src/__tests__/ReactElement-test.js index f1e9cffb8424..a2440486c286 100644 --- a/packages/react/src/__tests__/ReactElement-test.js +++ b/packages/react/src/__tests__/ReactElement-test.js @@ -71,9 +71,7 @@ describe('ReactElement', () => { it('should warn when `key` is being accessed on a host element', () => { const element =
    ; - expect( - () => void element.props.key, - ).toErrorDev( + expect(() => void element.props.key).toErrorDev( 'div: `key` is not a prop. Trying to access it will result ' + 'in `undefined` being returned. If you need to access the same ' + 'value within the child component, you should pass it as a different ' + @@ -336,7 +334,7 @@ describe('ReactElement', () => { const el =
    ; if (__DEV__) { - expect(function() { + expect(function () { el.props.className = 'quack'; }).toThrow(); expect(el.props.className).toBe('moo'); @@ -363,7 +361,7 @@ describe('ReactElement', () => { const el =
    {this.props.sound}
    ; if (__DEV__) { - expect(function() { + expect(function () { el.props.className = 'quack'; }).toThrow(); expect(el.props.className).toBe(undefined); diff --git a/packages/react/src/__tests__/ReactElementJSX-test.js b/packages/react/src/__tests__/ReactElementJSX-test.js index ad42046087c4..4ef68f96be54 100644 --- a/packages/react/src/__tests__/ReactElementJSX-test.js +++ b/packages/react/src/__tests__/ReactElementJSX-test.js @@ -92,7 +92,7 @@ describe('ReactElement.jsx', () => { const el = JSXRuntime.jsx('div', {className: 'moo'}); if (__DEV__) { - expect(function() { + expect(function () { el.props.className = 'quack'; }).toThrow(); expect(el.props.className).toBe('moo'); @@ -121,7 +121,7 @@ describe('ReactElement.jsx', () => { const el = JSXRuntime.jsx('div', {children: this.props.sound}); if (__DEV__) { - expect(function() { + expect(function () { el.props.className = 'quack'; }).toThrow(); expect(el.props.className).toBe(undefined); @@ -200,9 +200,7 @@ describe('ReactElement.jsx', () => { it('should warn when `key` is being accessed on a host element', () => { const element = JSXRuntime.jsxs('div', {}, '3'); - expect( - () => void element.props.key, - ).toErrorDev( + expect(() => void element.props.key).toErrorDev( 'div: `key` is not a prop. Trying to access it will result ' + 'in `undefined` being returned. If you need to access the same ' + 'value within the child component, you should pass it as a different ' + @@ -264,33 +262,31 @@ describe('ReactElement.jsx', () => { ); }); - if (require('shared/ReactFeatureFlags').warnAboutSpreadingKeyToJSX) { - it('should warn when keys are passed as part of props', () => { - const container = document.createElement('div'); - class Child extends React.Component { - render() { - return JSXRuntime.jsx('div', {}); - } + it('should warn when keys are passed as part of props', () => { + const container = document.createElement('div'); + class Child extends React.Component { + render() { + return JSXRuntime.jsx('div', {}); } - class Parent extends React.Component { - render() { - return JSXRuntime.jsx('div', { - children: [JSXRuntime.jsx(Child, {key: '0', prop: 'hi'})], - }); - } + } + class Parent extends React.Component { + render() { + return JSXRuntime.jsx('div', { + children: [JSXRuntime.jsx(Child, {key: '0', prop: 'hi'})], + }); } - expect(() => - ReactDOM.render(JSXRuntime.jsx(Parent, {}), container), - ).toErrorDev( - 'Warning: A props object containing a "key" prop is being spread into JSX:\n' + - ' let props = {key: someKey, prop: ...};\n' + - ' \n' + - 'React keys must be passed directly to JSX without using spread:\n' + - ' let props = {prop: ...};\n' + - ' ', - ); - }); - } + } + expect(() => + ReactDOM.render(JSXRuntime.jsx(Parent, {}), container), + ).toErrorDev( + 'Warning: A props object containing a "key" prop is being spread into JSX:\n' + + ' let props = {key: someKey, prop: ...};\n' + + ' \n' + + 'React keys must be passed directly to JSX without using spread:\n' + + ' let props = {prop: ...};\n' + + ' ', + ); + }); it('should not warn when unkeyed children are passed to jsxs', () => { const container = document.createElement('div'); diff --git a/packages/react/src/__tests__/ReactElementValidator-test.internal.js b/packages/react/src/__tests__/ReactElementValidator-test.internal.js index e0150cc4aa0d..c447f554a71a 100644 --- a/packages/react/src/__tests__/ReactElementValidator-test.internal.js +++ b/packages/react/src/__tests__/ReactElementValidator-test.internal.js @@ -148,10 +148,10 @@ describe('ReactElementValidator', () => { it('warns for keys for iterables of elements in rest args', () => { const iterable = { - '@@iterator': function() { + '@@iterator': function () { let i = 0; return { - next: function() { + next: function () { const done = ++i > 2; return { value: done ? undefined : React.createElement(ComponentClass), @@ -176,10 +176,10 @@ describe('ReactElementValidator', () => { it('does not warns for iterable elements with keys', () => { const iterable = { - '@@iterator': function() { + '@@iterator': function () { let i = 0; return { - next: function() { + next: function () { const done = ++i > 2; return { value: done @@ -455,9 +455,7 @@ describe('ReactElementValidator', () => { {withoutStack: true}, ); - expect( - () => TestFactory.type, - ).toWarnDev( + expect(() => TestFactory.type).toWarnDev( 'Warning: Factory.type is deprecated. Access the class directly before ' + 'passing it to createFactory.', {withoutStack: true}, @@ -485,7 +483,7 @@ describe('ReactElementValidator', () => { it('should not enumerate enumerable numbers (#4776)', () => { /*eslint-disable no-extend-native */ - Number.prototype['@@iterator'] = function() { + Number.prototype['@@iterator'] = function () { throw new Error('number iterator called'); }; /*eslint-enable no-extend-native */ diff --git a/packages/react/src/__tests__/ReactFetch-test.js b/packages/react/src/__tests__/ReactFetch-test.js index e1da3b10abf9..2993ed564cdb 100644 --- a/packages/react/src/__tests__/ReactFetch-test.js +++ b/packages/react/src/__tests__/ReactFetch-test.js @@ -10,14 +10,13 @@ 'use strict'; // Polyfills for test environment -global.ReadableStream = require('web-streams-polyfill/ponyfill/es6').ReadableStream; +global.ReadableStream = + require('web-streams-polyfill/ponyfill/es6').ReadableStream; global.TextEncoder = require('util').TextEncoder; global.TextDecoder = require('util').TextDecoder; global.Headers = require('node-fetch').Headers; global.Request = require('node-fetch').Request; global.Response = require('node-fetch').Response; -// Patch for Browser environments to be able to polyfill AsyncLocalStorage -global.AsyncLocalStorage = require('async_hooks').AsyncLocalStorage; let fetchCount = 0; async function fetchMock(resource, options) { @@ -184,7 +183,7 @@ describe('ReactFetch', () => { return text + ' ' + text2; } expect(await render(Component)).toMatchInlineSnapshot( - `"GET world [[\\"a\\",\\"A\\"]] GET world [[\\"b\\",\\"B\\"]]"`, + `"GET world [["a","A"]] GET world [["b","B"]]"`, ); expect(fetchCount).toBe(2); }); diff --git a/packages/react/src/__tests__/ReactFetchEdge-test.js b/packages/react/src/__tests__/ReactFetchEdge-test.js new file mode 100644 index 000000000000..832b8e542843 --- /dev/null +++ b/packages/react/src/__tests__/ReactFetchEdge-test.js @@ -0,0 +1,84 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @emails react-core + */ + +'use strict'; + +// Polyfills for test environment +global.ReadableStream = + require('web-streams-polyfill/ponyfill/es6').ReadableStream; +global.TextEncoder = require('util').TextEncoder; +global.TextDecoder = require('util').TextDecoder; +global.Headers = require('node-fetch').Headers; +global.Request = require('node-fetch').Request; +global.Response = require('node-fetch').Response; +// Patch for Edge environments for global scope +global.AsyncLocalStorage = require('async_hooks').AsyncLocalStorage; + +// Don't wait before processing work on the server. +// TODO: we can replace this with FlightServer.act(). +global.setTimeout = cb => cb(); + +let fetchCount = 0; +async function fetchMock(resource, options) { + fetchCount++; + const request = new Request(resource, options); + return new Response( + request.method + + ' ' + + request.url + + ' ' + + JSON.stringify(Array.from(request.headers.entries())), + ); +} + +let React; +let ReactServerDOMServer; +let ReactServerDOMClient; +let use; + +describe('ReactFetch', () => { + beforeEach(() => { + jest.resetModules(); + fetchCount = 0; + global.fetch = fetchMock; + + if (gate(flags => !flags.www)) { + jest.mock('react', () => require('react/react.shared-subset')); + } + + React = require('react'); + ReactServerDOMServer = require('react-server-dom-webpack/server.edge'); + ReactServerDOMClient = require('react-server-dom-webpack/client'); + use = React.use; + }); + + async function render(Component) { + const stream = ReactServerDOMServer.renderToReadableStream(); + return ReactServerDOMClient.createFromReadableStream(stream); + } + + // @gate enableFetchInstrumentation && enableCache + it('can dedupe fetches separately in interleaved renders', async () => { + async function getData() { + const r1 = await fetch('hi'); + const t1 = await r1.text(); + const r2 = await fetch('hi'); + const t2 = await r2.text(); + return t1 + ' ' + t2; + } + function Component() { + return use(getData()); + } + const render1 = render(Component); + const render2 = render(Component); + expect(await render1).toMatchInlineSnapshot(`"GET hi [] GET hi []"`); + expect(await render2).toMatchInlineSnapshot(`"GET hi [] GET hi []"`); + expect(fetchCount).toBe(2); + }); +}); diff --git a/packages/react/src/__tests__/ReactJSXElementValidator-test.js b/packages/react/src/__tests__/ReactJSXElementValidator-test.js index 69bd83a5a289..71753ed0ace2 100644 --- a/packages/react/src/__tests__/ReactJSXElementValidator-test.js +++ b/packages/react/src/__tests__/ReactJSXElementValidator-test.js @@ -75,10 +75,10 @@ describe('ReactJSXElementValidator', () => { it('warns for keys for iterables of elements in rest args', () => { const iterable = { - '@@iterator': function() { + '@@iterator': function () { let i = 0; return { - next: function() { + next: function () { const done = ++i > 2; return {value: done ? undefined : , done: done}; }, @@ -99,10 +99,10 @@ describe('ReactJSXElementValidator', () => { it('does not warn for iterable elements with keys', () => { const iterable = { - '@@iterator': function() { + '@@iterator': function () { let i = 0; return { - next: function() { + next: function () { const done = ++i > 2; return { value: done ? undefined : , @@ -118,10 +118,10 @@ describe('ReactJSXElementValidator', () => { it('does not warn for numeric keys in entry iterable as a child', () => { const iterable = { - '@@iterator': function() { + '@@iterator': function () { let i = 0; return { - next: function() { + next: function () { const done = ++i > 2; return {value: done ? undefined : [i, ], done: done}; }, @@ -210,9 +210,7 @@ describe('ReactJSXElementValidator', () => { const Null = null; const True = true; const Div = 'div'; - expect( - () => void (), - ).toErrorDev( + expect(() => void ()).toErrorDev( 'Warning: React.createElement: type is invalid -- expected a string ' + '(for built-in components) or a class/function (for composite ' + 'components) but got: undefined. You likely forgot to export your ' + @@ -221,18 +219,14 @@ describe('ReactJSXElementValidator', () => { '\n\nCheck your code at **.', {withoutStack: true}, ); - expect( - () => void (), - ).toErrorDev( + expect(() => void ()).toErrorDev( 'Warning: React.createElement: type is invalid -- expected a string ' + '(for built-in components) or a class/function (for composite ' + 'components) but got: null.' + '\n\nCheck your code at **.', {withoutStack: true}, ); - expect( - () => void (), - ).toErrorDev( + expect(() => void ()).toErrorDev( 'Warning: React.createElement: type is invalid -- expected a string ' + '(for built-in components) or a class/function (for composite ' + 'components) but got: boolean.' + @@ -308,6 +302,7 @@ describe('ReactJSXElementValidator', () => { ); }); + // @gate !disableLegacyContext || !__DEV__ it('should warn on invalid context types', () => { class NullContextTypeComponent extends React.Component { render() { diff --git a/packages/react/src/__tests__/ReactProfiler-test.internal.js b/packages/react/src/__tests__/ReactProfiler-test.internal.js index 3d7e061851b1..de6b7f973e66 100644 --- a/packages/react/src/__tests__/ReactProfiler-test.internal.js +++ b/packages/react/src/__tests__/ReactProfiler-test.internal.js @@ -17,6 +17,10 @@ let Scheduler; let ReactTestRenderer; let act; let AdvanceTime; +let assertLog; +let waitFor; +let waitForAll; +let waitForThrow; function loadModules({ enableProfilerTimer = true, @@ -30,13 +34,16 @@ function loadModules({ ReactFeatureFlags.enableProfilerTimer = enableProfilerTimer; ReactFeatureFlags.enableProfilerCommitHooks = enableProfilerCommitHooks; - ReactFeatureFlags.enableProfilerNestedUpdatePhase = enableProfilerNestedUpdatePhase; - ReactFeatureFlags.enableProfilerNestedUpdateScheduledHook = enableProfilerNestedUpdateScheduledHook; - ReactFeatureFlags.replayFailedUnitOfWorkWithInvokeGuardedCallback = replayFailedUnitOfWorkWithInvokeGuardedCallback; + ReactFeatureFlags.enableProfilerNestedUpdatePhase = + enableProfilerNestedUpdatePhase; + ReactFeatureFlags.enableProfilerNestedUpdateScheduledHook = + enableProfilerNestedUpdateScheduledHook; + ReactFeatureFlags.replayFailedUnitOfWorkWithInvokeGuardedCallback = + replayFailedUnitOfWorkWithInvokeGuardedCallback; React = require('react'); Scheduler = require('scheduler'); - act = require('jest-react').act; + act = require('internal-test-utils').act; if (useNoopRenderer) { ReactNoop = require('react-noop-renderer'); @@ -46,6 +53,12 @@ function loadModules({ ReactTestRenderer = require('react-test-renderer'); } + const InternalTestUtils = require('internal-test-utils'); + assertLog = InternalTestUtils.assertLog; + waitFor = InternalTestUtils.waitFor; + waitForAll = InternalTestUtils.waitForAll; + waitForThrow = InternalTestUtils.waitForThrow; + AdvanceTime = class extends React.Component { static defaultProps = { byAmount: 10, @@ -185,27 +198,15 @@ describe(`onRender`, () => { expect(callback).toHaveBeenCalledTimes(2); }); - it('is not invoked until the commit phase', () => { + it('is not invoked until the commit phase', async () => { const callback = jest.fn(); const Yield = ({value}) => { - Scheduler.unstable_yieldValue(value); + Scheduler.log(value); return null; }; - if (gate(flags => flags.enableSyncDefaultUpdates)) { - React.startTransition(() => { - ReactTestRenderer.create( - - - - , - { - unstable_isConcurrent: true, - }, - ); - }); - } else { + React.startTransition(() => { ReactTestRenderer.create( @@ -215,12 +216,12 @@ describe(`onRender`, () => { unstable_isConcurrent: true, }, ); - } + }); // Times are logged until a render is committed. - expect(Scheduler).toFlushAndYieldThrough(['first']); + await waitFor(['first']); expect(callback).toHaveBeenCalledTimes(0); - expect(Scheduler).toFlushAndYield(['last']); + await waitForAll(['last']); expect(callback).toHaveBeenCalledTimes(1); }); @@ -232,7 +233,7 @@ describe(`onRender`, () => { return { ...ActualScheduler, unstable_now: function mockUnstableNow() { - ActualScheduler.unstable_yieldValue('read current time'); + ActualScheduler.log('read current time'); return ActualScheduler.unstable_now(); }, }; @@ -243,7 +244,7 @@ describe(`onRender`, () => { loadModules(); // Clear yields in case the current time is read during initialization. - Scheduler.unstable_clearYields(); + Scheduler.unstable_clearLog(); ReactTestRenderer.create(
    @@ -257,19 +258,28 @@ describe(`onRender`, () => { // TODO: unstable_now is called by more places than just the profiler. // Rewrite this test so it's less fragile. - expect(Scheduler).toHaveYielded([ - 'read current time', - 'read current time', - 'read current time', - 'read current time', - 'read current time', - ]); + if (gate(flags => flags.enableDeferRootSchedulingToMicrotask)) { + assertLog([ + 'read current time', + 'read current time', + 'read current time', + ]); + } else { + assertLog([ + 'read current time', + 'read current time', + 'read current time', + 'read current time', + 'read current time', + 'read current time', + ]); + } // Restore original mock jest.mock('scheduler', () => jest.requireActual('scheduler/unstable_mock')); }); - it('does not report work done on a sibling', () => { + it('does not report work done on a sibling', async () => { const callback = jest.fn(); const DoesNotUpdate = React.memo( @@ -345,7 +355,7 @@ describe(`onRender`, () => { Scheduler.unstable_advanceTime(20); // 30 -> 50 // Updating a sibling should not report a re-render. - act(updateProfilerSibling); + await act(() => updateProfilerSibling()); expect(callback).not.toHaveBeenCalled(); }); @@ -645,7 +655,7 @@ describe(`onRender`, () => { expect(updateCall[5]).toBe(43); // commit time }); - it('should clear nested-update flag when multiple cascading renders are scheduled', () => { + it('should clear nested-update flag when multiple cascading renders are scheduled', async () => { loadModules({ useNoopRenderer: true, }); @@ -664,21 +674,21 @@ describe(`onRender`, () => { } }, [didMount, didMountAndUpdate]); - Scheduler.unstable_yieldValue(`${didMount}:${didMountAndUpdate}`); + Scheduler.log(`${didMount}:${didMountAndUpdate}`); return null; } const onRender = jest.fn(); - act(() => { + await act(() => { ReactNoop.render( , ); }); - expect(Scheduler).toHaveYielded(['false:false', 'true:false', 'true:true']); + assertLog(['false:false', 'true:false', 'true:true']); expect(onRender).toHaveBeenCalledTimes(3); expect(onRender.mock.calls[0][1]).toBe('mount'); @@ -697,7 +707,7 @@ describe(`onRender`, () => { React.useLayoutEffect(() => { setDidMount(true); }, []); - Scheduler.unstable_yieldValue(didMount); + Scheduler.log(didMount); return didMount; } @@ -720,7 +730,7 @@ describe(`onRender`, () => { , ); }); - expect(Scheduler).toHaveYielded([false, true]); + assertLog([false, true]); // Verify that the nested update inside of the sync work is appropriately tagged. expect(onRender).toHaveBeenCalledTimes(2); @@ -729,29 +739,19 @@ describe(`onRender`, () => { }); describe('with regard to interruptions', () => { - it('should accumulate actual time after a scheduling interruptions', () => { + it('should accumulate actual time after a scheduling interruptions', async () => { const callback = jest.fn(); const Yield = ({renderTime}) => { Scheduler.unstable_advanceTime(renderTime); - Scheduler.unstable_yieldValue('Yield:' + renderTime); + Scheduler.log('Yield:' + renderTime); return null; }; Scheduler.unstable_advanceTime(5); // 0 -> 5 // Render partially, but run out of time before completing. - if (gate(flags => flags.enableSyncDefaultUpdates)) { - React.startTransition(() => { - ReactTestRenderer.create( - - - - , - {unstable_isConcurrent: true}, - ); - }); - } else { + React.startTransition(() => { ReactTestRenderer.create( @@ -759,12 +759,12 @@ describe(`onRender`, () => { , {unstable_isConcurrent: true}, ); - } - expect(Scheduler).toFlushAndYieldThrough(['Yield:2']); + }); + await waitFor(['Yield:2']); expect(callback).toHaveBeenCalledTimes(0); // Resume render for remaining children. - expect(Scheduler).toFlushAndYield(['Yield:3']); + await waitForAll(['Yield:3']); // Verify that logged times include both durations above. expect(callback).toHaveBeenCalledTimes(1); @@ -775,12 +775,12 @@ describe(`onRender`, () => { expect(call[5]).toBe(10); // commit time }); - it('should not include time between frames', () => { + it('should not include time between frames', async () => { const callback = jest.fn(); const Yield = ({renderTime}) => { Scheduler.unstable_advanceTime(renderTime); - Scheduler.unstable_yieldValue('Yield:' + renderTime); + Scheduler.log('Yield:' + renderTime); return null; }; @@ -788,20 +788,7 @@ describe(`onRender`, () => { // Render partially, but don't finish. // This partial render should take 5ms of simulated time. - if (gate(flags => flags.enableSyncDefaultUpdates)) { - React.startTransition(() => { - ReactTestRenderer.create( - - - - - - - , - {unstable_isConcurrent: true}, - ); - }); - } else { + React.startTransition(() => { ReactTestRenderer.create( @@ -812,8 +799,8 @@ describe(`onRender`, () => { , {unstable_isConcurrent: true}, ); - } - expect(Scheduler).toFlushAndYieldThrough(['Yield:5']); + }); + await waitFor(['Yield:5']); expect(callback).toHaveBeenCalledTimes(0); // Simulate time moving forward while frame is paused. @@ -821,7 +808,7 @@ describe(`onRender`, () => { // Flush the remaining work, // Which should take an additional 10ms of simulated time. - expect(Scheduler).toFlushAndYield(['Yield:10', 'Yield:17']); + await waitForAll(['Yield:10', 'Yield:17']); expect(callback).toHaveBeenCalledTimes(2); const [innerCall, outerCall] = callback.mock.calls; @@ -840,12 +827,12 @@ describe(`onRender`, () => { expect(outerCall[5]).toBe(87); // commit time }); - it('should report the expected times when a high-pri update replaces a mount in-progress', () => { + it('should report the expected times when a high-pri update replaces a mount in-progress', async () => { const callback = jest.fn(); const Yield = ({renderTime}) => { Scheduler.unstable_advanceTime(renderTime); - Scheduler.unstable_yieldValue('Yield:' + renderTime); + Scheduler.log('Yield:' + renderTime); return null; }; @@ -854,17 +841,7 @@ describe(`onRender`, () => { // Render a partially update, but don't finish. // This partial render should take 10ms of simulated time. let renderer; - if (gate(flags => flags.enableSyncDefaultUpdates)) { - React.startTransition(() => { - renderer = ReactTestRenderer.create( - - - - , - {unstable_isConcurrent: true}, - ); - }); - } else { + React.startTransition(() => { renderer = ReactTestRenderer.create( @@ -872,8 +849,8 @@ describe(`onRender`, () => { , {unstable_isConcurrent: true}, ); - } - expect(Scheduler).toFlushAndYieldThrough(['Yield:10']); + }); + await waitFor(['Yield:10']); expect(callback).toHaveBeenCalledTimes(0); // Simulate time moving forward while frame is paused. @@ -888,7 +865,7 @@ describe(`onRender`, () => { , ); }); - expect(Scheduler).toHaveYielded(['Yield:5']); + assertLog(['Yield:5']); // The initial work was thrown away in this case, // So the actual and base times should only include the final rendered tree times. @@ -902,16 +879,16 @@ describe(`onRender`, () => { callback.mockReset(); // Verify no more unexpected callbacks from low priority work - expect(Scheduler).toFlushWithoutYielding(); + await waitForAll([]); expect(callback).toHaveBeenCalledTimes(0); }); - it('should report the expected times when a high-priority update replaces a low-priority update', () => { + it('should report the expected times when a high-priority update replaces a low-priority update', async () => { const callback = jest.fn(); const Yield = ({renderTime}) => { Scheduler.unstable_advanceTime(renderTime); - Scheduler.unstable_yieldValue('Yield:' + renderTime); + Scheduler.log('Yield:' + renderTime); return null; }; @@ -927,7 +904,7 @@ describe(`onRender`, () => { // Render everything initially. // This should take 21 seconds of actual and base time. - expect(Scheduler).toFlushAndYield(['Yield:6', 'Yield:15']); + await waitForAll(['Yield:6', 'Yield:15']); expect(callback).toHaveBeenCalledTimes(1); let call = callback.mock.calls[0]; expect(call[2]).toBe(21); // actual time @@ -941,17 +918,7 @@ describe(`onRender`, () => { // Render a partially update, but don't finish. // This partial render should take 3ms of simulated time. - if (gate(flags => flags.enableSyncDefaultUpdates)) { - React.startTransition(() => { - renderer.update( - - - - - , - ); - }); - } else { + React.startTransition(() => { renderer.update( @@ -959,15 +926,15 @@ describe(`onRender`, () => { , ); - } - expect(Scheduler).toFlushAndYieldThrough(['Yield:3']); + }); + await waitFor(['Yield:3']); expect(callback).toHaveBeenCalledTimes(0); // Simulate time moving forward while frame is paused. Scheduler.unstable_advanceTime(100); // 59 -> 159 // Render another 5ms of simulated time. - expect(Scheduler).toFlushAndYieldThrough(['Yield:5']); + await waitFor(['Yield:5']); expect(callback).toHaveBeenCalledTimes(0); // Simulate time moving forward while frame is paused. @@ -982,7 +949,7 @@ describe(`onRender`, () => { , ); }); - expect(Scheduler).toHaveYielded(['Yield:11']); + assertLog(['Yield:11']); // The actual time should include only the most recent render, // Because this lets us avoid a lot of commit phase reset complexity. @@ -995,16 +962,16 @@ describe(`onRender`, () => { expect(call[5]).toBe(275); // commit time // Verify no more unexpected callbacks from low priority work - expect(Scheduler).toFlushAndYield([]); + await waitForAll([]); expect(callback).toHaveBeenCalledTimes(1); }); - it('should report the expected times when a high-priority update interrupts a low-priority update', () => { + it('should report the expected times when a high-priority update interrupts a low-priority update', async () => { const callback = jest.fn(); const Yield = ({renderTime}) => { Scheduler.unstable_advanceTime(renderTime); - Scheduler.unstable_yieldValue('Yield:' + renderTime); + Scheduler.log('Yield:' + renderTime); return null; }; @@ -1014,9 +981,7 @@ describe(`onRender`, () => { render() { first = this; Scheduler.unstable_advanceTime(this.state.renderTime); - Scheduler.unstable_yieldValue( - 'FirstComponent:' + this.state.renderTime, - ); + Scheduler.log('FirstComponent:' + this.state.renderTime); return ; } } @@ -1026,9 +991,7 @@ describe(`onRender`, () => { render() { second = this; Scheduler.unstable_advanceTime(this.state.renderTime); - Scheduler.unstable_yieldValue( - 'SecondComponent:' + this.state.renderTime, - ); + Scheduler.log('SecondComponent:' + this.state.renderTime); return ; } } @@ -1046,7 +1009,7 @@ describe(`onRender`, () => { // Render everything initially. // This simulates a total of 14ms of actual render time. // The base render time is also 14ms for the initial render. - expect(Scheduler).toFlushAndYield([ + await waitForAll([ 'FirstComponent:1', 'Yield:4', 'SecondComponent:2', @@ -1065,14 +1028,10 @@ describe(`onRender`, () => { // Render a partially update, but don't finish. // This partial render will take 10ms of actual render time. - if (gate(flags => flags.enableSyncDefaultUpdates)) { - React.startTransition(() => { - first.setState({renderTime: 10}); - }); - } else { + React.startTransition(() => { first.setState({renderTime: 10}); - } - expect(Scheduler).toFlushAndYieldThrough(['FirstComponent:10']); + }); + await waitFor(['FirstComponent:10']); expect(callback).toHaveBeenCalledTimes(0); // Simulate time moving forward while frame is paused. @@ -1081,7 +1040,7 @@ describe(`onRender`, () => { // Interrupt with higher priority work. // This simulates a total of 37ms of actual render time. renderer.unstable_flushSync(() => second.setState({renderTime: 30})); - expect(Scheduler).toHaveYielded(['SecondComponent:30', 'Yield:7']); + assertLog(['SecondComponent:30', 'Yield:7']); // The actual time should include only the most recent render (37ms), // Because this greatly simplifies the commit phase logic. @@ -1105,7 +1064,7 @@ describe(`onRender`, () => { // The tree contains 42ms of base render time at this point, // Reflecting the most recent (longer) render durations. // TODO: This actual time should decrease by 10ms once the scheduler supports resuming. - expect(Scheduler).toFlushAndYield(['FirstComponent:10', 'Yield:4']); + await waitForAll(['FirstComponent:10', 'Yield:4']); expect(callback).toHaveBeenCalledTimes(1); call = callback.mock.calls[0]; expect(call[2]).toBe(14); // actual time @@ -1263,7 +1222,7 @@ describe(`onRender`, () => { ); }); - it('should reset the fiber stack correct after a "complete" phase error', () => { + it('should reset the fiber stack correct after a "complete" phase error', async () => { jest.resetModules(); loadModules({ @@ -1278,7 +1237,7 @@ describe(`onRender`, () => { hi , ); - expect(Scheduler).toFlushAndThrow('Error in host config.'); + await waitForThrow('Error in host config.'); // A similar case we've seen caused by an invariant in ReactDOM. // It didn't reproduce without a host component inside. @@ -1289,7 +1248,7 @@ describe(`onRender`, () => { , ); - expect(Scheduler).toFlushAndThrow('Error in host config.'); + await waitForThrow('Error in host config.'); // So long as the profiler timer's fiber stack is reset correctly, // Subsequent renders should not error. @@ -1298,7 +1257,7 @@ describe(`onRender`, () => { hi , ); - expect(Scheduler).toFlushWithoutYielding(); + await waitForAll([]); }); }); }); @@ -1619,7 +1578,7 @@ describe(`onCommit`, () => { expect(call[3]).toBe(1011); // commit start time (before mutations or effects) }); - it('should bubble time spent in layout effects to higher profilers', () => { + it('should bubble time spent in layout effects to higher profilers', async () => { const callback = jest.fn(); const ComponentWithEffects = ({cleanupDuration, duration, setCountRef}) => { @@ -1640,7 +1599,7 @@ describe(`onCommit`, () => { const setCountRef = React.createRef(null); let renderer = null; - act(() => { + await act(() => { renderer = ReactTestRenderer.create( @@ -1667,7 +1626,7 @@ describe(`onCommit`, () => { expect(call[2]).toBe(1010); // durations expect(call[3]).toBe(2); // commit start time (before mutations or effects) - act(() => setCountRef.current(count => count + 1)); + await act(() => setCountRef.current(count => count + 1)); expect(callback).toHaveBeenCalledTimes(2); @@ -1679,7 +1638,7 @@ describe(`onCommit`, () => { expect(call[2]).toBe(110); // durations expect(call[3]).toBe(1013); // commit start time (before mutations or effects) - act(() => { + await act(() => { renderer.update( @@ -1700,7 +1659,7 @@ describe(`onCommit`, () => { expect(call[3]).toBe(1124); // commit start time (before mutations or effects) }); - it('should properly report time in layout effects even when there are errors', () => { + it('should properly report time in layout effects even when there are errors', async () => { const callback = jest.fn(); class ErrorBoundary extends React.Component { @@ -1738,7 +1697,7 @@ describe(`onCommit`, () => { // Test an error that happens during an effect - act(() => { + await act(() => { ReactTestRenderer.create( { expect(call[3]).toBe(10110111); // commit start time (before mutations or effects) }); - it('should properly report time in layout effect cleanup functions even when there are errors', () => { + it('should properly report time in layout effect cleanup functions even when there are errors', async () => { const callback = jest.fn(); class ErrorBoundary extends React.Component { @@ -1824,7 +1783,7 @@ describe(`onCommit`, () => { let renderer = null; - act(() => { + await act(() => { renderer = ReactTestRenderer.create( { // Test an error that happens during an cleanup function - act(() => { + await act(() => { renderer.update( { loadModules(); }); - it('should report time spent in passive effects', () => { + it('should report time spent in passive effects', async () => { const callback = jest.fn(); const ComponentWithEffects = () => { @@ -1951,14 +1910,14 @@ describe(`onPostCommit`, () => { Scheduler.unstable_advanceTime(1); let renderer; - act(() => { + await act(() => { renderer = ReactTestRenderer.create( , ); }); - Scheduler.unstable_flushAll(); + await waitForAll([]); expect(callback).toHaveBeenCalledTimes(1); @@ -1972,14 +1931,14 @@ describe(`onPostCommit`, () => { Scheduler.unstable_advanceTime(1); - act(() => { + await act(() => { renderer.update( , ); }); - Scheduler.unstable_flushAll(); + await waitForAll([]); expect(callback).toHaveBeenCalledTimes(2); @@ -1993,12 +1952,12 @@ describe(`onPostCommit`, () => { Scheduler.unstable_advanceTime(1); - act(() => { + await act(() => { renderer.update( , ); }); - Scheduler.unstable_flushAll(); + await waitForAll([]); expect(callback).toHaveBeenCalledTimes(3); @@ -2015,7 +1974,7 @@ describe(`onPostCommit`, () => { expect(call[3]).toBe(12030); // commit start time (before mutations or effects) }); - it('should report time spent in passive effects with cascading renders', () => { + it('should report time spent in passive effects with cascading renders', async () => { const callback = jest.fn(); const ComponentWithEffects = () => { @@ -2035,7 +1994,7 @@ describe(`onPostCommit`, () => { Scheduler.unstable_advanceTime(1); - act(() => { + await act(() => { ReactTestRenderer.create( @@ -2062,7 +2021,7 @@ describe(`onPostCommit`, () => { expect(call[3]).toBe(2011); // commit start time (before mutations or effects) }); - it('should bubble time spent in effects to higher profilers', () => { + it('should bubble time spent in effects to higher profilers', async () => { const callback = jest.fn(); const ComponentWithEffects = ({cleanupDuration, duration, setCountRef}) => { @@ -2083,7 +2042,7 @@ describe(`onPostCommit`, () => { const setCountRef = React.createRef(null); let renderer = null; - act(() => { + await act(() => { renderer = ReactTestRenderer.create( @@ -2110,7 +2069,7 @@ describe(`onPostCommit`, () => { expect(call[2]).toBe(1010); // durations expect(call[3]).toBe(2); // commit start time (before mutations or effects) - act(() => setCountRef.current(count => count + 1)); + await act(() => setCountRef.current(count => count + 1)); expect(callback).toHaveBeenCalledTimes(2); @@ -2122,7 +2081,7 @@ describe(`onPostCommit`, () => { expect(call[2]).toBe(110); // durations expect(call[3]).toBe(1013); // commit start time (before mutations or effects) - act(() => { + await act(() => { renderer.update( @@ -2143,7 +2102,7 @@ describe(`onPostCommit`, () => { expect(call[3]).toBe(1124); // commit start time (before mutations or effects) }); - it('should properly report time in passive effects even when there are errors', () => { + it('should properly report time in passive effects even when there are errors', async () => { const callback = jest.fn(); class ErrorBoundary extends React.Component { @@ -2181,7 +2140,7 @@ describe(`onPostCommit`, () => { // Test an error that happens during an effect - act(() => { + await act(() => { ReactTestRenderer.create( { expect(call[3]).toBe(10110111); // commit start time (before mutations or effects) }); - it('should properly report time in passive effect cleanup functions even when there are errors', () => { + it('should properly report time in passive effect cleanup functions even when there are errors', async () => { const callback = jest.fn(); class ErrorBoundary extends React.Component { @@ -2268,7 +2227,7 @@ describe(`onPostCommit`, () => { let renderer = null; - act(() => { + await act(() => { renderer = ReactTestRenderer.create( { // Test an error that happens during an cleanup function - act(() => { + await act(() => { renderer.update( { expect(onNestedUpdateScheduled).not.toHaveBeenCalled(); }); - it('is called when a function component schedules an update during a layout effect', () => { + it('is called when a function component schedules an update during a layout effect', async () => { function Component() { const [didMount, setDidMount] = React.useState(false); React.useLayoutEffect(() => { setDidMount(true); }, []); - Scheduler.unstable_yieldValue(`Component:${didMount}`); + Scheduler.log(`Component:${didMount}`); return didMount; } const onNestedUpdateScheduled = jest.fn(); - act(() => { + await act(() => { ReactNoop.render( { ); }); - expect(Scheduler).toHaveYielded(['Component:false', 'Component:true']); + assertLog(['Component:false', 'Component:true']); expect(onNestedUpdateScheduled).toHaveBeenCalledTimes(1); expect(onNestedUpdateScheduled.mock.calls[0][0]).toBe('test'); }); - it('is called when a function component schedules a batched update during a layout effect', () => { + it('is called when a function component schedules a batched update during a layout effect', async () => { function Component() { const [didMount, setDidMount] = React.useState(false); React.useLayoutEffect(() => { @@ -2450,7 +2409,7 @@ describe(`onNestedUpdateScheduled`, () => { setDidMount(true); }); }, []); - Scheduler.unstable_yieldValue(`Component:${didMount}`); + Scheduler.log(`Component:${didMount}`); return didMount; } @@ -2465,7 +2424,7 @@ describe(`onNestedUpdateScheduled`, () => { , ); - expect(Scheduler).toFlushAndYield(['Component:false', 'Component:true']); + await waitForAll(['Component:false', 'Component:true']); expect(onRender).toHaveBeenCalledTimes(2); expect(onRender.mock.calls[0][1]).toBe('mount'); @@ -2475,20 +2434,20 @@ describe(`onNestedUpdateScheduled`, () => { expect(onNestedUpdateScheduled.mock.calls[0][0]).toBe('root'); }); - it('bubbles up and calls all ancestor Profilers', () => { + it('bubbles up and calls all ancestor Profilers', async () => { function Component() { const [didMount, setDidMount] = React.useState(false); React.useLayoutEffect(() => { setDidMount(true); }, []); - Scheduler.unstable_yieldValue(`Component:${didMount}`); + Scheduler.log(`Component:${didMount}`); return didMount; } const onNestedUpdateScheduledOne = jest.fn(); const onNestedUpdateScheduledTwo = jest.fn(); const onNestedUpdateScheduledThree = jest.fn(); - act(() => { + await act(() => { ReactNoop.render( { ); }); - expect(Scheduler).toHaveYielded(['Component:false', 'Component:true']); + assertLog(['Component:false', 'Component:true']); expect(onNestedUpdateScheduledOne).toHaveBeenCalledTimes(1); expect(onNestedUpdateScheduledOne.mock.calls[0][0]).toBe('one'); expect(onNestedUpdateScheduledTwo).toHaveBeenCalledTimes(1); @@ -2516,13 +2475,13 @@ describe(`onNestedUpdateScheduled`, () => { expect(onNestedUpdateScheduledThree).not.toHaveBeenCalled(); }); - it('is not called when an update is scheduled for another doort during a layout effect', () => { + it('is not called when an update is scheduled for another doort during a layout effect', async () => { const setStateRef = React.createRef(null); function ComponentRootOne() { const [state, setState] = React.useState(false); setStateRef.current = setState; - Scheduler.unstable_yieldValue(`ComponentRootOne:${state}`); + Scheduler.log(`ComponentRootOne:${state}`); return state; } @@ -2530,13 +2489,13 @@ describe(`onNestedUpdateScheduled`, () => { React.useLayoutEffect(() => { setStateRef.current(true); }, []); - Scheduler.unstable_yieldValue('ComponentRootTwo'); + Scheduler.log('ComponentRootTwo'); return null; } const onNestedUpdateScheduled = jest.fn(); - act(() => { + await act(() => { ReactNoop.renderToRootWithID( { ); }); - expect(Scheduler).toHaveYielded([ + assertLog([ 'ComponentRootOne:false', 'ComponentRootTwo', 'ComponentRootOne:true', @@ -2564,19 +2523,19 @@ describe(`onNestedUpdateScheduled`, () => { expect(onNestedUpdateScheduled).not.toHaveBeenCalled(); }); - it('is not called when a function component schedules an update during a passive effect', () => { + it('is not called when a function component schedules an update during a passive effect', async () => { function Component() { const [didMount, setDidMount] = React.useState(false); React.useEffect(() => { setDidMount(true); }, []); - Scheduler.unstable_yieldValue(`Component:${didMount}`); + Scheduler.log(`Component:${didMount}`); return didMount; } const onNestedUpdateScheduled = jest.fn(); - act(() => { + await act(() => { ReactNoop.render( { ); }); - expect(Scheduler).toHaveYielded(['Component:false', 'Component:true']); + assertLog(['Component:false', 'Component:true']); expect(onNestedUpdateScheduled).not.toHaveBeenCalled(); }); - it('is not called when a function component schedules an update outside of render', () => { + it('is not called when a function component schedules an update outside of render', async () => { const updateFnRef = React.createRef(null); function Component() { const [state, setState] = React.useState(false); updateFnRef.current = () => setState(true); - Scheduler.unstable_yieldValue(`Component:${state}`); + Scheduler.log(`Component:${state}`); return state; } const onNestedUpdateScheduled = jest.fn(); - act(() => { + await act(() => { ReactNoop.render( { , ); }); - expect(Scheduler).toHaveYielded(['Component:false']); + assertLog(['Component:false']); - act(() => { + await act(() => { updateFnRef.current(); }); - expect(Scheduler).toHaveYielded(['Component:true']); + assertLog(['Component:true']); expect(onNestedUpdateScheduled).not.toHaveBeenCalled(); }); - it('it is not called when a component schedules an update during render', () => { + it('it is not called when a component schedules an update during render', async () => { function Component() { const [state, setState] = React.useState(false); if (state === false) { setState(true); } - Scheduler.unstable_yieldValue(`Component:${state}`); + Scheduler.log(`Component:${state}`); return state; } const onNestedUpdateScheduled = jest.fn(); - act(() => { + await act(() => { ReactNoop.render( { ); }); - expect(Scheduler).toHaveYielded(['Component:false', 'Component:true']); + assertLog(['Component:false', 'Component:true']); expect(onNestedUpdateScheduled).not.toHaveBeenCalled(); }); - it('it is called when a component schedules an update from a ref callback', () => { + it('it is called when a component schedules an update from a ref callback', async () => { function Component({mountChild}) { const [refAttached, setRefAttached] = React.useState(false); const [refDetached, setRefDetached] = React.useState(false); @@ -2657,13 +2616,13 @@ describe(`onNestedUpdateScheduled`, () => { setRefDetached(true); } }, []); - Scheduler.unstable_yieldValue(`Component:${refAttached}:${refDetached}`); + Scheduler.log(`Component:${refAttached}:${refDetached}`); return mountChild ?
    : null; } const onNestedUpdateScheduled = jest.fn(); - act(() => { + await act(() => { ReactNoop.render( { ); }); - expect(Scheduler).toHaveYielded([ - 'Component:false:false', - 'Component:true:false', - ]); + assertLog(['Component:false:false', 'Component:true:false']); expect(onNestedUpdateScheduled).toHaveBeenCalledTimes(1); expect(onNestedUpdateScheduled.mock.calls[0][0]).toBe('test'); - act(() => { + await act(() => { ReactNoop.render( { ); }); - expect(Scheduler).toHaveYielded([ - 'Component:true:false', - 'Component:true:true', - ]); + assertLog(['Component:true:false', 'Component:true:true']); expect(onNestedUpdateScheduled).toHaveBeenCalledTimes(2); expect(onNestedUpdateScheduled.mock.calls[1][0]).toBe('test'); }); - it('is called when a class component schedules an update from the componentDidMount lifecycles', () => { + it('is called when a class component schedules an update from the componentDidMount lifecycles', async () => { class Component extends React.Component { state = { value: false, @@ -2708,14 +2661,14 @@ describe(`onNestedUpdateScheduled`, () => { } render() { const {value} = this.state; - Scheduler.unstable_yieldValue(`Component:${value}`); + Scheduler.log(`Component:${value}`); return value; } } const onNestedUpdateScheduled = jest.fn(); - act(() => { + await act(() => { ReactNoop.render( { ); }); - expect(Scheduler).toHaveYielded(['Component:false', 'Component:true']); + assertLog(['Component:false', 'Component:true']); expect(onNestedUpdateScheduled).toHaveBeenCalledTimes(1); expect(onNestedUpdateScheduled.mock.calls[0][0]).toBe('test'); }); - it('is called when a class component schedules an update from the componentDidUpdate lifecycles', () => { + it('is called when a class component schedules an update from the componentDidUpdate lifecycles', async () => { class Component extends React.Component { state = { nestedUpdateSheduled: false, @@ -2746,7 +2699,7 @@ describe(`onNestedUpdateScheduled`, () => { render() { const {scheduleNestedUpdate} = this.props; const {nestedUpdateSheduled} = this.state; - Scheduler.unstable_yieldValue( + Scheduler.log( `Component:${scheduleNestedUpdate}:${nestedUpdateSheduled}`, ); return nestedUpdateSheduled; @@ -2755,7 +2708,7 @@ describe(`onNestedUpdateScheduled`, () => { const onNestedUpdateScheduled = jest.fn(); - act(() => { + await act(() => { ReactNoop.render( { , ); }); - expect(Scheduler).toHaveYielded(['Component:false:false']); + assertLog(['Component:false:false']); expect(onNestedUpdateScheduled).not.toHaveBeenCalled(); - act(() => { + await act(() => { ReactNoop.render( { ); }); - expect(Scheduler).toHaveYielded([ - 'Component:true:false', - 'Component:true:true', - ]); + assertLog(['Component:true:false', 'Component:true:true']); expect(onNestedUpdateScheduled).toHaveBeenCalledTimes(1); expect(onNestedUpdateScheduled.mock.calls[0][0]).toBe('test'); }); - it('is not called when a class component schedules an update outside of render', () => { + it('is not called when a class component schedules an update outside of render', async () => { const updateFnRef = React.createRef(null); class Component extends React.Component { @@ -2795,14 +2745,14 @@ describe(`onNestedUpdateScheduled`, () => { render() { const {value} = this.state; updateFnRef.current = () => this.setState({value: true}); - Scheduler.unstable_yieldValue(`Component:${value}`); + Scheduler.log(`Component:${value}`); return value; } } const onNestedUpdateScheduled = jest.fn(); - act(() => { + await act(() => { ReactNoop.render( { , ); }); - expect(Scheduler).toHaveYielded(['Component:false']); + assertLog(['Component:false']); - act(() => { + await act(() => { updateFnRef.current(); }); - expect(Scheduler).toHaveYielded(['Component:true']); + assertLog(['Component:true']); expect(onNestedUpdateScheduled).not.toHaveBeenCalled(); }); diff --git a/packages/react/src/__tests__/ReactProfilerDevToolsIntegration-test.internal.js b/packages/react/src/__tests__/ReactProfilerDevToolsIntegration-test.internal.js index a0fef0cb2313..d28e2ad8de64 100644 --- a/packages/react/src/__tests__/ReactProfilerDevToolsIntegration-test.internal.js +++ b/packages/react/src/__tests__/ReactProfilerDevToolsIntegration-test.internal.js @@ -17,6 +17,8 @@ describe('ReactProfiler DevTools integration', () => { let Scheduler; let AdvanceTime; let hook; + let waitForAll; + let waitFor; beforeEach(() => { global.__REACT_DEVTOOLS_GLOBAL_HOOK__ = hook = { @@ -34,6 +36,10 @@ describe('ReactProfiler DevTools integration', () => { React = require('react'); ReactTestRenderer = require('react-test-renderer'); + const InternalTestUtils = require('internal-test-utils'); + waitForAll = InternalTestUtils.waitForAll; + waitFor = InternalTestUtils.waitFor; + AdvanceTime = class extends React.Component { static defaultProps = { byAmount: 10, @@ -138,9 +144,9 @@ describe('ReactProfiler DevTools integration', () => { ).toBe(7); }); - it('regression test: #17159', () => { + it('regression test: #17159', async () => { function Text({text}) { - Scheduler.unstable_yieldValue(text); + Scheduler.log(text); return text; } @@ -148,25 +154,21 @@ describe('ReactProfiler DevTools integration', () => { // Commit something root.update(); - expect(Scheduler).toFlushAndYield(['A']); + await waitForAll(['A']); expect(root).toMatchRenderedOutput('A'); // Advance time by many seconds, larger than the default expiration time // for updates. Scheduler.unstable_advanceTime(10000); // Schedule an update. - if (gate(flags => flags.enableSyncDefaultUpdates)) { - React.startTransition(() => { - root.update(); - }); - } else { + React.startTransition(() => { root.update(); - } + }); // Update B should not instantly expire. - expect(Scheduler).toFlushAndYieldThrough([]); + await waitFor([]); - expect(Scheduler).toFlushAndYield(['B']); + await waitForAll(['B']); expect(root).toMatchRenderedOutput('B'); }); }); diff --git a/packages/react/src/__tests__/ReactStartTransition-test.js b/packages/react/src/__tests__/ReactStartTransition-test.js index d02a221afb48..9e689ac6e710 100644 --- a/packages/react/src/__tests__/ReactStartTransition-test.js +++ b/packages/react/src/__tests__/ReactStartTransition-test.js @@ -22,12 +22,12 @@ describe('ReactStartTransition', () => { jest.resetModules(); React = require('react'); ReactTestRenderer = require('react-test-renderer'); - act = require('jest-react').act; + act = require('internal-test-utils').act; useState = React.useState; useTransition = React.useTransition; }); - it('Warns if a suspicious number of fibers are updated inside startTransition', () => { + it('Warns if a suspicious number of fibers are updated inside startTransition', async () => { const subs = new Set(); const useUserSpaceSubscription = () => { const setState = useState(0)[1]; @@ -47,14 +47,14 @@ describe('ReactStartTransition', () => { return null; }; - act(() => { + await act(() => { ReactTestRenderer.create(, { unstable_isConcurrent: true, }); }); - expect(() => { - act(() => { + await expect(async () => { + await act(() => { React.startTransition(() => { subs.forEach(setState => { setState(state => state + 1); @@ -70,8 +70,8 @@ describe('ReactStartTransition', () => { {withoutStack: true}, ); - expect(() => { - act(() => { + await expect(async () => { + await act(() => { triggerHookTransition(() => { subs.forEach(setState => { setState(state => state + 1); diff --git a/packages/react/src/__tests__/ReactStrictMode-test.internal.js b/packages/react/src/__tests__/ReactStrictMode-test.internal.js index 9661d0f72e35..48efe5e7d5fd 100644 --- a/packages/react/src/__tests__/ReactStrictMode-test.internal.js +++ b/packages/react/src/__tests__/ReactStrictMode-test.internal.js @@ -19,7 +19,7 @@ describe('ReactStrictMode', () => { React = require('react'); ReactDOMClient = require('react-dom/client'); - act = require('jest-react').act; + act = require('internal-test-utils').act; }); describe('levels', () => { @@ -45,8 +45,8 @@ describe('ReactStrictMode', () => { return null; } - it('should default to not strict', () => { - act(() => { + it('should default to not strict', async () => { + await act(() => { const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); root.render(); @@ -60,8 +60,8 @@ describe('ReactStrictMode', () => { }); if (__DEV__) { - it('should support enabling strict mode via createRoot option', () => { - act(() => { + it('should support enabling strict mode via createRoot option', async () => { + await act(() => { const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container, { unstable_strictMode: true, @@ -81,8 +81,8 @@ describe('ReactStrictMode', () => { ]); }); - it('should include legacy + strict effects mode', () => { - act(() => { + it('should include legacy + strict effects mode', async () => { + await act(() => { const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); root.render( @@ -104,8 +104,8 @@ describe('ReactStrictMode', () => { ]); }); - it('should allow level to be increased with nesting', () => { - act(() => { + it('should allow level to be increased with nesting', async () => { + await act(() => { const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); root.render( diff --git a/packages/react/src/__tests__/ReactStrictMode-test.js b/packages/react/src/__tests__/ReactStrictMode-test.js index b41497ae3ab7..c0d392a61f2d 100644 --- a/packages/react/src/__tests__/ReactStrictMode-test.js +++ b/packages/react/src/__tests__/ReactStrictMode-test.js @@ -13,7 +13,6 @@ let React; let ReactDOM; let ReactDOMClient; let ReactDOMServer; -let Scheduler; let PropTypes; let act; let useMemo; @@ -29,7 +28,7 @@ describe('ReactStrictMode', () => { ReactDOM = require('react-dom'); ReactDOMClient = require('react-dom/client'); ReactDOMServer = require('react-dom/server'); - act = require('jest-react').act; + act = require('internal-test-utils').act; useMemo = React.useMemo; useState = React.useState; useReducer = React.useReducer; @@ -525,10 +524,10 @@ describe('Concurrent Mode', () => { React = require('react'); ReactDOM = require('react-dom'); ReactDOMClient = require('react-dom/client'); - Scheduler = require('scheduler'); + act = require('internal-test-utils').act; }); - it('should warn about unsafe legacy lifecycle methods anywhere in a StrictMode tree', () => { + it('should warn about unsafe legacy lifecycle methods anywhere in a StrictMode tree', async () => { function StrictRoot() { return ( @@ -571,8 +570,9 @@ describe('Concurrent Mode', () => { const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); - root.render(); - expect(() => Scheduler.unstable_flushAll()).toErrorDev( + await expect( + async () => await act(() => root.render()), + ).toErrorDev( [ /* eslint-disable max-len */ `Warning: Using UNSAFE_componentWillMount in strict mode is not recommended and may indicate bugs in your code. See https://reactjs.org/link/unsafe-component-lifecycles for details. @@ -597,11 +597,10 @@ Please update the following components: App`, ); // Dedupe - root.render(); - Scheduler.unstable_flushAll(); + await act(() => root.render()); }); - it('should coalesce warnings by lifecycle name', () => { + it('should coalesce warnings by lifecycle name', async () => { function StrictRoot() { return ( @@ -633,10 +632,11 @@ Please update the following components: App`, const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); - root.render(); - expect(() => { - expect(() => Scheduler.unstable_flushAll()).toErrorDev( + await expect(async () => { + await expect( + async () => await act(() => root.render()), + ).toErrorDev( [ /* eslint-disable max-len */ `Warning: Using UNSAFE_componentWillMount in strict mode is not recommended and may indicate bugs in your code. See https://reactjs.org/link/unsafe-component-lifecycles for details. @@ -686,11 +686,10 @@ Please update the following components: Parent`, {withoutStack: true}, ); // Dedupe - root.render(); - Scheduler.unstable_flushAll(); + await act(() => root.render()); }); - it('should warn about components not present during the initial render', () => { + it('should warn about components not present during the initial render', async () => { function StrictRoot({foo}) { return {foo ? : }; } @@ -709,27 +708,23 @@ Please update the following components: Parent`, const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); - root.render(); - expect(() => - Scheduler.unstable_flushAll(), - ).toErrorDev( + await expect(async () => { + await act(() => root.render()); + }).toErrorDev( 'Using UNSAFE_componentWillMount in strict mode is not recommended', {withoutStack: true}, ); - root.render(); - expect(() => - Scheduler.unstable_flushAll(), - ).toErrorDev( + await expect(async () => { + await act(() => root.render()); + }).toErrorDev( 'Using UNSAFE_componentWillMount in strict mode is not recommended', {withoutStack: true}, ); // Dedupe - root.render(); - Scheduler.unstable_flushAll(); - root.render(); - Scheduler.unstable_flushAll(); + await act(() => root.render()); + await act(() => root.render()); }); it('should also warn inside of "strict" mode trees', () => { @@ -770,9 +765,7 @@ Please update the following components: Parent`, const container = document.createElement('div'); - expect(() => - ReactDOM.render(, container), - ).toErrorDev( + expect(() => ReactDOM.render(, container)).toErrorDev( 'Using UNSAFE_componentWillReceiveProps in strict mode is not recommended', {withoutStack: true}, ); @@ -926,18 +919,11 @@ describe('string refs', () => { expect(() => { ReactDOM.render(, container); }).toErrorDev( - ReactFeatureFlags.warnAboutStringRefs - ? 'Warning: Component "StrictMode" contains the string ref "somestring". ' + - 'Support for string refs will be removed in a future major release. ' + - 'We recommend using useRef() or createRef() instead. ' + - 'Learn more about using refs safely here: https://reactjs.org/link/strict-mode-string-ref\n' + - ' in OuterComponent (at **)' - : 'Warning: A string ref, "somestring", has been found within a strict mode tree. ' + - 'String refs are a source of potential bugs and should be avoided. ' + - 'We recommend using useRef() or createRef() instead. ' + - 'Learn more about using refs safely here: ' + - 'https://reactjs.org/link/strict-mode-string-ref\n' + - ' in OuterComponent (at **)', + 'Warning: Component "StrictMode" contains the string ref "somestring". ' + + 'Support for string refs will be removed in a future major release. ' + + 'We recommend using useRef() or createRef() instead. ' + + 'Learn more about using refs safely here: https://reactjs.org/link/strict-mode-string-ref\n' + + ' in OuterComponent (at **)', ); // Dedup @@ -973,20 +959,12 @@ describe('string refs', () => { expect(() => { ReactDOM.render(, container); }).toErrorDev( - ReactFeatureFlags.warnAboutStringRefs - ? 'Warning: Component "InnerComponent" contains the string ref "somestring". ' + - 'Support for string refs will be removed in a future major release. ' + - 'We recommend using useRef() or createRef() instead. ' + - 'Learn more about using refs safely here: https://reactjs.org/link/strict-mode-string-ref\n' + - ' in InnerComponent (at **)\n' + - ' in OuterComponent (at **)' - : 'Warning: A string ref, "somestring", has been found within a strict mode tree. ' + - 'String refs are a source of potential bugs and should be avoided. ' + - 'We recommend using useRef() or createRef() instead. ' + - 'Learn more about using refs safely here: ' + - 'https://reactjs.org/link/strict-mode-string-ref\n' + - ' in InnerComponent (at **)\n' + - ' in OuterComponent (at **)', + 'Warning: Component "InnerComponent" contains the string ref "somestring". ' + + 'Support for string refs will be removed in a future major release. ' + + 'We recommend using useRef() or createRef() instead. ' + + 'Learn more about using refs safely here: https://reactjs.org/link/strict-mode-string-ref\n' + + ' in InnerComponent (at **)\n' + + ' in OuterComponent (at **)', ); // Dedup @@ -1003,6 +981,11 @@ describe('context legacy', () => { PropTypes = require('prop-types'); }); + afterEach(() => { + jest.restoreAllMocks(); + }); + + // @gate !disableLegacyContext || !__DEV__ it('should warn if the legacy context API have been used in strict mode', () => { class LegacyContextProvider extends React.Component { getChildContext() { diff --git a/packages/react/src/__tests__/ReactTypeScriptClass-test.ts b/packages/react/src/__tests__/ReactTypeScriptClass-test.ts index 39304e9d6f2a..6f0dc668f5be 100644 --- a/packages/react/src/__tests__/ReactTypeScriptClass-test.ts +++ b/packages/react/src/__tests__/ReactTypeScriptClass-test.ts @@ -16,8 +16,7 @@ import ReactDOM = require('react-dom'); import ReactDOMClient = require('react-dom/client'); import ReactDOMTestUtils = require('react-dom/test-utils'); import PropTypes = require('prop-types'); -import internalAct = require('jest-react'); -import ReactFeatureFlags = require('shared/ReactFeatureFlags') +import ReactFeatureFlags = require('shared/ReactFeatureFlags'); // Before Each @@ -25,7 +24,6 @@ let container; let root; let attachedListener = null; let renderedName = null; -let act = internalAct.act; class Inner extends React.Component { getName() { @@ -39,7 +37,7 @@ class Inner extends React.Component { } function test(element, expectedTag, expectedClassName) { - act(() => root.render(element)); + ReactDOM.flushSync(() => root.render(element)); expect(container.firstChild).not.toBeNull(); expect(container.firstChild.tagName).toBe(expectedTag); expect(container.firstChild.className).toBe(expectedClassName); @@ -332,7 +330,7 @@ describe('ReactTypeScriptClass', function() { it('throws if no render function is defined', function() { expect(() => { expect(() => - act(() => root.render(React.createElement(Empty))) + ReactDOM.flushSync(() => root.render(React.createElement(Empty))) ).toThrow(); }).toErrorDev([ // A failed component renders four times in DEV in concurrent mode @@ -367,7 +365,7 @@ describe('ReactTypeScriptClass', function() { 'DIV', 'foo' ); - act(() => ref.current.changeState()); + ReactDOM.flushSync(() => ref.current.changeState()); test(React.createElement(StateBasedOnProps), 'SPAN', 'bar'); }); @@ -402,7 +400,7 @@ describe('ReactTypeScriptClass', function() { } } expect(function() { - act(() => + ReactDOM.flushSync(() => root.render(React.createElement(Foo, {foo: 'foo'})) ); }).toErrorDev( @@ -421,7 +419,7 @@ describe('ReactTypeScriptClass', function() { } } expect(function() { - act(() => + ReactDOM.flushSync(() => root.render(React.createElement(Foo, {foo: 'foo'})) ); }).toErrorDev( @@ -438,7 +436,7 @@ describe('ReactTypeScriptClass', function() { } } expect(function() { - act(() => + ReactDOM.flushSync(() => root.render(React.createElement(Foo, {foo: 'foo'})) ); }).toErrorDev( @@ -462,7 +460,7 @@ describe('ReactTypeScriptClass', function() { } } expect(function() { - act(() => + ReactDOM.flushSync(() => root.render(React.createElement(Foo, {foo: 'foo'})) ); }).toErrorDev( @@ -514,16 +512,16 @@ describe('ReactTypeScriptClass', function() { test(React.createElement(Foo, {update: true}), 'DIV', 'updated'); }); - it('renders based on context in the constructor', function() { - test(React.createElement(ProvideChildContextTypes), 'SPAN', 'foo'); - }); + if (!ReactFeatureFlags.disableLegacyContext) { + it('renders based on context in the constructor', function() { + test(React.createElement(ProvideChildContextTypes), 'SPAN', 'foo'); + }); + } it('renders only once when setting state in componentWillMount', function() { renderCount = 0; test(React.createElement(RenderOnce, {initialValue: 'foo'}), 'SPAN', 'bar'); - // This is broken with deferRenderPhaseUpdateToNextBatch flag on. - // We can't use the gate feature in TypeScript. - expect(renderCount).toBe(global.__WWW__ && !global.__VARIANT__ ? 2 : 1); + expect(renderCount).toBe(1); }); it('should warn with non-object in the initial state property', function() { @@ -548,7 +546,7 @@ describe('ReactTypeScriptClass', function() { 'DIV', 'foo' ); - act(() => attachedListener()); + ReactDOM.flushSync(() => attachedListener()); expect(renderedName).toBe('bar'); }); @@ -567,7 +565,7 @@ describe('ReactTypeScriptClass', function() { 'DIV', 'foo' ); - act(() => attachedListener()); + ReactDOM.flushSync(() => attachedListener()); expect(renderedName).toBe('bar'); }); @@ -591,31 +589,33 @@ describe('ReactTypeScriptClass', function() { {}, ]); lifeCycles = []; // reset - act(() => root.unmount(container)); + ReactDOM.flushSync(() => root.unmount(container)); expect(lifeCycles).toEqual(['will-unmount']); }); - it( - 'warns when classic properties are defined on the instance, ' + - 'but does not invoke them.', - function() { - getInitialStateWasCalled = false; - getDefaultPropsWasCalled = false; - expect(() => - test(React.createElement(ClassicProperties), 'SPAN', 'foo') - ).toErrorDev([ - 'getInitialState was defined on ClassicProperties, ' + - 'a plain JavaScript class.', - 'getDefaultProps was defined on ClassicProperties, ' + - 'a plain JavaScript class.', - 'propTypes was defined as an instance property on ClassicProperties.', - 'contextTypes was defined as an instance property on ClassicProperties.', - 'contextType was defined as an instance property on ClassicProperties.', - ]); - expect(getInitialStateWasCalled).toBe(false); - expect(getDefaultPropsWasCalled).toBe(false); - } - ); + if (!ReactFeatureFlags.disableLegacyContext) { + it( + 'warns when classic properties are defined on the instance, ' + + 'but does not invoke them.', + function() { + getInitialStateWasCalled = false; + getDefaultPropsWasCalled = false; + expect(() => + test(React.createElement(ClassicProperties), 'SPAN', 'foo') + ).toErrorDev([ + 'getInitialState was defined on ClassicProperties, ' + + 'a plain JavaScript class.', + 'getDefaultProps was defined on ClassicProperties, ' + + 'a plain JavaScript class.', + 'propTypes was defined as an instance property on ClassicProperties.', + 'contextTypes was defined as an instance property on ClassicProperties.', + 'contextType was defined as an instance property on ClassicProperties.', + ]); + expect(getInitialStateWasCalled).toBe(false); + expect(getDefaultPropsWasCalled).toBe(false); + } + ); + } it( 'does not warn about getInitialState() on class components ' + @@ -683,25 +683,23 @@ describe('ReactTypeScriptClass', function() { ); }); - it('supports this.context passed via getChildContext', function() { - test(React.createElement(ProvideContext), 'DIV', 'bar-through-context'); - }); + if (!ReactFeatureFlags.disableLegacyContext) { + it('supports this.context passed via getChildContext', function() { + test(React.createElement(ProvideContext), 'DIV', 'bar-through-context'); + }); + } it('supports string refs', function() { const ref = React.createRef(); expect(() => { test(React.createElement(ClassicRefs, {ref: ref}), 'DIV', 'foo'); - }).toErrorDev( - ReactFeatureFlags.warnAboutStringRefs - ? [ - 'Warning: Component "ClassicRefs" contains the string ref "inner". ' + - 'Support for string refs will be removed in a future major release. ' + - 'We recommend using useRef() or createRef() instead. ' + - 'Learn more about using refs safely here: https://reactjs.org/link/strict-mode-string-ref\n' + - ' in ClassicRefs (at **)', - ] - : [], - ); + }).toErrorDev([ + 'Warning: Component "ClassicRefs" contains the string ref "inner". ' + + 'Support for string refs will be removed in a future major release. ' + + 'We recommend using useRef() or createRef() instead. ' + + 'Learn more about using refs safely here: https://reactjs.org/link/strict-mode-string-ref\n' + + ' in ClassicRefs (at **)', + ]); expect(ref.current.refs.inner.getName()).toBe('foo'); }); diff --git a/packages/react/src/__tests__/__snapshots__/ReactProfiler-test.internal.js.snap b/packages/react/src/__tests__/__snapshots__/ReactProfiler-test.internal.js.snap index 35da4c29611a..6ea6dacc4c81 100644 --- a/packages/react/src/__tests__/__snapshots__/ReactProfiler-test.internal.js.snap +++ b/packages/react/src/__tests__/__snapshots__/ReactProfiler-test.internal.js.snap @@ -19,7 +19,7 @@ exports[`Profiler works in profiling and non-profiling bundles enableProfilerTim exports[`Profiler works in profiling and non-profiling bundles enableProfilerTimer:disabled should support an empty Profiler (with no children) 2`] = `
    `; exports[`Profiler works in profiling and non-profiling bundles enableProfilerTimer:disabled should support nested Profilers 1`] = ` -Array [ +[
    outer function component
    , @@ -51,7 +51,7 @@ exports[`Profiler works in profiling and non-profiling bundles enableProfilerTim exports[`Profiler works in profiling and non-profiling bundles enableProfilerTimer:enabled should support an empty Profiler (with no children) 2`] = `
    `; exports[`Profiler works in profiling and non-profiling bundles enableProfilerTimer:enabled should support nested Profilers 1`] = ` -Array [ +[
    outer function component
    , diff --git a/packages/react/src/__tests__/createReactClassIntegration-test.js b/packages/react/src/__tests__/createReactClassIntegration-test.js index 00d7b87a15fc..b907fafea64f 100644 --- a/packages/react/src/__tests__/createReactClassIntegration-test.js +++ b/packages/react/src/__tests__/createReactClassIntegration-test.js @@ -30,7 +30,7 @@ describe('create-react-class-integration', () => { }); it('should throw when `render` is not specified', () => { - expect(function() { + expect(function () { createReactClass({}); }).toThrowError( 'createClass(...): Class specification must implement a `render` method.', @@ -43,7 +43,7 @@ describe('create-react-class-integration', () => { propTypes: { value: propValidator, }, - render: function() { + render: function () { return
    ; }, }); @@ -59,7 +59,7 @@ describe('create-react-class-integration', () => { propTypes: { prop: null, }, - render: function() { + render: function () { return {this.props.prop}; }, }), @@ -77,7 +77,7 @@ describe('create-react-class-integration', () => { contextTypes: { prop: null, }, - render: function() { + render: function () { return {this.props.prop}; }, }), @@ -95,7 +95,7 @@ describe('create-react-class-integration', () => { childContextTypes: { prop: null, }, - render: function() { + render: function () { return {this.props.prop}; }, }), @@ -109,10 +109,10 @@ describe('create-react-class-integration', () => { it('should warn when misspelling shouldComponentUpdate', () => { expect(() => createReactClass({ - componentShouldUpdate: function() { + componentShouldUpdate: function () { return false; }, - render: function() { + render: function () { return
    ; }, }), @@ -126,10 +126,10 @@ describe('create-react-class-integration', () => { expect(() => createReactClass({ displayName: 'NamedComponent', - componentShouldUpdate: function() { + componentShouldUpdate: function () { return false; }, - render: function() { + render: function () { return
    ; }, }), @@ -144,10 +144,10 @@ describe('create-react-class-integration', () => { it('should warn when misspelling componentWillReceiveProps', () => { expect(() => createReactClass({ - componentWillRecieveProps: function() { + componentWillRecieveProps: function () { return false; }, - render: function() { + render: function () { return
    ; }, }), @@ -161,10 +161,10 @@ describe('create-react-class-integration', () => { it('should warn when misspelling UNSAFE_componentWillReceiveProps', () => { expect(() => createReactClass({ - UNSAFE_componentWillRecieveProps: function() { + UNSAFE_componentWillRecieveProps: function () { return false; }, - render: function() { + render: function () { return
    ; }, }), @@ -176,17 +176,17 @@ describe('create-react-class-integration', () => { }); it('should throw if a reserved property is in statics', () => { - expect(function() { + expect(function () { createReactClass({ statics: { - getDefaultProps: function() { + getDefaultProps: function () { return { foo: 0, }; }, }, - render: function() { + render: function () { return ; }, }); @@ -212,7 +212,7 @@ describe('create-react-class-integration', () => { childContextTypes: { foo: PropTypes.string, }, - render: function() { + render: function () { return
    ; }, }), @@ -235,12 +235,12 @@ describe('create-react-class-integration', () => { def: 0, ghi: null, jkl: 'mno', - pqr: function() { + pqr: function () { return this; }, }, - render: function() { + render: function () { return ; }, }); @@ -260,12 +260,12 @@ describe('create-react-class-integration', () => { it('should work with object getInitialState() return values', () => { const Component = createReactClass({ - getInitialState: function() { + getInitialState: function () { return { occupation: 'clown', }; }, - render: function() { + render: function () { return ; }, }); @@ -279,7 +279,7 @@ describe('create-react-class-integration', () => { getInitialState() { return {}; }, - render: function() { + render: function () { return ; }, }); @@ -291,6 +291,7 @@ describe('create-react-class-integration', () => { expect(instance.state.occupation).toEqual('clown'); }); + // @gate !disableLegacyContext it('renders based on context getInitialState', () => { const Foo = createReactClass({ contextTypes: { @@ -322,17 +323,17 @@ describe('create-react-class-integration', () => { }); it('should throw with non-object getInitialState() return values', () => { - [['an array'], 'a string', 1234].forEach(function(state) { + [['an array'], 'a string', 1234].forEach(function (state) { const Component = createReactClass({ - getInitialState: function() { + getInitialState: function () { return state; }, - render: function() { + render: function () { return ; }, }); let instance = ; - expect(function() { + expect(function () { instance = ReactTestUtils.renderIntoDocument(instance); }).toThrowError( 'Component.getInitialState(): must return an object or null', @@ -342,10 +343,10 @@ describe('create-react-class-integration', () => { it('should work with a null getInitialState() return value', () => { const Component = createReactClass({ - getInitialState: function() { + getInitialState: function () { return null; }, - render: function() { + render: function () { return ; }, }); @@ -361,9 +362,7 @@ describe('create-react-class-integration', () => { }, }); - expect(() => - expect(() => Component()).toThrow(), - ).toErrorDev( + expect(() => expect(() => Component()).toThrow()).toErrorDev( 'Warning: Something is calling a React component directly. Use a ' + 'factory or JSX instead. See: https://fb.me/react-legacyfactory', {withoutStack: true}, @@ -425,7 +424,7 @@ describe('create-react-class-integration', () => { let instance; const Component = createReactClass({ statics: { - getDerivedStateFromProps: function() { + getDerivedStateFromProps: function () { return {foo: 'bar'}; }, }, @@ -434,7 +433,7 @@ describe('create-react-class-integration', () => { return {}; }, - render: function() { + render: function () { instance = this; return null; }, @@ -483,7 +482,7 @@ describe('create-react-class-integration', () => { const Foo = createReactClass({ displayName: 'Foo', statics: { - getSnapshotBeforeUpdate: function() { + getSnapshotBeforeUpdate: function () { return null; }, }, @@ -503,11 +502,11 @@ describe('create-react-class-integration', () => { const Component = createReactClass({ displayName: 'Component', statics: { - getDerivedStateFromProps: function() { + getDerivedStateFromProps: function () { return null; }, }, - render: function() { + render: function () { return null; }, }); @@ -524,23 +523,23 @@ describe('create-react-class-integration', () => { it('should not invoke deprecated lifecycles (cWM/cWRP/cWU) if new static gDSFP is present', () => { const Component = createReactClass({ statics: { - getDerivedStateFromProps: function() { + getDerivedStateFromProps: function () { return null; }, }, - componentWillMount: function() { + componentWillMount: function () { throw Error('unexpected'); }, - componentWillReceiveProps: function() { + componentWillReceiveProps: function () { throw Error('unexpected'); }, - componentWillUpdate: function() { + componentWillUpdate: function () { throw Error('unexpected'); }, - getInitialState: function() { + getInitialState: function () { return {}; }, - render: function() { + render: function () { return null; }, }); @@ -570,20 +569,20 @@ describe('create-react-class-integration', () => { it('should not invoke deprecated lifecycles (cWM/cWRP/cWU) if new getSnapshotBeforeUpdate is present', () => { const Component = createReactClass({ - getSnapshotBeforeUpdate: function() { + getSnapshotBeforeUpdate: function () { return null; }, - componentWillMount: function() { + componentWillMount: function () { throw Error('unexpected'); }, - componentWillReceiveProps: function() { + componentWillReceiveProps: function () { throw Error('unexpected'); }, - componentWillUpdate: function() { + componentWillUpdate: function () { throw Error('unexpected'); }, - componentDidUpdate: function() {}, - render: function() { + componentDidUpdate: function () {}, + render: function () { return null; }, }); @@ -617,27 +616,27 @@ describe('create-react-class-integration', () => { const Component = createReactClass({ mixins: [ { - componentWillMount: function() { + componentWillMount: function () { log.push('componentWillMount'); }, - componentWillReceiveProps: function() { + componentWillReceiveProps: function () { log.push('componentWillReceiveProps'); }, - componentWillUpdate: function() { + componentWillUpdate: function () { log.push('componentWillUpdate'); }, }, ], - UNSAFE_componentWillMount: function() { + UNSAFE_componentWillMount: function () { log.push('UNSAFE_componentWillMount'); }, - UNSAFE_componentWillReceiveProps: function() { + UNSAFE_componentWillReceiveProps: function () { log.push('UNSAFE_componentWillReceiveProps'); }, - UNSAFE_componentWillUpdate: function() { + UNSAFE_componentWillUpdate: function () { log.push('UNSAFE_componentWillUpdate'); }, - render: function() { + render: function () { return null; }, }); @@ -719,9 +718,7 @@ describe('create-react-class-integration', () => { const container = document.createElement('div'); - expect(() => - ReactDOM.render(, container), - ).toErrorDev( + expect(() => ReactDOM.render(, container)).toErrorDev( 'Warning: MyComponent: isMounted is deprecated. Instead, make sure to ' + 'clean up subscriptions and pending requests in componentWillUnmount ' + 'to prevent memory leaks.', diff --git a/packages/react/src/__tests__/forwardRef-test.internal.js b/packages/react/src/__tests__/forwardRef-test.internal.js index e3bf67f035be..6da58ee051fc 100644 --- a/packages/react/src/__tests__/forwardRef-test.internal.js +++ b/packages/react/src/__tests__/forwardRef-test.internal.js @@ -14,6 +14,7 @@ describe('forwardRef', () => { let ReactFeatureFlags; let ReactNoop; let Scheduler; + let waitForAll; beforeEach(() => { jest.resetModules(); @@ -23,12 +24,15 @@ describe('forwardRef', () => { React = require('react'); ReactNoop = require('react-noop-renderer'); Scheduler = require('scheduler'); + + const InternalTestUtils = require('internal-test-utils'); + waitForAll = InternalTestUtils.waitForAll; }); - it('should work without a ref to be forwarded', () => { + it('should work without a ref to be forwarded', async () => { class Child extends React.Component { render() { - Scheduler.unstable_yieldValue(this.props.value); + Scheduler.log(this.props.value); return null; } } @@ -42,13 +46,13 @@ describe('forwardRef', () => { )); ReactNoop.render(); - expect(Scheduler).toFlushAndYield([123]); + await waitForAll([123]); }); - it('should forward a ref for a single child', () => { + it('should forward a ref for a single child', async () => { class Child extends React.Component { render() { - Scheduler.unstable_yieldValue(this.props.value); + Scheduler.log(this.props.value); return null; } } @@ -64,14 +68,14 @@ describe('forwardRef', () => { const ref = React.createRef(); ReactNoop.render(); - expect(Scheduler).toFlushAndYield([123]); + await waitForAll([123]); expect(ref.current instanceof Child).toBe(true); }); - it('should forward a ref for multiple children', () => { + it('should forward a ref for multiple children', async () => { class Child extends React.Component { render() { - Scheduler.unstable_yieldValue(this.props.value); + Scheduler.log(this.props.value); return null; } } @@ -93,17 +97,17 @@ describe('forwardRef', () => {
    , ); - expect(Scheduler).toFlushAndYield([123]); + await waitForAll([123]); expect(ref.current instanceof Child).toBe(true); }); - it('should maintain child instance and ref through updates', () => { + it('should maintain child instance and ref through updates', async () => { class Child extends React.Component { constructor(props) { super(props); } render() { - Scheduler.unstable_yieldValue(this.props.value); + Scheduler.log(this.props.value); return null; } } @@ -125,42 +129,42 @@ describe('forwardRef', () => { }; ReactNoop.render(); - expect(Scheduler).toFlushAndYield([123]); + await waitForAll([123]); expect(ref instanceof Child).toBe(true); expect(setRefCount).toBe(1); ReactNoop.render(); - expect(Scheduler).toFlushAndYield([456]); + await waitForAll([456]); expect(ref instanceof Child).toBe(true); expect(setRefCount).toBe(1); }); - it('should not break lifecycle error handling', () => { + it('should not break lifecycle error handling', async () => { class ErrorBoundary extends React.Component { state = {error: null}; componentDidCatch(error) { - Scheduler.unstable_yieldValue('ErrorBoundary.componentDidCatch'); + Scheduler.log('ErrorBoundary.componentDidCatch'); this.setState({error}); } render() { if (this.state.error) { - Scheduler.unstable_yieldValue('ErrorBoundary.render: catch'); + Scheduler.log('ErrorBoundary.render: catch'); return null; } - Scheduler.unstable_yieldValue('ErrorBoundary.render: try'); + Scheduler.log('ErrorBoundary.render: try'); return this.props.children; } } class BadRender extends React.Component { render() { - Scheduler.unstable_yieldValue('BadRender throw'); + Scheduler.log('BadRender throw'); throw new Error('oops!'); } } function Wrapper(props) { const forwardedRef = props.forwardedRef; - Scheduler.unstable_yieldValue('Wrapper'); + Scheduler.log('Wrapper'); return ; } @@ -175,7 +179,7 @@ describe('forwardRef', () => { , ); - expect(Scheduler).toFlushAndYield([ + await waitForAll([ 'ErrorBoundary.render: try', 'Wrapper', 'BadRender throw', @@ -192,36 +196,36 @@ describe('forwardRef', () => { expect(ref.current).toBe(null); }); - it('should not re-run the render callback on a deep setState', () => { + it('should not re-run the render callback on a deep setState', async () => { let inst; class Inner extends React.Component { render() { - Scheduler.unstable_yieldValue('Inner'); + Scheduler.log('Inner'); inst = this; return
    ; } } function Middle(props) { - Scheduler.unstable_yieldValue('Middle'); + Scheduler.log('Middle'); return ; } const Forward = React.forwardRef((props, ref) => { - Scheduler.unstable_yieldValue('Forward'); + Scheduler.log('Forward'); return ; }); function App() { - Scheduler.unstable_yieldValue('App'); + Scheduler.log('App'); return ; } ReactNoop.render(); - expect(Scheduler).toFlushAndYield(['App', 'Forward', 'Middle', 'Inner']); + await waitForAll(['App', 'Forward', 'Middle', 'Inner']); inst.setState({}); - expect(Scheduler).toFlushAndYield(['Inner']); + await waitForAll(['Inner']); }); }); diff --git a/packages/react/src/__tests__/forwardRef-test.js b/packages/react/src/__tests__/forwardRef-test.js index 87bb9f3c6afa..6509ca23e837 100644 --- a/packages/react/src/__tests__/forwardRef-test.js +++ b/packages/react/src/__tests__/forwardRef-test.js @@ -13,17 +13,19 @@ describe('forwardRef', () => { let PropTypes; let React; let ReactNoop; - let Scheduler; + let waitForAll; beforeEach(() => { jest.resetModules(); PropTypes = require('prop-types'); React = require('react'); ReactNoop = require('react-noop-renderer'); - Scheduler = require('scheduler'); + + const InternalTestUtils = require('internal-test-utils'); + waitForAll = InternalTestUtils.waitForAll; }); - it('should update refs when switching between children', () => { + it('should update refs when switching between children', async () => { function FunctionComponent({forwardedRef, setRefOnDiv}) { return (
    @@ -40,25 +42,25 @@ describe('forwardRef', () => { const ref = React.createRef(); ReactNoop.render(); - expect(Scheduler).toFlushWithoutYielding(); + await waitForAll([]); expect(ref.current.type).toBe('div'); ReactNoop.render(); - expect(Scheduler).toFlushWithoutYielding(); + await waitForAll([]); expect(ref.current.type).toBe('span'); }); - it('should support rendering null', () => { + it('should support rendering null', async () => { const RefForwardingComponent = React.forwardRef((props, ref) => null); const ref = React.createRef(); ReactNoop.render(); - expect(Scheduler).toFlushWithoutYielding(); + await waitForAll([]); expect(ref.current).toBe(null); }); - it('should support rendering null for multiple children', () => { + it('should support rendering null for multiple children', async () => { const RefForwardingComponent = React.forwardRef((props, ref) => null); const ref = React.createRef(); @@ -70,11 +72,11 @@ describe('forwardRef', () => {
    , ); - expect(Scheduler).toFlushWithoutYielding(); + await waitForAll([]); expect(ref.current).toBe(null); }); - it('should support propTypes and defaultProps', () => { + it('should support propTypes and defaultProps', async () => { function FunctionComponent({forwardedRef, optional, required}) { return (
    @@ -103,14 +105,14 @@ describe('forwardRef', () => { ReactNoop.render( , ); - expect(Scheduler).toFlushWithoutYielding(); + await waitForAll([]); expect(ref.current.children).toEqual([ {text: 'foo', hidden: false}, {text: 'bar', hidden: false}, ]); ReactNoop.render(); - expect(Scheduler).toFlushWithoutYielding(); + await waitForAll([]); expect(ref.current.children).toEqual([ {text: 'default', hidden: false}, {text: 'foo', hidden: false}, @@ -126,9 +128,7 @@ describe('forwardRef', () => { }); it('should warn if not provided a callback during creation', () => { - expect(() => - React.forwardRef(undefined), - ).toErrorDev( + expect(() => React.forwardRef(undefined)).toErrorDev( 'forwardRef requires a render function but was given undefined.', {withoutStack: true}, ); @@ -138,18 +138,14 @@ describe('forwardRef', () => { withoutStack: true, }, ); - expect(() => - React.forwardRef('foo'), - ).toErrorDev( + expect(() => React.forwardRef('foo')).toErrorDev( 'forwardRef requires a render function but was given string.', {withoutStack: true}, ); }); it('should warn if no render function is provided', () => { - expect( - React.forwardRef, - ).toErrorDev( + expect(React.forwardRef).toErrorDev( 'forwardRef requires a render function but was given undefined.', {withoutStack: true}, ); @@ -166,16 +162,12 @@ describe('forwardRef', () => { } renderWithDefaultProps.defaultProps = {}; - expect(() => - React.forwardRef(renderWithPropTypes), - ).toErrorDev( + expect(() => React.forwardRef(renderWithPropTypes)).toErrorDev( 'forwardRef render functions do not support propTypes or defaultProps. ' + 'Did you accidentally pass a React component?', {withoutStack: true}, ); - expect(() => - React.forwardRef(renderWithDefaultProps), - ).toErrorDev( + expect(() => React.forwardRef(renderWithDefaultProps)).toErrorDev( 'forwardRef render functions do not support propTypes or defaultProps. ' + 'Did you accidentally pass a React component?', {withoutStack: true}, @@ -191,9 +183,7 @@ describe('forwardRef', () => { it('should warn if the render function provided does not use the forwarded ref parameter', () => { const arityOfOne = props =>
    ; - expect(() => - React.forwardRef(arityOfOne), - ).toErrorDev( + expect(() => React.forwardRef(arityOfOne)).toErrorDev( 'forwardRef render functions accept exactly two parameters: props and ref. ' + 'Did you forget to use the ref parameter?', {withoutStack: true}, @@ -208,9 +198,7 @@ describe('forwardRef', () => { it('should warn if the render function provided expects to use more than two parameters', () => { const arityOfThree = (props, ref, x) =>
    ; - expect(() => - React.forwardRef(arityOfThree), - ).toErrorDev( + expect(() => React.forwardRef(arityOfThree)).toErrorDev( 'forwardRef render functions accept exactly two parameters: props and ref. ' + 'Any additional parameter will be undefined.', {withoutStack: true}, @@ -358,7 +346,7 @@ describe('forwardRef', () => { ); }); - it('should not bailout if forwardRef is not wrapped in memo', () => { + it('should not bailout if forwardRef is not wrapped in memo', async () => { const Component = props =>
    ; let renderCount = 0; @@ -371,15 +359,15 @@ describe('forwardRef', () => { const ref = React.createRef(); ReactNoop.render(); - expect(Scheduler).toFlushWithoutYielding(); + await waitForAll([]); expect(renderCount).toBe(1); ReactNoop.render(); - expect(Scheduler).toFlushWithoutYielding(); + await waitForAll([]); expect(renderCount).toBe(2); }); - it('should bailout if forwardRef is wrapped in memo', () => { + it('should bailout if forwardRef is wrapped in memo', async () => { const Component = props =>
    ; let renderCount = 0; @@ -394,13 +382,13 @@ describe('forwardRef', () => { const ref = React.createRef(); ReactNoop.render(); - expect(Scheduler).toFlushWithoutYielding(); + await waitForAll([]); expect(renderCount).toBe(1); expect(ref.current.type).toBe('div'); ReactNoop.render(); - expect(Scheduler).toFlushWithoutYielding(); + await waitForAll([]); expect(renderCount).toBe(1); const differentRef = React.createRef(); @@ -408,18 +396,18 @@ describe('forwardRef', () => { ReactNoop.render( , ); - expect(Scheduler).toFlushWithoutYielding(); + await waitForAll([]); expect(renderCount).toBe(2); expect(ref.current).toBe(null); expect(differentRef.current.type).toBe('div'); ReactNoop.render(); - expect(Scheduler).toFlushWithoutYielding(); + await waitForAll([]); expect(renderCount).toBe(3); }); - it('should custom memo comparisons to compose', () => { + it('should custom memo comparisons to compose', async () => { const Component = props =>
    ; let renderCount = 0; @@ -435,19 +423,19 @@ describe('forwardRef', () => { const ref = React.createRef(); ReactNoop.render(); - expect(Scheduler).toFlushWithoutYielding(); + await waitForAll([]); expect(renderCount).toBe(1); expect(ref.current.type).toBe('div'); // Changing either a or b rerenders ReactNoop.render(); - expect(Scheduler).toFlushWithoutYielding(); + await waitForAll([]); expect(renderCount).toBe(2); // Changing c doesn't rerender ReactNoop.render(); - expect(Scheduler).toFlushWithoutYielding(); + await waitForAll([]); expect(renderCount).toBe(2); const ComposedMemo = React.memo( @@ -456,29 +444,29 @@ describe('forwardRef', () => { ); ReactNoop.render(); - expect(Scheduler).toFlushWithoutYielding(); + await waitForAll([]); expect(renderCount).toBe(3); // Changing just b no longer updates ReactNoop.render(); - expect(Scheduler).toFlushWithoutYielding(); + await waitForAll([]); expect(renderCount).toBe(3); // Changing just a and c updates ReactNoop.render(); - expect(Scheduler).toFlushWithoutYielding(); + await waitForAll([]); expect(renderCount).toBe(4); // Changing just c does not update ReactNoop.render(); - expect(Scheduler).toFlushWithoutYielding(); + await waitForAll([]); expect(renderCount).toBe(4); // Changing ref still rerenders const differentRef = React.createRef(); ReactNoop.render(); - expect(Scheduler).toFlushWithoutYielding(); + await waitForAll([]); expect(renderCount).toBe(5); expect(ref.current).toBe(null); diff --git a/packages/react/src/__tests__/onlyChild-test.js b/packages/react/src/__tests__/onlyChild-test.js index 328b2f6686fb..f6bec485fe2b 100644 --- a/packages/react/src/__tests__/onlyChild-test.js +++ b/packages/react/src/__tests__/onlyChild-test.js @@ -27,7 +27,7 @@ describe('onlyChild', () => { }); it('should fail when passed two children', () => { - expect(function() { + expect(function () { const instance = (
    @@ -39,26 +39,26 @@ describe('onlyChild', () => { }); it('should fail when passed nully values', () => { - expect(function() { + expect(function () { const instance = {null}; React.Children.only(instance.props.children); }).toThrow(); - expect(function() { + expect(function () { const instance = {undefined}; React.Children.only(instance.props.children); }).toThrow(); }); it('should fail when key/value objects', () => { - expect(function() { + expect(function () { const instance = {[]}; React.Children.only(instance.props.children); }).toThrow(); }); it('should not fail when passed interpolated single child', () => { - expect(function() { + expect(function () { const instance = {}; React.Children.only(instance.props.children); }).not.toThrow(); diff --git a/packages/react/src/__tests__/testDefinitions/ReactDOM.d.ts b/packages/react/src/__tests__/testDefinitions/ReactDOM.d.ts index 0b20d827b451..eb6ad709efb1 100644 --- a/packages/react/src/__tests__/testDefinitions/ReactDOM.d.ts +++ b/packages/react/src/__tests__/testDefinitions/ReactDOM.d.ts @@ -16,4 +16,5 @@ declare module 'react-dom' { export function render(element : any, container : any) : any export function unmountComponentAtNode(container : any) : void export function findDOMNode(instance : any) : any + export function flushSync(cb : any) : any } diff --git a/packages/react/src/jsx/ReactJSXElement.js b/packages/react/src/jsx/ReactJSXElement.js index 47d9cec8f3e4..e2d8fe0af4ae 100644 --- a/packages/react/src/jsx/ReactJSXElement.js +++ b/packages/react/src/jsx/ReactJSXElement.js @@ -83,7 +83,7 @@ function warnIfStringRefCannotBeAutoConverted(config, self) { function defineKeyPropWarningGetter(props, displayName) { if (__DEV__) { - const warnAboutAccessingKey = function() { + const warnAboutAccessingKey = function () { if (!specialPropKeyWarningShown) { specialPropKeyWarningShown = true; console.error( @@ -105,7 +105,7 @@ function defineKeyPropWarningGetter(props, displayName) { function defineRefPropWarningGetter(props, displayName) { if (__DEV__) { - const warnAboutAccessingRef = function() { + const warnAboutAccessingRef = function () { if (!specialPropRefWarningShown) { specialPropRefWarningShown = true; console.error( @@ -145,7 +145,7 @@ function defineRefPropWarningGetter(props, displayName) { * indicating filename, line number, and/or other information. * @internal */ -const ReactElement = function(type, key, ref, self, source, owner, props) { +function ReactElement(type, key, ref, self, source, owner, props) { const element = { // This tag allows us to uniquely identify this as a React Element $$typeof: REACT_ELEMENT_TYPE, @@ -199,7 +199,7 @@ const ReactElement = function(type, key, ref, self, source, owner, props) { } return element; -}; +} /** * https://github.com/reactjs/rfcs/pull/107 diff --git a/packages/react/src/jsx/ReactJSXElementValidator.js b/packages/react/src/jsx/ReactJSXElementValidator.js index 044f9a2b0594..da000079ee90 100644 --- a/packages/react/src/jsx/ReactJSXElementValidator.js +++ b/packages/react/src/jsx/ReactJSXElementValidator.js @@ -21,7 +21,6 @@ import { REACT_FRAGMENT_TYPE, REACT_ELEMENT_TYPE, } from 'shared/ReactSymbols'; -import {warnAboutSpreadingKeyToJSX} from 'shared/ReactFeatureFlags'; import hasOwnProperty from 'shared/hasOwnProperty'; import isArray from 'shared/isArray'; import {jsxDEV} from './ReactJSXElement'; @@ -33,6 +32,8 @@ import ReactSharedInternals from 'shared/ReactSharedInternals'; const ReactCurrentOwner = ReactSharedInternals.ReactCurrentOwner; const ReactDebugCurrentFrame = ReactSharedInternals.ReactDebugCurrentFrame; +const REACT_CLIENT_REFERENCE = Symbol.for('react.client.reference'); + function setCurrentlyValidatingElement(element) { if (__DEV__) { if (element) { @@ -180,10 +181,12 @@ function validateExplicitKey(element, parentType) { */ function validateChildKeys(node, parentType) { if (__DEV__) { - if (typeof node !== 'object') { + if (typeof node !== 'object' || !node) { return; } - if (isArray(node)) { + if (node.$$typeof === REACT_CLIENT_REFERENCE) { + // This is a reference to a client component so it's unknown. + } else if (isArray(node)) { for (let i = 0; i < node.length; i++) { const child = node[i]; if (isValidElement(child)) { @@ -195,7 +198,7 @@ function validateChildKeys(node, parentType) { if (node._store) { node._store.validated = true; } - } else if (node) { + } else { const iteratorFn = getIteratorFn(node); if (typeof iteratorFn === 'function') { // Entry iterators used to provide implicit keys, @@ -226,6 +229,9 @@ function validatePropTypes(element) { if (type === null || type === undefined || typeof type === 'string') { return; } + if (type.$$typeof === REACT_CLIENT_REFERENCE) { + return; + } let propTypes; if (typeof type === 'function') { propTypes = type.propTypes; @@ -390,31 +396,29 @@ export function jsxWithValidation( } } - if (warnAboutSpreadingKeyToJSX) { - if (hasOwnProperty.call(props, 'key')) { - const componentName = getComponentNameFromType(type); - const keys = Object.keys(props).filter(k => k !== 'key'); - const beforeExample = - keys.length > 0 - ? '{key: someKey, ' + keys.join(': ..., ') + ': ...}' - : '{key: someKey}'; - if (!didWarnAboutKeySpread[componentName + beforeExample]) { - const afterExample = - keys.length > 0 ? '{' + keys.join(': ..., ') + ': ...}' : '{}'; - console.error( - 'A props object containing a "key" prop is being spread into JSX:\n' + - ' let props = %s;\n' + - ' <%s {...props} />\n' + - 'React keys must be passed directly to JSX without using spread:\n' + - ' let props = %s;\n' + - ' <%s key={someKey} {...props} />', - beforeExample, - componentName, - afterExample, - componentName, - ); - didWarnAboutKeySpread[componentName + beforeExample] = true; - } + if (hasOwnProperty.call(props, 'key')) { + const componentName = getComponentNameFromType(type); + const keys = Object.keys(props).filter(k => k !== 'key'); + const beforeExample = + keys.length > 0 + ? '{key: someKey, ' + keys.join(': ..., ') + ': ...}' + : '{key: someKey}'; + if (!didWarnAboutKeySpread[componentName + beforeExample]) { + const afterExample = + keys.length > 0 ? '{' + keys.join(': ..., ') + ': ...}' : '{}'; + console.error( + 'A props object containing a "key" prop is being spread into JSX:\n' + + ' let props = %s;\n' + + ' <%s {...props} />\n' + + 'React keys must be passed directly to JSX without using spread:\n' + + ' let props = %s;\n' + + ' <%s key={someKey} {...props} />', + beforeExample, + componentName, + afterExample, + componentName, + ); + didWarnAboutKeySpread[componentName + beforeExample] = true; } } diff --git a/packages/scheduler/npm/index.js b/packages/scheduler/npm/index.js index 77770b0c219e..67a283b8aa05 100644 --- a/packages/scheduler/npm/index.js +++ b/packages/scheduler/npm/index.js @@ -1,6 +1,8 @@ 'use strict'; -if (process.env.NODE_ENV === 'production') { +if (typeof global.nativeRuntimeScheduler !== 'undefined') { + module.exports = global.nativeRuntimeScheduler; +} else if (process.env.NODE_ENV === 'production') { module.exports = require('./cjs/scheduler.production.min.js'); } else { module.exports = require('./cjs/scheduler.development.js'); diff --git a/packages/scheduler/npm/umd/scheduler.development.js b/packages/scheduler/npm/umd/scheduler.development.js index 2a060933a1ac..f7d2e7cd4cf0 100644 --- a/packages/scheduler/npm/umd/scheduler.development.js +++ b/packages/scheduler/npm/umd/scheduler.development.js @@ -11,14 +11,14 @@ 'use strict'; -(function(global, factory) { - // eslint-disable-next-line no-unused-expressions +(function (global, factory) { + // eslint-disable-next-line ft-flow/no-unused-expressions typeof exports === 'object' && typeof module !== 'undefined' ? (module.exports = factory(require('react'))) : typeof define === 'function' && define.amd // eslint-disable-line no-undef ? define(['react'], factory) // eslint-disable-line no-undef : (global.Scheduler = factory(global)); -})(this, function(global) { +})(this, function (global) { function unstable_now() { return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler.unstable_now.apply( this, diff --git a/packages/scheduler/npm/umd/scheduler.production.min.js b/packages/scheduler/npm/umd/scheduler.production.min.js index 083586fa186a..1a1f5f5ee0b5 100644 --- a/packages/scheduler/npm/umd/scheduler.production.min.js +++ b/packages/scheduler/npm/umd/scheduler.production.min.js @@ -11,14 +11,14 @@ 'use strict'; -(function(global, factory) { - // eslint-disable-next-line no-unused-expressions +(function (global, factory) { + // eslint-disable-next-line ft-flow/no-unused-expressions typeof exports === 'object' && typeof module !== 'undefined' ? (module.exports = factory(require('react'))) : typeof define === 'function' && define.amd // eslint-disable-line no-undef ? define(['react'], factory) // eslint-disable-line no-undef : (global.Scheduler = factory(global)); -})(this, function(global) { +})(this, function (global) { function unstable_now() { return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler.unstable_now.apply( this, diff --git a/packages/scheduler/npm/umd/scheduler.profiling.min.js b/packages/scheduler/npm/umd/scheduler.profiling.min.js index 083586fa186a..1a1f5f5ee0b5 100644 --- a/packages/scheduler/npm/umd/scheduler.profiling.min.js +++ b/packages/scheduler/npm/umd/scheduler.profiling.min.js @@ -11,14 +11,14 @@ 'use strict'; -(function(global, factory) { - // eslint-disable-next-line no-unused-expressions +(function (global, factory) { + // eslint-disable-next-line ft-flow/no-unused-expressions typeof exports === 'object' && typeof module !== 'undefined' ? (module.exports = factory(require('react'))) : typeof define === 'function' && define.amd // eslint-disable-line no-undef ? define(['react'], factory) // eslint-disable-line no-undef : (global.Scheduler = factory(global)); -})(this, function(global) { +})(this, function (global) { function unstable_now() { return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler.unstable_now.apply( this, diff --git a/packages/scheduler/src/__tests__/SchedulerMock-test.js b/packages/scheduler/src/__tests__/SchedulerMock-test.js index c52e30e9b1fb..d71957ef3eba 100644 --- a/packages/scheduler/src/__tests__/SchedulerMock-test.js +++ b/packages/scheduler/src/__tests__/SchedulerMock-test.js @@ -21,6 +21,10 @@ let cancelCallback; let wrapCallback; let getCurrentPriorityLevel; let shouldYield; +let waitForAll; +let assertLog; +let waitFor; +let waitForPaint; describe('Scheduler', () => { beforeEach(() => { @@ -40,117 +44,114 @@ describe('Scheduler', () => { wrapCallback = Scheduler.unstable_wrapCallback; getCurrentPriorityLevel = Scheduler.unstable_getCurrentPriorityLevel; shouldYield = Scheduler.unstable_shouldYield; + + const InternalTestUtils = require('internal-test-utils'); + waitForAll = InternalTestUtils.waitForAll; + assertLog = InternalTestUtils.assertLog; + waitFor = InternalTestUtils.waitFor; + waitForPaint = InternalTestUtils.waitForPaint; }); - it('flushes work incrementally', () => { - scheduleCallback(NormalPriority, () => Scheduler.unstable_yieldValue('A')); - scheduleCallback(NormalPriority, () => Scheduler.unstable_yieldValue('B')); - scheduleCallback(NormalPriority, () => Scheduler.unstable_yieldValue('C')); - scheduleCallback(NormalPriority, () => Scheduler.unstable_yieldValue('D')); + it('flushes work incrementally', async () => { + scheduleCallback(NormalPriority, () => Scheduler.log('A')); + scheduleCallback(NormalPriority, () => Scheduler.log('B')); + scheduleCallback(NormalPriority, () => Scheduler.log('C')); + scheduleCallback(NormalPriority, () => Scheduler.log('D')); - expect(Scheduler).toFlushAndYieldThrough(['A', 'B']); - expect(Scheduler).toFlushAndYieldThrough(['C']); - expect(Scheduler).toFlushAndYield(['D']); + await waitFor(['A', 'B']); + await waitFor(['C']); + await waitForAll(['D']); }); - it('cancels work', () => { - scheduleCallback(NormalPriority, () => Scheduler.unstable_yieldValue('A')); + it('cancels work', async () => { + scheduleCallback(NormalPriority, () => Scheduler.log('A')); const callbackHandleB = scheduleCallback(NormalPriority, () => - Scheduler.unstable_yieldValue('B'), + Scheduler.log('B'), ); - scheduleCallback(NormalPriority, () => Scheduler.unstable_yieldValue('C')); + scheduleCallback(NormalPriority, () => Scheduler.log('C')); cancelCallback(callbackHandleB); - expect(Scheduler).toFlushAndYield([ + await waitForAll([ 'A', // B should have been cancelled 'C', ]); }); - it('executes the highest priority callbacks first', () => { - scheduleCallback(NormalPriority, () => Scheduler.unstable_yieldValue('A')); - scheduleCallback(NormalPriority, () => Scheduler.unstable_yieldValue('B')); + it('executes the highest priority callbacks first', async () => { + scheduleCallback(NormalPriority, () => Scheduler.log('A')); + scheduleCallback(NormalPriority, () => Scheduler.log('B')); // Yield before B is flushed - expect(Scheduler).toFlushAndYieldThrough(['A']); + await waitFor(['A']); - scheduleCallback(UserBlockingPriority, () => - Scheduler.unstable_yieldValue('C'), - ); - scheduleCallback(UserBlockingPriority, () => - Scheduler.unstable_yieldValue('D'), - ); + scheduleCallback(UserBlockingPriority, () => Scheduler.log('C')); + scheduleCallback(UserBlockingPriority, () => Scheduler.log('D')); // C and D should come first, because they are higher priority - expect(Scheduler).toFlushAndYield(['C', 'D', 'B']); + await waitForAll(['C', 'D', 'B']); }); - it('expires work', () => { + it('expires work', async () => { scheduleCallback(NormalPriority, didTimeout => { Scheduler.unstable_advanceTime(100); - Scheduler.unstable_yieldValue(`A (did timeout: ${didTimeout})`); + Scheduler.log(`A (did timeout: ${didTimeout})`); }); scheduleCallback(UserBlockingPriority, didTimeout => { Scheduler.unstable_advanceTime(100); - Scheduler.unstable_yieldValue(`B (did timeout: ${didTimeout})`); + Scheduler.log(`B (did timeout: ${didTimeout})`); }); scheduleCallback(UserBlockingPriority, didTimeout => { Scheduler.unstable_advanceTime(100); - Scheduler.unstable_yieldValue(`C (did timeout: ${didTimeout})`); + Scheduler.log(`C (did timeout: ${didTimeout})`); }); // Advance time, but not by enough to expire any work Scheduler.unstable_advanceTime(249); - expect(Scheduler).toHaveYielded([]); + assertLog([]); // Schedule a few more callbacks scheduleCallback(NormalPriority, didTimeout => { Scheduler.unstable_advanceTime(100); - Scheduler.unstable_yieldValue(`D (did timeout: ${didTimeout})`); + Scheduler.log(`D (did timeout: ${didTimeout})`); }); scheduleCallback(NormalPriority, didTimeout => { Scheduler.unstable_advanceTime(100); - Scheduler.unstable_yieldValue(`E (did timeout: ${didTimeout})`); + Scheduler.log(`E (did timeout: ${didTimeout})`); }); // Advance by just a bit more to expire the user blocking callbacks Scheduler.unstable_advanceTime(1); - expect(Scheduler).toFlushAndYieldThrough([ - 'B (did timeout: true)', - 'C (did timeout: true)', - ]); + await waitFor(['B (did timeout: true)', 'C (did timeout: true)']); // Expire A Scheduler.unstable_advanceTime(4600); - expect(Scheduler).toFlushAndYieldThrough(['A (did timeout: true)']); + await waitFor(['A (did timeout: true)']); // Flush the rest without expiring - expect(Scheduler).toFlushAndYield([ - 'D (did timeout: false)', - 'E (did timeout: true)', - ]); + await waitForAll(['D (did timeout: false)', 'E (did timeout: true)']); }); it('has a default expiration of ~5 seconds', () => { - scheduleCallback(NormalPriority, () => Scheduler.unstable_yieldValue('A')); + scheduleCallback(NormalPriority, () => Scheduler.log('A')); Scheduler.unstable_advanceTime(4999); - expect(Scheduler).toHaveYielded([]); + assertLog([]); Scheduler.unstable_advanceTime(1); - expect(Scheduler).toFlushExpired(['A']); + Scheduler.unstable_flushExpired(); + assertLog(['A']); }); - it('continues working on same task after yielding', () => { + it('continues working on same task after yielding', async () => { scheduleCallback(NormalPriority, () => { Scheduler.unstable_advanceTime(100); - Scheduler.unstable_yieldValue('A'); + Scheduler.log('A'); }); scheduleCallback(NormalPriority, () => { Scheduler.unstable_advanceTime(100); - Scheduler.unstable_yieldValue('B'); + Scheduler.log('B'); }); let didYield = false; @@ -163,7 +164,7 @@ describe('Scheduler', () => { while (tasks.length > 0) { const [label, ms] = tasks.shift(); Scheduler.unstable_advanceTime(ms); - Scheduler.unstable_yieldValue(label); + Scheduler.log(label); if (shouldYield()) { didYield = true; return C; @@ -175,23 +176,23 @@ describe('Scheduler', () => { scheduleCallback(NormalPriority, () => { Scheduler.unstable_advanceTime(100); - Scheduler.unstable_yieldValue('D'); + Scheduler.log('D'); }); scheduleCallback(NormalPriority, () => { Scheduler.unstable_advanceTime(100); - Scheduler.unstable_yieldValue('E'); + Scheduler.log('E'); }); // Flush, then yield while in the middle of C. expect(didYield).toBe(false); - expect(Scheduler).toFlushAndYieldThrough(['A', 'B', 'C1']); + await waitFor(['A', 'B', 'C1']); expect(didYield).toBe(true); // When we resume, we should continue working on C. - expect(Scheduler).toFlushAndYield(['C2', 'C3', 'D', 'E']); + await waitForAll(['C2', 'C3', 'D', 'E']); }); - it('continuation callbacks inherit the expiration of the previous callback', () => { + it('continuation callbacks inherit the expiration of the previous callback', async () => { const tasks = [ ['A', 125], ['B', 124], @@ -202,7 +203,7 @@ describe('Scheduler', () => { while (tasks.length > 0) { const [label, ms] = tasks.shift(); Scheduler.unstable_advanceTime(ms); - Scheduler.unstable_yieldValue(label); + Scheduler.log(label); if (shouldYield()) { return work; } @@ -213,14 +214,15 @@ describe('Scheduler', () => { scheduleCallback(UserBlockingPriority, work); // Flush until just before the expiration time - expect(Scheduler).toFlushAndYieldThrough(['A', 'B']); + await waitFor(['A', 'B']); // Advance time by just a bit more. This should expire all the remaining work. Scheduler.unstable_advanceTime(1); - expect(Scheduler).toFlushExpired(['C', 'D']); + Scheduler.unstable_flushExpired(); + assertLog(['C', 'D']); }); - it('continuations are interrupted by higher priority work', () => { + it('continuations are interrupted by higher priority work', async () => { const tasks = [ ['A', 100], ['B', 100], @@ -231,27 +233,27 @@ describe('Scheduler', () => { while (tasks.length > 0) { const [label, ms] = tasks.shift(); Scheduler.unstable_advanceTime(ms); - Scheduler.unstable_yieldValue(label); + Scheduler.log(label); if (tasks.length > 0 && shouldYield()) { return work; } } }; scheduleCallback(NormalPriority, work); - expect(Scheduler).toFlushAndYieldThrough(['A']); + await waitFor(['A']); scheduleCallback(UserBlockingPriority, () => { Scheduler.unstable_advanceTime(100); - Scheduler.unstable_yieldValue('High pri'); + Scheduler.log('High pri'); }); - expect(Scheduler).toFlushAndYield(['High pri', 'B', 'C', 'D']); + await waitForAll(['High pri', 'B', 'C', 'D']); }); it( 'continuations do not block higher priority work scheduled ' + 'inside an executing callback', - () => { + async () => { const tasks = [ ['A', 100], ['B', 100], @@ -263,13 +265,13 @@ describe('Scheduler', () => { const task = tasks.shift(); const [label, ms] = task; Scheduler.unstable_advanceTime(ms); - Scheduler.unstable_yieldValue(label); + Scheduler.log(label); if (label === 'B') { // Schedule high pri work from inside another callback - Scheduler.unstable_yieldValue('Schedule high pri'); + Scheduler.log('Schedule high pri'); scheduleCallback(UserBlockingPriority, () => { Scheduler.unstable_advanceTime(100); - Scheduler.unstable_yieldValue('High pri'); + Scheduler.log('High pri'); }); } if (tasks.length > 0) { @@ -279,7 +281,7 @@ describe('Scheduler', () => { } }; scheduleCallback(NormalPriority, work); - expect(Scheduler).toFlushAndYield([ + await waitForAll([ 'A', 'B', 'Schedule high pri', @@ -293,55 +295,43 @@ describe('Scheduler', () => { }, ); - it('cancelling a continuation', () => { + it('cancelling a continuation', async () => { const task = scheduleCallback(NormalPriority, () => { - Scheduler.unstable_yieldValue('Yield'); + Scheduler.log('Yield'); return () => { - Scheduler.unstable_yieldValue('Continuation'); + Scheduler.log('Continuation'); }; }); - expect(Scheduler).toFlushAndYieldThrough(['Yield']); + await waitFor(['Yield']); cancelCallback(task); - expect(Scheduler).toFlushWithoutYielding(); + await waitForAll([]); }); it('top-level immediate callbacks fire in a subsequent task', () => { - scheduleCallback(ImmediatePriority, () => - Scheduler.unstable_yieldValue('A'), - ); - scheduleCallback(ImmediatePriority, () => - Scheduler.unstable_yieldValue('B'), - ); - scheduleCallback(ImmediatePriority, () => - Scheduler.unstable_yieldValue('C'), - ); - scheduleCallback(ImmediatePriority, () => - Scheduler.unstable_yieldValue('D'), - ); + scheduleCallback(ImmediatePriority, () => Scheduler.log('A')); + scheduleCallback(ImmediatePriority, () => Scheduler.log('B')); + scheduleCallback(ImmediatePriority, () => Scheduler.log('C')); + scheduleCallback(ImmediatePriority, () => Scheduler.log('D')); // Immediate callback hasn't fired, yet. - expect(Scheduler).toHaveYielded([]); + assertLog([]); // They all flush immediately within the subsequent task. - expect(Scheduler).toFlushExpired(['A', 'B', 'C', 'D']); + Scheduler.unstable_flushExpired(); + assertLog(['A', 'B', 'C', 'D']); }); it('nested immediate callbacks are added to the queue of immediate callbacks', () => { - scheduleCallback(ImmediatePriority, () => - Scheduler.unstable_yieldValue('A'), - ); + scheduleCallback(ImmediatePriority, () => Scheduler.log('A')); scheduleCallback(ImmediatePriority, () => { - Scheduler.unstable_yieldValue('B'); + Scheduler.log('B'); // This callback should go to the end of the queue - scheduleCallback(ImmediatePriority, () => - Scheduler.unstable_yieldValue('C'), - ); + scheduleCallback(ImmediatePriority, () => Scheduler.log('C')); }); - scheduleCallback(ImmediatePriority, () => - Scheduler.unstable_yieldValue('D'), - ); - expect(Scheduler).toHaveYielded([]); + scheduleCallback(ImmediatePriority, () => Scheduler.log('D')); + assertLog([]); // C should flush at the end - expect(Scheduler).toFlushExpired(['A', 'B', 'D', 'C']); + Scheduler.unstable_flushExpired(); + assertLog(['A', 'B', 'D', 'C']); }); it('wrapped callbacks have same signature as original callback', () => { @@ -352,7 +342,7 @@ describe('Scheduler', () => { it('wrapped callbacks inherit the current priority', () => { const wrappedCallback = runWithPriority(NormalPriority, () => wrapCallback(() => { - Scheduler.unstable_yieldValue(getCurrentPriorityLevel()); + Scheduler.log(getCurrentPriorityLevel()); }), ); @@ -360,15 +350,15 @@ describe('Scheduler', () => { UserBlockingPriority, () => wrapCallback(() => { - Scheduler.unstable_yieldValue(getCurrentPriorityLevel()); + Scheduler.log(getCurrentPriorityLevel()); }), ); wrappedCallback(); - expect(Scheduler).toHaveYielded([NormalPriority]); + assertLog([NormalPriority]); wrappedUserBlockingCallback(); - expect(Scheduler).toHaveYielded([UserBlockingPriority]); + assertLog([UserBlockingPriority]); }); it('wrapped callbacks inherit the current priority even when nested', () => { @@ -377,42 +367,42 @@ describe('Scheduler', () => { runWithPriority(NormalPriority, () => { wrappedCallback = wrapCallback(() => { - Scheduler.unstable_yieldValue(getCurrentPriorityLevel()); + Scheduler.log(getCurrentPriorityLevel()); }); wrappedUserBlockingCallback = runWithPriority(UserBlockingPriority, () => wrapCallback(() => { - Scheduler.unstable_yieldValue(getCurrentPriorityLevel()); + Scheduler.log(getCurrentPriorityLevel()); }), ); }); wrappedCallback(); - expect(Scheduler).toHaveYielded([NormalPriority]); + assertLog([NormalPriority]); wrappedUserBlockingCallback(); - expect(Scheduler).toHaveYielded([UserBlockingPriority]); + assertLog([UserBlockingPriority]); }); it("immediate callbacks fire even if there's an error", () => { scheduleCallback(ImmediatePriority, () => { - Scheduler.unstable_yieldValue('A'); + Scheduler.log('A'); throw new Error('Oops A'); }); scheduleCallback(ImmediatePriority, () => { - Scheduler.unstable_yieldValue('B'); + Scheduler.log('B'); }); scheduleCallback(ImmediatePriority, () => { - Scheduler.unstable_yieldValue('C'); + Scheduler.log('C'); throw new Error('Oops C'); }); - expect(() => expect(Scheduler).toFlushExpired()).toThrow('Oops A'); - expect(Scheduler).toHaveYielded(['A']); + expect(() => Scheduler.unstable_flushExpired()).toThrow('Oops A'); + assertLog(['A']); // B and C flush in a subsequent event. That way, the second error is not // swallowed. - expect(() => expect(Scheduler).toFlushExpired()).toThrow('Oops C'); - expect(Scheduler).toHaveYielded(['B', 'C']); + expect(() => Scheduler.unstable_flushExpired()).toThrow('Oops C'); + assertLog(['B', 'C']); }); it('multiple immediate callbacks can throw and there will be an error for each one', () => { @@ -428,19 +418,19 @@ describe('Scheduler', () => { }); it('exposes the current priority level', () => { - Scheduler.unstable_yieldValue(getCurrentPriorityLevel()); + Scheduler.log(getCurrentPriorityLevel()); runWithPriority(ImmediatePriority, () => { - Scheduler.unstable_yieldValue(getCurrentPriorityLevel()); + Scheduler.log(getCurrentPriorityLevel()); runWithPriority(NormalPriority, () => { - Scheduler.unstable_yieldValue(getCurrentPriorityLevel()); + Scheduler.log(getCurrentPriorityLevel()); runWithPriority(UserBlockingPriority, () => { - Scheduler.unstable_yieldValue(getCurrentPriorityLevel()); + Scheduler.log(getCurrentPriorityLevel()); }); }); - Scheduler.unstable_yieldValue(getCurrentPriorityLevel()); + Scheduler.log(getCurrentPriorityLevel()); }); - expect(Scheduler).toHaveYielded([ + assertLog([ NormalPriority, ImmediatePriority, NormalPriority, @@ -454,7 +444,7 @@ describe('Scheduler', () => { // priority if you have sourcemaps. // TODO: Feature temporarily disabled while we investigate a bug in one of // our minifiers. - it.skip('adds extra function to the JS stack whose name includes the priority level', () => { + it.skip('adds extra function to the JS stack whose name includes the priority level', async () => { function inferPriorityFromCallstack() { try { throw Error(); @@ -487,28 +477,22 @@ describe('Scheduler', () => { } scheduleCallback(ImmediatePriority, () => - Scheduler.unstable_yieldValue( - 'Immediate: ' + inferPriorityFromCallstack(), - ), + Scheduler.log('Immediate: ' + inferPriorityFromCallstack()), ); scheduleCallback(UserBlockingPriority, () => - Scheduler.unstable_yieldValue( - 'UserBlocking: ' + inferPriorityFromCallstack(), - ), + Scheduler.log('UserBlocking: ' + inferPriorityFromCallstack()), ); scheduleCallback(NormalPriority, () => - Scheduler.unstable_yieldValue( - 'Normal: ' + inferPriorityFromCallstack(), - ), + Scheduler.log('Normal: ' + inferPriorityFromCallstack()), ); scheduleCallback(LowPriority, () => - Scheduler.unstable_yieldValue('Low: ' + inferPriorityFromCallstack()), + Scheduler.log('Low: ' + inferPriorityFromCallstack()), ); scheduleCallback(IdlePriority, () => - Scheduler.unstable_yieldValue('Idle: ' + inferPriorityFromCallstack()), + Scheduler.log('Idle: ' + inferPriorityFromCallstack()), ); - expect(Scheduler).toFlushAndYield([ + await waitForAll([ 'Immediate: ' + ImmediatePriority, 'UserBlocking: ' + UserBlockingPriority, 'Normal: ' + NormalPriority, @@ -519,134 +503,99 @@ describe('Scheduler', () => { } describe('delayed tasks', () => { - it('schedules a delayed task', () => { - scheduleCallback( - NormalPriority, - () => Scheduler.unstable_yieldValue('A'), - { - delay: 1000, - }, - ); + it('schedules a delayed task', async () => { + scheduleCallback(NormalPriority, () => Scheduler.log('A'), { + delay: 1000, + }); // Should flush nothing, because delay hasn't elapsed - expect(Scheduler).toFlushAndYield([]); + await waitForAll([]); // Advance time until right before the threshold Scheduler.unstable_advanceTime(999); // Still nothing - expect(Scheduler).toFlushAndYield([]); + await waitForAll([]); // Advance time past the threshold Scheduler.unstable_advanceTime(1); // Now it should flush like normal - expect(Scheduler).toFlushAndYield(['A']); + await waitForAll(['A']); }); - it('schedules multiple delayed tasks', () => { - scheduleCallback( - NormalPriority, - () => Scheduler.unstable_yieldValue('C'), - { - delay: 300, - }, - ); + it('schedules multiple delayed tasks', async () => { + scheduleCallback(NormalPriority, () => Scheduler.log('C'), { + delay: 300, + }); - scheduleCallback( - NormalPriority, - () => Scheduler.unstable_yieldValue('B'), - { - delay: 200, - }, - ); + scheduleCallback(NormalPriority, () => Scheduler.log('B'), { + delay: 200, + }); - scheduleCallback( - NormalPriority, - () => Scheduler.unstable_yieldValue('D'), - { - delay: 400, - }, - ); + scheduleCallback(NormalPriority, () => Scheduler.log('D'), { + delay: 400, + }); - scheduleCallback( - NormalPriority, - () => Scheduler.unstable_yieldValue('A'), - { - delay: 100, - }, - ); + scheduleCallback(NormalPriority, () => Scheduler.log('A'), { + delay: 100, + }); // Should flush nothing, because delay hasn't elapsed - expect(Scheduler).toFlushAndYield([]); + await waitForAll([]); // Advance some time. Scheduler.unstable_advanceTime(200); // Both A and B are no longer delayed. They can now flush incrementally. - expect(Scheduler).toFlushAndYieldThrough(['A']); - expect(Scheduler).toFlushAndYield(['B']); + await waitFor(['A']); + await waitForAll(['B']); // Advance the rest Scheduler.unstable_advanceTime(200); - expect(Scheduler).toFlushAndYield(['C', 'D']); + await waitForAll(['C', 'D']); }); - it('interleaves normal tasks and delayed tasks', () => { + it('interleaves normal tasks and delayed tasks', async () => { // Schedule some high priority callbacks with a delay. When their delay // elapses, they will be the most important callback in the queue. - scheduleCallback( - UserBlockingPriority, - () => Scheduler.unstable_yieldValue('Timer 2'), - {delay: 300}, - ); - scheduleCallback( - UserBlockingPriority, - () => Scheduler.unstable_yieldValue('Timer 1'), - {delay: 100}, - ); + scheduleCallback(UserBlockingPriority, () => Scheduler.log('Timer 2'), { + delay: 300, + }); + scheduleCallback(UserBlockingPriority, () => Scheduler.log('Timer 1'), { + delay: 100, + }); // Schedule some tasks at default priority. scheduleCallback(NormalPriority, () => { - Scheduler.unstable_yieldValue('A'); + Scheduler.log('A'); Scheduler.unstable_advanceTime(100); }); scheduleCallback(NormalPriority, () => { - Scheduler.unstable_yieldValue('B'); + Scheduler.log('B'); Scheduler.unstable_advanceTime(100); }); scheduleCallback(NormalPriority, () => { - Scheduler.unstable_yieldValue('C'); + Scheduler.log('C'); Scheduler.unstable_advanceTime(100); }); scheduleCallback(NormalPriority, () => { - Scheduler.unstable_yieldValue('D'); + Scheduler.log('D'); Scheduler.unstable_advanceTime(100); }); // Flush all the work. The timers should be interleaved with the // other tasks. - expect(Scheduler).toFlushAndYield([ - 'A', - 'Timer 1', - 'B', - 'C', - 'Timer 2', - 'D', - ]); + await waitForAll(['A', 'Timer 1', 'B', 'C', 'Timer 2', 'D']); }); - it('interleaves delayed tasks with time-sliced tasks', () => { + it('interleaves delayed tasks with time-sliced tasks', async () => { // Schedule some high priority callbacks with a delay. When their delay // elapses, they will be the most important callback in the queue. - scheduleCallback( - UserBlockingPriority, - () => Scheduler.unstable_yieldValue('Timer 2'), - {delay: 300}, - ); - scheduleCallback( - UserBlockingPriority, - () => Scheduler.unstable_yieldValue('Timer 1'), - {delay: 100}, - ); + scheduleCallback(UserBlockingPriority, () => Scheduler.log('Timer 2'), { + delay: 300, + }); + scheduleCallback(UserBlockingPriority, () => Scheduler.log('Timer 1'), { + delay: 100, + }); // Schedule a time-sliced task at default priority. const tasks = [ @@ -660,7 +609,7 @@ describe('Scheduler', () => { const task = tasks.shift(); const [label, ms] = task; Scheduler.unstable_advanceTime(ms); - Scheduler.unstable_yieldValue(label); + Scheduler.log(label); if (tasks.length > 0) { return work; } @@ -670,38 +619,27 @@ describe('Scheduler', () => { // Flush all the work. The timers should be interleaved with the // other tasks. - expect(Scheduler).toFlushAndYield([ - 'A', - 'Timer 1', - 'B', - 'C', - 'Timer 2', - 'D', - ]); + await waitForAll(['A', 'Timer 1', 'B', 'C', 'Timer 2', 'D']); }); - it('cancels a delayed task', () => { + it('cancels a delayed task', async () => { // Schedule several tasks with the same delay const options = {delay: 100}; - scheduleCallback( - NormalPriority, - () => Scheduler.unstable_yieldValue('A'), - options, - ); + scheduleCallback(NormalPriority, () => Scheduler.log('A'), options); const taskB = scheduleCallback( NormalPriority, - () => Scheduler.unstable_yieldValue('B'), + () => Scheduler.log('B'), options, ); const taskC = scheduleCallback( NormalPriority, - () => Scheduler.unstable_yieldValue('C'), + () => Scheduler.log('C'), options, ); // Cancel B before its delay has elapsed - expect(Scheduler).toFlushAndYield([]); + await waitForAll([]); cancelCallback(taskB); // Cancel C after its delay has elapsed @@ -709,34 +647,34 @@ describe('Scheduler', () => { cancelCallback(taskC); // Only A should flush - expect(Scheduler).toFlushAndYield(['A']); + await waitForAll(['A']); }); - it('gracefully handles scheduled tasks that are not a function', () => { + it('gracefully handles scheduled tasks that are not a function', async () => { scheduleCallback(ImmediatePriority, null); - expect(Scheduler).toFlushWithoutYielding(); + await waitForAll([]); scheduleCallback(ImmediatePriority, undefined); - expect(Scheduler).toFlushWithoutYielding(); + await waitForAll([]); scheduleCallback(ImmediatePriority, {}); - expect(Scheduler).toFlushWithoutYielding(); + await waitForAll([]); scheduleCallback(ImmediatePriority, 42); - expect(Scheduler).toFlushWithoutYielding(); + await waitForAll([]); }); - it('toFlushUntilNextPaint stops if a continuation is returned', () => { + it('toFlushUntilNextPaint stops if a continuation is returned', async () => { scheduleCallback(NormalPriority, () => { - Scheduler.unstable_yieldValue('Original Task'); - Scheduler.unstable_yieldValue('shouldYield: ' + shouldYield()); - Scheduler.unstable_yieldValue('Return a continuation'); + Scheduler.log('Original Task'); + Scheduler.log('shouldYield: ' + shouldYield()); + Scheduler.log('Return a continuation'); return () => { - Scheduler.unstable_yieldValue('Continuation Task'); + Scheduler.log('Continuation Task'); }; }); - expect(Scheduler).toFlushUntilNextPaint([ + await waitForPaint([ 'Original Task', // Immediately before returning a continuation, `shouldYield` returns // false, which means there must be time remaining in the frame. @@ -750,20 +688,20 @@ describe('Scheduler', () => { expect(Scheduler.unstable_now()).toBe(0); // Continue the task - expect(Scheduler).toFlushAndYield(['Continuation Task']); + await waitForAll(['Continuation Task']); }); - it("toFlushAndYield keeps flushing even if there's a continuation", () => { + it("toFlushAndYield keeps flushing even if there's a continuation", async () => { scheduleCallback(NormalPriority, () => { - Scheduler.unstable_yieldValue('Original Task'); - Scheduler.unstable_yieldValue('shouldYield: ' + shouldYield()); - Scheduler.unstable_yieldValue('Return a continuation'); + Scheduler.log('Original Task'); + Scheduler.log('shouldYield: ' + shouldYield()); + Scheduler.log('Return a continuation'); return () => { - Scheduler.unstable_yieldValue('Continuation Task'); + Scheduler.log('Continuation Task'); }; }); - expect(Scheduler).toFlushAndYield([ + await waitForAll([ 'Original Task', // Immediately before returning a continuation, `shouldYield` returns // false, which means there must be time remaining in the frame. diff --git a/packages/scheduler/src/__tests__/SchedulerPostTask-test.js b/packages/scheduler/src/__tests__/SchedulerPostTask-test.js index 93eaaa14faa7..03952c50b915 100644 --- a/packages/scheduler/src/__tests__/SchedulerPostTask-test.js +++ b/packages/scheduler/src/__tests__/SchedulerPostTask-test.js @@ -83,7 +83,7 @@ describe('SchedulerPostTask', () => { const scheduler = {}; global.scheduler = scheduler; - scheduler.postTask = function(callback, {priority, signal}) { + scheduler.postTask = function (callback, {priority, signal}) { const id = idCounter++; log( `Post Task ${id} [${priority === undefined ? '' : priority}]`, diff --git a/packages/scheduler/src/__tests__/SchedulerProfiling-test.js b/packages/scheduler/src/__tests__/SchedulerProfiling-test.js index afca0fb410c5..eef602d90ac3 100644 --- a/packages/scheduler/src/__tests__/SchedulerProfiling-test.js +++ b/packages/scheduler/src/__tests__/SchedulerProfiling-test.js @@ -24,6 +24,9 @@ let cancelCallback; // let wrapCallback; // let getCurrentPriorityLevel; // let shouldYield; +let waitForAll; +let waitFor; +let waitForThrow; function priorityLevelToString(priorityLevel) { switch (priorityLevel) { @@ -69,6 +72,11 @@ describe('Scheduler', () => { // wrapCallback = Scheduler.unstable_wrapCallback; // getCurrentPriorityLevel = Scheduler.unstable_getCurrentPriorityLevel; // shouldYield = Scheduler.unstable_shouldYield; + + const InternalTestUtils = require('internal-test-utils'); + waitForAll = InternalTestUtils.waitForAll; + waitFor = InternalTestUtils.waitFor; + waitForThrow = InternalTestUtils.waitForThrow; }); const TaskStartEvent = 1; @@ -81,7 +89,8 @@ describe('Scheduler', () => { const SchedulerResumeEvent = 8; function stopProfilingAndPrintFlamegraph() { - const eventBuffer = Scheduler.unstable_Profiling.stopLoggingProfilingEvents(); + const eventBuffer = + Scheduler.unstable_Profiling.stopLoggingProfilingEvents(); if (eventBuffer === null) { return '(empty profile)'; } @@ -253,7 +262,7 @@ describe('Scheduler', () => { return '\n' + result; } - it('creates a basic flamegraph', () => { + it('creates a basic flamegraph', async () => { Scheduler.unstable_Profiling.startLoggingProfilingEvents(); Scheduler.unstable_advanceTime(100); @@ -261,27 +270,27 @@ describe('Scheduler', () => { NormalPriority, () => { Scheduler.unstable_advanceTime(300); - Scheduler.unstable_yieldValue('Yield 1'); + Scheduler.log('Yield 1'); scheduleCallback( UserBlockingPriority, () => { - Scheduler.unstable_yieldValue('Yield 2'); + Scheduler.log('Yield 2'); Scheduler.unstable_advanceTime(300); }, {label: 'Bar'}, ); Scheduler.unstable_advanceTime(100); - Scheduler.unstable_yieldValue('Yield 3'); + Scheduler.log('Yield 3'); return () => { - Scheduler.unstable_yieldValue('Yield 4'); + Scheduler.log('Yield 4'); Scheduler.unstable_advanceTime(300); }; }, {label: 'Foo'}, ); - expect(Scheduler).toFlushAndYieldThrough(['Yield 1', 'Yield 3']); + await waitFor(['Yield 1', 'Yield 3']); Scheduler.unstable_advanceTime(100); - expect(Scheduler).toFlushAndYield(['Yield 2', 'Yield 4']); + await waitForAll(['Yield 2', 'Yield 4']); expect(stopProfilingAndPrintFlamegraph()).toEqual( ` @@ -292,26 +301,26 @@ Task 1 [Normal] │ ████████░░░░░░░ ); }); - it('marks when a task is canceled', () => { + it('marks when a task is canceled', async () => { Scheduler.unstable_Profiling.startLoggingProfilingEvents(); const task = scheduleCallback(NormalPriority, () => { - Scheduler.unstable_yieldValue('Yield 1'); + Scheduler.log('Yield 1'); Scheduler.unstable_advanceTime(300); - Scheduler.unstable_yieldValue('Yield 2'); + Scheduler.log('Yield 2'); return () => { - Scheduler.unstable_yieldValue('Continuation'); + Scheduler.log('Continuation'); Scheduler.unstable_advanceTime(200); }; }); - expect(Scheduler).toFlushAndYieldThrough(['Yield 1', 'Yield 2']); + await waitFor(['Yield 1', 'Yield 2']); Scheduler.unstable_advanceTime(100); cancelCallback(task); Scheduler.unstable_advanceTime(1000); - expect(Scheduler).toFlushWithoutYielding(); + await waitForAll([]); expect(stopProfilingAndPrintFlamegraph()).toEqual( ` !!! Main thread │░░░░░░██████████████████████ @@ -320,7 +329,7 @@ Task 1 [Normal] │██████░░🡐 canceled ); }); - it('marks when a task errors', () => { + it('marks when a task errors', async () => { Scheduler.unstable_Profiling.startLoggingProfilingEvents(); scheduleCallback(NormalPriority, () => { @@ -328,11 +337,11 @@ Task 1 [Normal] │██████░░🡐 canceled throw Error('Oops'); }); - expect(Scheduler).toFlushAndThrow('Oops'); + await waitForThrow('Oops'); Scheduler.unstable_advanceTime(100); Scheduler.unstable_advanceTime(1000); - expect(Scheduler).toFlushWithoutYielding(); + await waitForAll([]); expect(stopProfilingAndPrintFlamegraph()).toEqual( ` !!! Main thread │░░░░░░██████████████████████ @@ -341,29 +350,29 @@ Task 1 [Normal] │██████🡐 errored ); }); - it('marks when multiple tasks are canceled', () => { + it('marks when multiple tasks are canceled', async () => { Scheduler.unstable_Profiling.startLoggingProfilingEvents(); const task1 = scheduleCallback(NormalPriority, () => { - Scheduler.unstable_yieldValue('Yield 1'); + Scheduler.log('Yield 1'); Scheduler.unstable_advanceTime(300); - Scheduler.unstable_yieldValue('Yield 2'); + Scheduler.log('Yield 2'); return () => { - Scheduler.unstable_yieldValue('Continuation'); + Scheduler.log('Continuation'); Scheduler.unstable_advanceTime(200); }; }); const task2 = scheduleCallback(NormalPriority, () => { - Scheduler.unstable_yieldValue('Yield 3'); + Scheduler.log('Yield 3'); Scheduler.unstable_advanceTime(300); - Scheduler.unstable_yieldValue('Yield 4'); + Scheduler.log('Yield 4'); return () => { - Scheduler.unstable_yieldValue('Continuation'); + Scheduler.log('Continuation'); Scheduler.unstable_advanceTime(200); }; }); - expect(Scheduler).toFlushAndYieldThrough(['Yield 1', 'Yield 2']); + await waitFor(['Yield 1', 'Yield 2']); Scheduler.unstable_advanceTime(100); cancelCallback(task1); @@ -372,7 +381,7 @@ Task 1 [Normal] │██████🡐 errored // Advance more time. This should not affect the size of the main // thread row, since the Scheduler queue is empty. Scheduler.unstable_advanceTime(1000); - expect(Scheduler).toFlushWithoutYielding(); + await waitForAll([]); // The main thread row should end when the callback is cancelled. expect(stopProfilingAndPrintFlamegraph()).toEqual( @@ -384,14 +393,14 @@ Task 2 [Normal] │░░░░░░░░🡐 canceled ); }); - it('handles cancelling a task that already finished', () => { + it('handles cancelling a task that already finished', async () => { Scheduler.unstable_Profiling.startLoggingProfilingEvents(); const task = scheduleCallback(NormalPriority, () => { - Scheduler.unstable_yieldValue('A'); + Scheduler.log('A'); Scheduler.unstable_advanceTime(1000); }); - expect(Scheduler).toFlushAndYield(['A']); + await waitForAll(['A']); cancelCallback(task); expect(stopProfilingAndPrintFlamegraph()).toEqual( ` @@ -401,13 +410,13 @@ Task 1 [Normal] │████████████████ ); }); - it('handles cancelling a task multiple times', () => { + it('handles cancelling a task multiple times', async () => { Scheduler.unstable_Profiling.startLoggingProfilingEvents(); scheduleCallback( NormalPriority, () => { - Scheduler.unstable_yieldValue('A'); + Scheduler.log('A'); Scheduler.unstable_advanceTime(1000); }, {label: 'A'}, @@ -416,7 +425,7 @@ Task 1 [Normal] │████████████████ const task = scheduleCallback( NormalPriority, () => { - Scheduler.unstable_yieldValue('B'); + Scheduler.log('B'); Scheduler.unstable_advanceTime(1000); }, {label: 'B'}, @@ -425,7 +434,7 @@ Task 1 [Normal] │████████████████ cancelCallback(task); cancelCallback(task); cancelCallback(task); - expect(Scheduler).toFlushAndYield(['A']); + await waitForAll(['A']); expect(stopProfilingAndPrintFlamegraph()).toEqual( ` !!! Main thread │████████████░░░░░░░░░░░░░░░░░░░░ @@ -435,23 +444,23 @@ Task 2 [Normal] │ ░░░░░░░░🡐 canceled ); }); - it('handles delayed tasks', () => { + it('handles delayed tasks', async () => { Scheduler.unstable_Profiling.startLoggingProfilingEvents(); scheduleCallback( NormalPriority, () => { Scheduler.unstable_advanceTime(1000); - Scheduler.unstable_yieldValue('A'); + Scheduler.log('A'); }, { delay: 1000, }, ); - expect(Scheduler).toFlushWithoutYielding(); + await waitForAll([]); Scheduler.unstable_advanceTime(1000); - expect(Scheduler).toFlushAndYield(['A']); + await waitForAll(['A']); expect(stopProfilingAndPrintFlamegraph()).toEqual( ` @@ -461,15 +470,13 @@ Task 1 [Normal] │ █████████ ); }); - it('handles cancelling a delayed task', () => { + it('handles cancelling a delayed task', async () => { Scheduler.unstable_Profiling.startLoggingProfilingEvents(); - const task = scheduleCallback( - NormalPriority, - () => Scheduler.unstable_yieldValue('A'), - {delay: 1000}, - ); + const task = scheduleCallback(NormalPriority, () => Scheduler.log('A'), { + delay: 1000, + }); cancelCallback(task); - expect(Scheduler).toFlushWithoutYielding(); + await waitForAll([]); expect(stopProfilingAndPrintFlamegraph()).toEqual( ` !!! Main thread │ @@ -480,22 +487,22 @@ Task 1 [Normal] │ █████████ it('automatically stops profiling and warns if event log gets too big', async () => { Scheduler.unstable_Profiling.startLoggingProfilingEvents(); - spyOnDevAndProd(console, 'error'); + spyOnDevAndProd(console, 'error').mockImplementation(() => {}); // Increase infinite loop guard limit const originalMaxIterations = global.__MAX_ITERATIONS__; global.__MAX_ITERATIONS__ = 120000; let taskId = 1; - while (console.error.calls.count() === 0) { + while (console.error.mock.calls.length === 0) { taskId++; const task = scheduleCallback(NormalPriority, () => {}); cancelCallback(task); - expect(Scheduler).toFlushAndYield([]); + Scheduler.unstable_flushAll(); } expect(console.error).toHaveBeenCalledTimes(1); - expect(console.error.calls.argsFor(0)[0]).toBe( + expect(console.error.mock.calls[0][0]).toBe( "Scheduler Profiling: Event log exceeded maximum size. Don't forget " + 'to call `stopLoggingProfilingEvents()`.', ); @@ -508,7 +515,7 @@ Task 1 [Normal] │ █████████ scheduleCallback(NormalPriority, () => { Scheduler.unstable_advanceTime(1000); }); - expect(Scheduler).toFlushAndYield([]); + await waitForAll([]); // Note: The exact task id is not super important. That just how many tasks // it happens to take before the array is resized. diff --git a/packages/scheduler/src/__tests__/SchedulerSetImmediate-test.js b/packages/scheduler/src/__tests__/SchedulerSetImmediate-test.js index cd6f5df4efe2..52b71b569f41 100644 --- a/packages/scheduler/src/__tests__/SchedulerSetImmediate-test.js +++ b/packages/scheduler/src/__tests__/SchedulerSetImmediate-test.js @@ -76,7 +76,7 @@ describe('SchedulerDOMSetImmediate', () => { }; // Unused: we expect setImmediate to be preferred. - global.MessageChannel = function() { + global.MessageChannel = function () { return { port1: {}, port2: { @@ -88,7 +88,7 @@ describe('SchedulerDOMSetImmediate', () => { }; let pendingSetImmediateCallback = null; - global.setImmediate = function(cb) { + global.setImmediate = function (cb) { if (pendingSetImmediateCallback) { throw Error('Message event already scheduled'); } diff --git a/packages/scheduler/src/__tests__/SchedulerSetTimeout-test.js b/packages/scheduler/src/__tests__/SchedulerSetTimeout-test.js index 04185a32393e..986d329632c2 100644 --- a/packages/scheduler/src/__tests__/SchedulerSetTimeout-test.js +++ b/packages/scheduler/src/__tests__/SchedulerSetTimeout-test.js @@ -23,6 +23,7 @@ describe('SchedulerNoDOM', () => { jest.resetModules(); jest.useFakeTimers(); delete global.setImmediate; + delete global.MessageChannel; jest.unmock('scheduler'); Scheduler = require('scheduler'); diff --git a/packages/scheduler/src/__tests__/SchedulerUMDBundle-test.internal.js b/packages/scheduler/src/__tests__/SchedulerUMDBundle-test.internal.js index d0f096e3c286..2909a5895ab7 100644 --- a/packages/scheduler/src/__tests__/SchedulerUMDBundle-test.internal.js +++ b/packages/scheduler/src/__tests__/SchedulerUMDBundle-test.internal.js @@ -42,8 +42,8 @@ describe('Scheduling UMD bundle', () => { const umdAPIDev = require('../../npm/umd/scheduler.development'); const umdAPIProd = require('../../npm/umd/scheduler.production.min'); const umdAPIProfiling = require('../../npm/umd/scheduler.profiling.min'); - const secretAPI = require('react/src/forks/ReactSharedInternals.umd') - .default; + const secretAPI = + require('react/src/forks/ReactSharedInternals.umd').default; validateForwardedAPIs(api, [ umdAPIDev, umdAPIProd, diff --git a/packages/scheduler/src/forks/Scheduler.js b/packages/scheduler/src/forks/Scheduler.js index d43f457c2d68..09c0d26d7ff6 100644 --- a/packages/scheduler/src/forks/Scheduler.js +++ b/packages/scheduler/src/forks/Scheduler.js @@ -329,7 +329,8 @@ function unstable_next(eventHandler: () => T): T { function unstable_wrapCallback) => mixed>(callback: T): T { var parentPriorityLevel = currentPriorityLevel; // $FlowFixMe[incompatible-return] - return function() { + // $FlowFixMe[missing-this-annot] + return function () { // This is a fork of runWithPriority, inlined for performance. var previousPriorityLevel = currentPriorityLevel; currentPriorityLevel = parentPriorityLevel; @@ -629,7 +630,9 @@ if (typeof localSetImmediate === 'function') { }; } -function requestHostCallback(callback) { +function requestHostCallback( + callback: (hasTimeRemaining: boolean, initialTime: number) => boolean, +) { scheduledHostCallback = callback; if (!isMessageLoopRunning) { isMessageLoopRunning = true; @@ -637,7 +640,10 @@ function requestHostCallback(callback) { } } -function requestHostTimeout(callback, ms: number) { +function requestHostTimeout( + callback: (currentTime: number) => void, + ms: number, +) { // $FlowFixMe[not-a-function] nullable value taskTimeoutID = localSetTimeout(() => { callback(getCurrentTime()); diff --git a/packages/scheduler/src/forks/SchedulerMock.js b/packages/scheduler/src/forks/SchedulerMock.js index e276aab0ed6b..7f148c45be3e 100644 --- a/packages/scheduler/src/forks/SchedulerMock.js +++ b/packages/scheduler/src/forks/SchedulerMock.js @@ -66,7 +66,7 @@ var LOW_PRIORITY_TIMEOUT = 10000; var IDLE_PRIORITY_TIMEOUT = maxSigned31BitInt; // Tasks are stored on a min heap -var taskQueue = []; +var taskQueue: Array = []; var timerQueue: Array = []; // Incrementing id counter. Used to maintain insertion order. @@ -322,7 +322,8 @@ function unstable_next(eventHandler: () => T): T { function unstable_wrapCallback) => mixed>(callback: T): T { var parentPriorityLevel = currentPriorityLevel; // $FlowFixMe[incompatible-return] - return function() { + // $FlowFixMe[missing-this-annot] + return function () { // This is a fork of runWithPriority, inlined for performance. var previousPriorityLevel = currentPriorityLevel; currentPriorityLevel = parentPriorityLevel; @@ -605,7 +606,7 @@ function unstable_flushAllWithoutAsserting(): boolean { } } -function unstable_clearYields(): Array { +function unstable_clearLog(): Array { if (yieldedValues === null) { return []; } @@ -631,7 +632,7 @@ function unstable_flushAll(): void { } } -function unstable_yieldValue(value: mixed): void { +function log(value: mixed): void { // eslint-disable-next-line react-internal/no-production-logging if (console.log.name === 'disabledLog' || disableYieldValue) { // If console.log has been patched, we assume we're in render @@ -686,11 +687,11 @@ export { unstable_flushAllWithoutAsserting, unstable_flushNumberOfYields, unstable_flushExpired, - unstable_clearYields, + unstable_clearLog, unstable_flushUntilNextPaint, unstable_hasPendingWork, unstable_flushAll, - unstable_yieldValue, + log, unstable_advanceTime, reset, setDisableYieldValue as unstable_setDisableYieldValue, diff --git a/packages/scheduler/src/forks/SchedulerPostTask.js b/packages/scheduler/src/forks/SchedulerPostTask.js index f78996f8028c..1e3788239465 100644 --- a/packages/scheduler/src/forks/SchedulerPostTask.js +++ b/packages/scheduler/src/forks/SchedulerPostTask.js @@ -67,9 +67,7 @@ export function unstable_requestPaint() { // Since we yield every frame regardless, `requestPaint` has no effect. } -type SchedulerCallback = ( - didTimeout_DEPRECATED: boolean, -) => +type SchedulerCallback = (didTimeout_DEPRECATED: boolean) => | T // May return a continuation | SchedulerCallback; @@ -168,7 +166,7 @@ function runTask( } } -function handleAbortError(error) { +function handleAbortError(error: any) { // Abort errors are an implementation detail. We don't expose the // TaskController to the user, nor do we expose the promise that is returned // from `postTask`. So we should suppress them, since there's no way for the diff --git a/packages/shared/CheckStringCoercion.js b/packages/shared/CheckStringCoercion.js index 8aca56b11787..a229ac6dae9d 100644 --- a/packages/shared/CheckStringCoercion.js +++ b/packages/shared/CheckStringCoercion.js @@ -17,7 +17,7 @@ * of the `value` object). */ -// $FlowFixMe only called in DEV, so void return is not possible. +// $FlowFixMe[incompatible-return] only called in DEV, so void return is not possible. function typeName(value: mixed): string { if (__DEV__) { // toStringTag is needed for namespaced types like Temporal.Instant @@ -26,12 +26,12 @@ function typeName(value: mixed): string { (hasToStringTag && (value: any)[Symbol.toStringTag]) || (value: any).constructor.name || 'Object'; - // $FlowFixMe + // $FlowFixMe[incompatible-return] return type; } } -// $FlowFixMe only called in DEV, so void return is not possible. +// $FlowFixMe[incompatible-return] only called in DEV, so void return is not possible. function willCoercionThrow(value: mixed): boolean { if (__DEV__) { try { diff --git a/packages/shared/ConsolePatchingDev.js b/packages/shared/ConsolePatchingDev.js index 728dd0b1a1cc..9af70b37adb2 100644 --- a/packages/shared/ConsolePatchingDev.js +++ b/packages/shared/ConsolePatchingDev.js @@ -42,7 +42,7 @@ export function disableLogs(): void { value: disabledLog, writable: true, }; - // $FlowFixMe Flow thinks console is immutable. + // $FlowFixMe[cannot-write] Flow thinks console is immutable. Object.defineProperties(console, { info: props, log: props, @@ -68,7 +68,7 @@ export function reenableLogs(): void { enumerable: true, writable: true, }; - // $FlowFixMe Flow thinks console is immutable. + // $FlowFixMe[cannot-write] Flow thinks console is immutable. Object.defineProperties(console, { log: {...props, value: prevLog}, info: {...props, value: prevInfo}, diff --git a/packages/shared/ReactComponentStackFrame.js b/packages/shared/ReactComponentStackFrame.js index 4af78fb2b6d2..9f89bad3875e 100644 --- a/packages/shared/ReactComponentStackFrame.js +++ b/packages/shared/ReactComponentStackFrame.js @@ -10,10 +10,7 @@ import type {Source} from 'shared/ReactElementType'; import type {LazyComponent} from 'react/src/ReactLazy'; -import { - enableComponentStackLocations, - disableNativeComponentFrames, -} from 'shared/ReactFeatureFlags'; +import {enableComponentStackLocations} from 'shared/ReactFeatureFlags'; import { REACT_SUSPENSE_TYPE, @@ -60,7 +57,7 @@ let reentry = false; let componentFrameCache; if (__DEV__) { const PossiblyWeakMap = typeof WeakMap === 'function' ? WeakMap : Map; - componentFrameCache = new PossiblyWeakMap(); + componentFrameCache = new PossiblyWeakMap(); } export function describeNativeComponentFrame( @@ -68,7 +65,7 @@ export function describeNativeComponentFrame( construct: boolean, ): string { // If something asked for a stack inside a fake render, it should get ignored. - if (disableNativeComponentFrames || !fn || reentry) { + if (!fn || reentry) { return ''; } @@ -83,7 +80,7 @@ export function describeNativeComponentFrame( reentry = true; const previousPrepareStackTrace = Error.prepareStackTrace; - // $FlowFixMe It does accept undefined. + // $FlowFixMe[incompatible-type] It does accept undefined. Error.prepareStackTrace = undefined; let previousDispatcher; if (__DEV__) { @@ -97,12 +94,12 @@ export function describeNativeComponentFrame( // This should throw. if (construct) { // Something should be setting the props in the constructor. - const Fake = function() { + const Fake = function () { throw Error(); }; - // $FlowFixMe + // $FlowFixMe[prop-missing] Object.defineProperty(Fake.prototype, 'props', { - set: function() { + set: function () { // We use a throwing setter instead of frozen or non-writable props // because that won't throw in a non-strict mode function. throw Error(); diff --git a/packages/shared/ReactErrorUtils.js b/packages/shared/ReactErrorUtils.js index b29110220736..b2d9cde7b712 100644 --- a/packages/shared/ReactErrorUtils.js +++ b/packages/shared/ReactErrorUtils.js @@ -72,6 +72,7 @@ export function invokeGuardedCallbackAndCatchFirstError< F, Context, >( + this: mixed, name: string | null, func: (a: A, b: B, c: C, d: D, e: E, f: F) => void, context: Context, diff --git a/packages/shared/ReactFeatureFlags.js b/packages/shared/ReactFeatureFlags.js index d3c128ae77bb..619b4bf152a9 100644 --- a/packages/shared/ReactFeatureFlags.js +++ b/packages/shared/ReactFeatureFlags.js @@ -13,9 +13,19 @@ // Flags that can likely be deleted or landed without consequences // ----------------------------------------------------------------------------- -export const warnAboutDeprecatedLifecycles = true; export const enableComponentStackLocations = true; -export const disableSchedulerTimeoutBasedOnReactExpirationTime = false; + +// ----------------------------------------------------------------------------- +// Killswitch +// +// Flags that exist solely to turn off a change in case it causes a regression +// when it rolls out to prod. We should remove these as soon as possible. +// ----------------------------------------------------------------------------- + +// This is phrased as a negative so that if someone forgets to add a GK, the +// default is to enable the feature. It should only be overridden if there's +// a regression in prod. +export const revertRemovalOfSiblingPrerendering = false; // ----------------------------------------------------------------------------- // Land or remove (moderate effort) @@ -24,16 +34,9 @@ export const disableSchedulerTimeoutBasedOnReactExpirationTime = false; // like migrating internal callers or performance testing. // ----------------------------------------------------------------------------- -// This rolled out to 10% public in www, so we should be able to land, but some -// internal tests need to be updated. The open source behavior is correct. -export const skipUnmountedBoundaries = true; - // TODO: Finish rolling out in www export const enableClientRenderFallbackOnTextMismatch = true; -// TODO: Need to review this code one more time before landing -export const enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay = true; - // Recoil still uses useMutableSource in www, need to delete export const enableUseMutableSource = false; @@ -45,6 +48,10 @@ export const enableSchedulerDebugging = false; // Need to remove didTimeout argument from Scheduler before landing export const disableSchedulerTimeoutInWorkLoop = false; +// This will break some internal tests at Meta so we need to gate this until +// those can be fixed. +export const enableDeferRootSchedulingToMicrotask = true; + // ----------------------------------------------------------------------------- // Slated for removal in the future (significant effort) // @@ -102,31 +109,18 @@ export const enableHostSingletons = true; export const enableFloat = true; -// When a node is unmounted, recurse into the Fiber subtree and clean out -// references. Each level cleans up more fiber fields than the previous level. -// As far as we know, React itself doesn't leak, but because the Fiber contains -// cycles, even a single leak in product code can cause us to retain large -// amounts of memory. -// -// The long term plan is to remove the cycles, but in the meantime, we clear -// additional fields to mitigate. -// -// It's an enum so that we can experiment with different levels of -// aggressiveness. -export const deletedTreeCleanUpLevel = 3; - export const enableUseHook = true; // Enables unstable_useMemoCache hook, intended as a compilation target for // auto-memoization. export const enableUseMemoCacheHook = __EXPERIMENTAL__; -export const enableUseEventHook = __EXPERIMENTAL__; +export const enableUseEffectEventHook = __EXPERIMENTAL__; // Test in www before enabling in open source. // Enables DOM-server to stream its instruction set as data-attributes // (handled with an MutationObserver) instead of inline-scripts -export const enableFizzExternalRuntime = false; +export const enableFizzExternalRuntime = true; // ----------------------------------------------------------------------------- // Chopping Block @@ -148,21 +142,12 @@ export const disableLegacyContext = false; export const enableUseRefAccessWarning = false; -// Enables time slicing for updates that aren't wrapped in startTransition. -export const enableSyncDefaultUpdates = true; +export const enableUnifiedSyncLane = __EXPERIMENTAL__; // Adds an opt-in to time slicing for updates that aren't wrapped in // startTransition. Only relevant when enableSyncDefaultUpdates is disabled. export const allowConcurrentByDefault = false; -// Updates that occur in the render phase are not officially supported. But when -// they do occur, we defer them to a subsequent render by picking a lane that's -// not currently rendering. We treat them the same as if they came from an -// interleaved event. Remove this flag once we have migrated to the -// new behavior. -// NOTE: Not sure if we'll end up doing this or not. -export const deferRenderPhaseUpdateToNextBatch = false; - // ----------------------------------------------------------------------------- // React DOM Chopping Block // @@ -183,10 +168,13 @@ export const enableTrustedTypesIntegration = false; // DOM properties export const disableInputAttributeSyncing = false; +// Remove IE and MsApp specific workarounds for innerHTML +export const disableIEWorkarounds = __EXPERIMENTAL__; + // Filter certain DOM attributes (e.g. src, href) if their values are empty // strings. This prevents e.g. from making an unnecessary HTTP // request for certain browsers. -export const enableFilterEmptyStringAttributesDOM = false; +export const enableFilterEmptyStringAttributesDOM = __EXPERIMENTAL__; // Changes the behavior for rendering custom elements in both server rendering // and client rendering, mostly to allow JSX attributes to apply to the custom @@ -197,26 +185,6 @@ export const enableCustomElementPropertySupport = __EXPERIMENTAL__; // Disables children for