diff --git a/.circleci/config.yml b/.circleci/config.yml index 18372084d3..37544484f2 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -7,32 +7,6 @@ aliases: - &environment TZ: /usr/share/zoneinfo/America/Los_Angeles - - &restore_yarn_cache_fixtures_dom - restore_cache: - name: Restore yarn cache for fixtures/dom - keys: - - v2-yarn_cache-{{ arch }}-{{ checksum "yarn.lock" }}-fixtures/dom - - - &yarn_install_fixtures_dom - run: - name: Install dependencies in fixtures/dom - working_directory: fixtures/dom - command: yarn install --frozen-lockfile --cache-folder ~/.cache/yarn - - - &yarn_install_fixtures_dom_retry - run: - name: Install dependencies in fixtures/dom (retry) - when: on_fail - working_directory: fixtures/dom - command: yarn install --frozen-lockfile --cache-folder ~/.cache/yarn - - - &save_yarn_cache_fixtures_dom - save_cache: - name: Save yarn cache for fixtures/dom - key: v2-yarn_cache-{{ arch }}-{{ checksum "yarn.lock" }}-fixtures/dom - paths: - - ~/.cache/yarn - - &TEST_PARALLELISM 20 - &attach_workspace @@ -97,7 +71,7 @@ jobs: steps: - checkout - setup_node_modules - - run: yarn build + - run: yarn build --ci=circleci - persist_to_workspace: root: . paths: @@ -222,52 +196,6 @@ jobs: RELEASE_CHANNEL: experimental command: ./scripts/circleci/run_devtools_e2e_tests.js - run_fixtures_flight_tests: - docker: *docker - environment: *environment - steps: - - checkout - - attach_workspace: - at: . - # Fixture copies some built packages from the workroot after install. - # That means dependencies of the built packages are not installed. - # We need to install dependencies of the workroot to fulfill all dependency constraints - - setup_node_modules - - restore_cache: - name: Restore yarn cache of fixture - keys: - - v2-yarn_cache_fixtures_flight-{{ arch }}-{{ checksum "yarn.lock" }} - - run: - name: Install fixture dependencies - working_directory: fixtures/flight - 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 of fixture - key: v2-yarn_cache_fixtures_flight-{{ arch }}-{{ checksum "yarn.lock" }} - paths: - - ~/.cache/yarn - - run: - working_directory: fixtures/flight - name: Playwright install deps - command: | - npx playwright install - sudo npx playwright install-deps - - run: - name: Run tests - working_directory: fixtures/flight - command: yarn test - environment: - # Otherwise the webserver is a blackbox - DEBUG: pw:webserver - - store_artifacts: - path: fixtures/flight/playwright-report - - store_artifacts: - path: fixtures/flight/test-results - run_devtools_tests_for_versions: docker: *docker environment: *environment @@ -311,75 +239,6 @@ jobs: - store_artifacts: path: ./tmp/screenshots - yarn_lint_build: - docker: *docker - environment: *environment - steps: - - checkout - - attach_workspace: - at: . - - setup_node_modules - - run: yarn lint-build - - yarn_check_release_dependencies: - docker: *docker - environment: *environment - steps: - - checkout - - attach_workspace: - at: . - - setup_node_modules - - run: yarn check-release-dependencies - - - check_error_codes: - docker: *docker - environment: *environment - steps: - - checkout - - attach_workspace: *attach_workspace - - setup_node_modules - - run: - name: Search build artifacts for unminified errors - command: | - yarn extract-errors - git diff --quiet || (echo "Found unminified errors. Either update the error codes map or disable error minification for the affected build, if appropriate." && false) - - yarn_test_build: - docker: *docker - environment: *environment - parallelism: *TEST_PARALLELISM - parameters: - args: - type: string - steps: - - checkout - - attach_workspace: - at: . - - setup_node_modules - - run: yarn test --build <> --ci=circleci - - RELEASE_CHANNEL_stable_yarn_test_dom_fixtures: - docker: *docker - environment: *environment - steps: - - checkout - - attach_workspace: - at: . - - setup_node_modules - - *restore_yarn_cache_fixtures_dom - - *yarn_install_fixtures_dom - - *yarn_install_fixtures_dom_retry - - *save_yarn_cache_fixtures_dom - - run: - name: Run DOM fixture tests - environment: - RELEASE_CHANNEL: stable - working_directory: fixtures/dom - command: | - yarn predev - yarn test --maxWorkers=2 - publish_prerelease: parameters: commit_sha: @@ -421,40 +280,6 @@ workflows: requires: - scrape_warning_messages - yarn_build - - yarn_test_build: - requires: - - yarn_build - matrix: - parameters: - args: - # Intentionally passing these as strings instead of creating a - # separate parameter per CLI argument, since it's easier to - # control/see which combinations we want to run. - - "-r=stable --env=development" - - "-r=stable --env=production" - - "-r=experimental --env=development" - - "-r=experimental --env=production" - - # Dev Tools - - "--project=devtools -r=experimental" - - # TODO: Update test config to support www build tests - # - "-r=www-classic --env=development --variant=false" - # - "-r=www-classic --env=production --variant=false" - # - "-r=www-classic --env=development --variant=true" - # - "-r=www-classic --env=production --variant=true" - # - "-r=www-modern --env=development --variant=false" - # - "-r=www-modern --env=production --variant=false" - # - "-r=www-modern --env=development --variant=true" - # - "-r=www-modern --env=production --variant=true" - - # TODO: Update test config to support xplat build tests - # - "-r=xplat --env=development --variant=false" - # - "-r=xplat --env=development --variant=true" - # - "-r=xplat --env=production --variant=false" - # - "-r=xplat --env=production --variant=true" - - # TODO: Test more persistent configurations? - download_base_build_for_sizebot: filters: branches: @@ -469,27 +294,6 @@ workflows: requires: - download_base_build_for_sizebot - yarn_build - - yarn_lint_build: - requires: - - yarn_build - - yarn_check_release_dependencies: - requires: - - yarn_build - - check_error_codes: - requires: - - yarn_build - - RELEASE_CHANNEL_stable_yarn_test_dom_fixtures: - requires: - - yarn_build - - build_devtools_and_process_artifacts: - requires: - - yarn_build - - run_devtools_e2e_tests: - requires: - - build_devtools_and_process_artifacts - - run_fixtures_flight_tests: - requires: - - yarn_build devtools_regression_tests: unless: << pipeline.parameters.prerelease_commit_sha >> diff --git a/.eslintrc.js b/.eslintrc.js index 1d45d68055..f39437a7d1 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -490,6 +490,7 @@ module.exports = { 'packages/react-devtools-extensions/**/*.js', 'packages/react-devtools-shared/src/hook.js', 'packages/react-devtools-shared/src/backend/console.js', + 'packages/react-devtools-shared/src/backend/DevToolsComponentStackFrame.js', ], globals: { __IS_CHROME__: 'readonly', diff --git a/.github/workflows/compiler_playground.yml b/.github/workflows/compiler_playground.yml index 7f2a75d324..eb48a9b210 100644 --- a/.github/workflows/compiler_playground.yml +++ b/.github/workflows/compiler_playground.yml @@ -5,9 +5,12 @@ on: branches: [main] pull_request: paths: - - "compiler/**" + - compiler/** - .github/workflows/compiler-playground.yml +env: + TZ: /usr/share/zoneinfo/America/Los_Angeles + defaults: run: working-directory: compiler @@ -20,8 +23,8 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: - node-version: 18.x - cache: "yarn" + node-version: 18.20.1 + cache: yarn cache-dependency-path: compiler/yarn.lock - name: Restore cached node_modules uses: actions/cache@v4 diff --git a/.github/workflows/compiler_rust.yml b/.github/workflows/compiler_rust.yml index cf911b9d47..3cda1325ca 100644 --- a/.github/workflows/compiler_rust.yml +++ b/.github/workflows/compiler_rust.yml @@ -18,6 +18,7 @@ on: env: CARGO_TERM_COLOR: always RUSTFLAGS: -Dwarnings + TZ: /usr/share/zoneinfo/America/Los_Angeles defaults: run: diff --git a/.github/workflows/compiler_typescript.yml b/.github/workflows/compiler_typescript.yml index 233e963bfa..8576f66a10 100644 --- a/.github/workflows/compiler_typescript.yml +++ b/.github/workflows/compiler_typescript.yml @@ -5,9 +5,12 @@ on: branches: [main] pull_request: paths: - - "compiler/**" + - compiler/** - .github/workflows/compiler-typescript.yml +env: + TZ: /usr/share/zoneinfo/America/Los_Angeles + defaults: run: working-directory: compiler @@ -31,8 +34,8 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: - node-version: 18.x - cache: "yarn" + node-version: 18.20.1 + cache: yarn cache-dependency-path: compiler/yarn.lock - name: Restore cached node_modules uses: actions/cache@v4 @@ -50,8 +53,8 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: - node-version: 18.x - cache: "yarn" + node-version: 18.20.1 + cache: yarn cache-dependency-path: compiler/yarn.lock - name: Restore cached node_modules uses: actions/cache@v4 @@ -74,8 +77,8 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: - node-version: 18.x - cache: "yarn" + node-version: 18.20.1 + cache: yarn cache-dependency-path: compiler/yarn.lock - name: Restore cached node_modules uses: actions/cache@v4 diff --git a/.github/workflows/devtools_check_repro.yml b/.github/workflows/devtools_check_repro.yml index adaef6ac32..735371e3a8 100644 --- a/.github/workflows/devtools_check_repro.yml +++ b/.github/workflows/devtools_check_repro.yml @@ -5,6 +5,9 @@ on: issue_comment: types: [created, edited] +env: + TZ: /usr/share/zoneinfo/America/Los_Angeles + jobs: check-repro: runs-on: ubuntu-latest diff --git a/.github/workflows/runtime_build_and_test.yml b/.github/workflows/runtime_build_and_test.yml new file mode 100644 index 0000000000..92b8e2c7b8 --- /dev/null +++ b/.github/workflows/runtime_build_and_test.yml @@ -0,0 +1,517 @@ +name: (Runtime) Build and Test + +on: + push: + branches: [main] + pull_request: + paths-ignore: + - compiler/** + +env: + TZ: /usr/share/zoneinfo/America/Los_Angeles + +jobs: + # ----- FLOW ----- + discover_flow_inline_configs: + name: Discover flow inline configs + runs-on: ubuntu-latest + outputs: + matrix: ${{ steps.set-matrix.outputs.result }} + steps: + - uses: actions/checkout@v4 + - uses: actions/github-script@v7 + id: set-matrix + with: + script: | + const inlinedHostConfigs = require('./scripts/shared/inlinedHostConfigs.js'); + return inlinedHostConfigs.map(config => config.shortName); + + flow: + name: Flow check ${{ matrix.flow_inline_config_shortname }} + needs: discover_flow_inline_configs + runs-on: ubuntu-latest + continue-on-error: true + strategy: + matrix: + flow_inline_config_shortname: ${{ fromJSON(needs.discover_flow_inline_configs.outputs.matrix) }} + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 18.20.1 + cache: yarn + cache-dependency-path: yarn.lock + - name: Restore cached node_modules + uses: actions/cache@v4 + id: node_modules + with: + path: "**/node_modules" + key: ${{ runner.arch }}-${{ runner.os }}-modules-${{ hashFiles('yarn.lock') }} + - run: yarn install --frozen-lockfile + - run: node ./scripts/tasks/flow-ci ${{ matrix.flow_inline_config_shortname }} + + # ----- FIZZ ----- + check_generated_fizz_runtime: + name: Confirm generated inline Fizz runtime is up to date + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 18.20.1 + cache: yarn + cache-dependency-path: yarn.lock + - name: Restore cached node_modules + uses: actions/cache@v4 + id: node_modules + with: + path: "**/node_modules" + key: ${{ runner.arch }}-${{ runner.os }}-modules-${{ hashFiles('yarn.lock') }} + - run: yarn install --frozen-lockfile + - run: | + yarn generate-inline-fizz-runtime + git diff --quiet || (echo "There was a change to the Fizz runtime. Run `yarn generate-inline-fizz-runtime` and check in the result." && false) + + # ----- FEATURE FLAGS ----- + flags: + name: Check flags + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 18.20.1 + cache: yarn + cache-dependency-path: yarn.lock + - name: Restore cached node_modules + uses: actions/cache@v4 + id: node_modules + with: + path: "**/node_modules" + key: ${{ runner.arch }}-${{ runner.os }}-modules-${{ hashFiles('yarn.lock') }} + - run: yarn install --frozen-lockfile + - run: yarn flags + + # ----- TESTS ----- + test: + name: yarn test ${{ matrix.params }} (Shard ${{ matrix.shard }}) + runs-on: ubuntu-latest + strategy: + matrix: + params: + - "-r=stable --env=development" + - "-r=stable --env=production" + - "-r=experimental --env=development" + - "-r=experimental --env=production" + - "-r=www-classic --env=development --variant=false" + - "-r=www-classic --env=production --variant=false" + - "-r=www-classic --env=development --variant=true" + - "-r=www-classic --env=production --variant=true" + - "-r=www-modern --env=development --variant=false" + - "-r=www-modern --env=production --variant=false" + - "-r=www-modern --env=development --variant=true" + - "-r=www-modern --env=production --variant=true" + - "-r=xplat --env=development --variant=false" + - "-r=xplat --env=development --variant=true" + - "-r=xplat --env=production --variant=false" + - "-r=xplat --env=production --variant=true" + # TODO: Test more persistent configurations? + - "-r=stable --env=development --persistent" + - "-r=experimental --env=development --persistent" + shard: + - 1/5 + - 2/5 + - 3/5 + - 4/5 + - 5/5 + continue-on-error: true + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 18.20.1 + cache: yarn + cache-dependency-path: yarn.lock + - name: Restore cached node_modules + uses: actions/cache@v4 + id: node_modules + with: + path: "**/node_modules" + key: ${{ runner.arch }}-${{ runner.os }}-modules-${{ hashFiles('yarn.lock') }} + - run: yarn install --frozen-lockfile + - run: yarn test ${{ matrix.params }} --ci=github --shard=${{ matrix.shard }} + + # ----- BUILD ----- + build_and_lint: + name: yarn build and lint + runs-on: ubuntu-latest + strategy: + matrix: + # yml is dumb. update the --total arg to yarn build if you change the number of workers + worker_id: [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19] + release_channel: [stable, experimental] + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 18.20.1 + cache: yarn + cache-dependency-path: yarn.lock + - uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: 11.0.22 + - name: Restore cached node_modules + uses: actions/cache@v4 + id: node_modules + with: + path: "**/node_modules" + key: ${{ runner.arch }}-${{ runner.os }}-modules-${{ hashFiles('yarn.lock') }} + - run: yarn install --frozen-lockfile + - run: yarn build --index=${{ matrix.worker_id }} --total=20 --r=${{ matrix.release_channel }} --ci=github + env: + CI: github + RELEASE_CHANNEL: ${{ matrix.release_channel }} + NODE_INDEX: ${{ matrix.worker_id }} + - name: Lint build + run: yarn lint-build + - name: Display structure of build + run: ls -R build + - name: Archive build + uses: actions/upload-artifact@v4 + with: + name: build_${{ matrix.worker_id }}_${{ matrix.release_channel }} + path: | + build + + test_build: + name: yarn test-build + needs: build_and_lint + strategy: + matrix: + test_params: [ + # Intentionally passing these as strings instead of creating a + # separate parameter per CLI argument, since it's easier to + # control/see which combinations we want to run. + -r=stable --env=development, + -r=stable --env=production, + -r=experimental --env=development, + -r=experimental --env=production, + + # Dev Tools + --project=devtools -r=experimental, + + # TODO: Update test config to support www build tests + # - "-r=www-classic --env=development --variant=false" + # - "-r=www-classic --env=production --variant=false" + # - "-r=www-classic --env=development --variant=true" + # - "-r=www-classic --env=production --variant=true" + # - "-r=www-modern --env=development --variant=false" + # - "-r=www-modern --env=production --variant=false" + # - "-r=www-modern --env=development --variant=true" + # - "-r=www-modern --env=production --variant=true" + + # TODO: Update test config to support xplat build tests + # - "-r=xplat --env=development --variant=false" + # - "-r=xplat --env=development --variant=true" + # - "-r=xplat --env=production --variant=false" + # - "-r=xplat --env=production --variant=true" + + # TODO: Test more persistent configurations? + ] + shard: + - 1/3 + - 2/3 + - 3/3 + continue-on-error: true + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 18.20.1 + cache: yarn + cache-dependency-path: yarn.lock + - name: Restore cached node_modules + uses: actions/cache@v4 + id: node_modules + with: + path: "**/node_modules" + key: ${{ runner.arch }}-${{ runner.os }}-modules-${{ hashFiles('yarn.lock') }} + - run: yarn install --frozen-lockfile + - name: Restore archived build + uses: actions/download-artifact@v4 + with: + path: build + merge-multiple: true + - name: Display structure of build + run: ls -R build + - run: yarn test --build ${{ matrix.test_params }} --shard=${{ matrix.shard }} --ci=github + + process_artifacts_combined: + name: Process artifacts combined + needs: build_and_lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 18.20.1 + cache: yarn + cache-dependency-path: yarn.lock + - name: Restore cached node_modules + uses: actions/cache@v4 + id: node_modules + with: + path: "**/node_modules" + key: ${{ runner.arch }}-${{ runner.os }}-modules-${{ hashFiles('yarn.lock') }} + - run: yarn install --frozen-lockfile + - name: Restore archived build + uses: actions/download-artifact@v4 + with: + path: build + merge-multiple: true + - name: Display structure of build + run: ls -R build + - run: echo ${{ github.sha }} >> build/COMMIT_SHA + - name: Scrape warning messages + run: | + mkdir -p ./build/__test_utils__ + node ./scripts/print-warnings/print-warnings.js > build/__test_utils__/ReactAllWarnings.js + # Compress build directory into a single tarball for easy download + - run: tar -zcvf ./build.tgz ./build + # TODO: Migrate scripts to use `build` directory instead of `build2` + - run: cp ./build.tgz ./build2.tgz + - name: Archive build artifacts + uses: actions/upload-artifact@v4 + with: + name: combined_artifacts_${{ github.sha }} + path: | + ./build.tgz + ./build2.tgz + + check_error_codes: + name: Search build artifacts for unminified errors + needs: build_and_lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 18.20.1 + cache: yarn + cache-dependency-path: yarn.lock + - name: Restore cached node_modules + uses: actions/cache@v4 + id: node_modules + with: + path: "**/node_modules" + key: ${{ runner.arch }}-${{ runner.os }}-modules-${{ hashFiles('yarn.lock') }} + - run: yarn install --frozen-lockfile + - name: Restore archived build + uses: actions/download-artifact@v4 + with: + path: build + merge-multiple: true + - name: Display structure of build + run: ls -R build + - name: Search build artifacts for unminified errors + run: | + yarn extract-errors + git diff --quiet || (echo "Found unminified errors. Either update the error codes map or disable error minification for the affected build, if appropriate." && false) + + check_release_dependencies: + name: Check release dependencies + needs: build_and_lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 18.20.1 + cache: yarn + cache-dependency-path: yarn.lock + - name: Restore cached node_modules + uses: actions/cache@v4 + id: node_modules + with: + path: "**/node_modules" + key: ${{ runner.arch }}-${{ runner.os }}-modules-${{ hashFiles('yarn.lock') }} + - run: yarn install --frozen-lockfile + - name: Restore archived build + uses: actions/download-artifact@v4 + with: + path: build + merge-multiple: true + - name: Display structure of build + run: ls -R build + - run: yarn check-release-dependencies + + RELEASE_CHANNEL_stable_yarn_test_dom_fixtures: + name: Check fixtures DOM (stable) + needs: build_and_lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 18.20.1 + cache: yarn + cache-dependency-path: yarn.lock + - name: Restore cached node_modules + uses: actions/cache@v4 + id: node_modules + with: + path: "**/node_modules" + key: v2-yarn_cache_fixtures_dom-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }} + - run: yarn install --frozen-lockfile + - run: yarn install --frozen-lockfile --cache-folder ~/.cache/yarn + working-directory: fixtures/dom + - name: Restore archived build + uses: actions/download-artifact@v4 + with: + path: build + merge-multiple: true + - name: Display structure of build + run: ls -R build + - name: Run DOM fixture tests + run: | + yarn predev + yarn test + working-directory: fixtures/dom + env: + RELEASE_CHANNEL: stable + + # ----- FLIGHT ----- + run_fixtures_flight_tests: + name: Run fixtures Flight tests + needs: build_and_lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 18.20.1 + cache: yarn + cache-dependency-path: yarn.lock + # Fixture copies some built packages from the workroot after install. + # That means dependencies of the built packages are not installed. + # We need to install dependencies of the workroot to fulfill all dependency constraints + - name: Restore cached node_modules + uses: actions/cache@v4 + id: node_modules + with: + path: "**/node_modules" + key: v2-yarn_cache_fixtures_flight-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }} + - run: yarn install --frozen-lockfile + - name: Restore archived build + uses: actions/download-artifact@v4 + with: + path: build + merge-multiple: true + - name: Display structure of build + run: ls -R build + - name: Install fixture dependencies + working-directory: fixtures/flight + run: | + yarn install --frozen-lockfile --cache-folder ~/.cache/yarn + if [ $? -ne 0 ]; then + yarn install --frozen-lockfile --cache-folder ~/.cache/yarn + fi + - name: Playwright install deps + working-directory: fixtures/flight + run: | + npx playwright install + sudo npx playwright install-deps + - name: Run tests + working-directory: fixtures/flight + run: yarn test + env: + # Otherwise the webserver is a blackbox + DEBUG: pw:webserver + - name: Archive Flight fixture artifacts + uses: actions/upload-artifact@v4 + with: + name: flight-playwright-report + path: fixtures/flight/playwright-report + - name: Archive Flight fixture artifacts + uses: actions/upload-artifact@v4 + with: + name: flight-test-results + path: fixtures/flight/test-results + + # ----- DEVTOOLS ----- + build_devtools_and_process_artifacts: + name: Build DevTools and process artifacts + needs: build_and_lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 18.20.1 + cache: yarn + cache-dependency-path: yarn.lock + - name: Restore cached node_modules + uses: actions/cache@v4 + id: node_modules + with: + path: "**/node_modules" + key: ${{ runner.arch }}-${{ runner.os }}-modules-${{ hashFiles('yarn.lock') }} + - run: yarn install --frozen-lockfile + - name: Restore archived build + uses: actions/download-artifact@v4 + with: + path: build + merge-multiple: true + - run: ./scripts/circleci/pack_and_store_devtools_artifacts.sh + env: + RELEASE_CHANNEL: experimental + - name: Display structure of build + run: ls -R build + - name: Archive devtools build + uses: actions/upload-artifact@v4 + with: + name: react-devtools + path: build/devtools.tgz + # Simplifies getting the extension for local testing + - name: Archive chrome extension + uses: actions/upload-artifact@v4 + with: + name: react-devtools-chrome-extension + path: build/devtools/chrome-extension.zip + - name: Archive firefox extension + uses: actions/upload-artifact@v4 + with: + name: react-devtools-firefox-extension + path: build/devtools/firefox-extension.zip + + run_devtools_e2e_tests: + name: Run DevTools e2e tests + needs: build_devtools_and_process_artifacts + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 18.20.1 + cache: yarn + cache-dependency-path: yarn.lock + - name: Restore cached node_modules + uses: actions/cache@v4 + id: node_modules + with: + path: "**/node_modules" + key: ${{ runner.arch }}-${{ runner.os }}-modules-${{ hashFiles('yarn.lock') }} + - run: yarn install --frozen-lockfile + - name: Restore archived build + uses: actions/download-artifact@v4 + with: + path: build + merge-multiple: true + - run: | + npx playwright install + sudo npx playwright install-deps + - run: ./scripts/circleci/run_devtools_e2e_tests.js + env: + RELEASE_CHANNEL: experimental diff --git a/.github/workflows/runtime_commit_artifacts.yml b/.github/workflows/runtime_commit_artifacts.yml index 2c32281634..c665f9b1bd 100644 --- a/.github/workflows/runtime_commit_artifacts.yml +++ b/.github/workflows/runtime_commit_artifacts.yml @@ -4,6 +4,9 @@ on: push: branches: [main, meta-www, meta-fbsource] +env: + TZ: /usr/share/zoneinfo/America/Los_Angeles + jobs: download_artifacts: runs-on: ubuntu-latest diff --git a/.github/workflows/runtime_fizz.yml b/.github/workflows/runtime_fizz.yml deleted file mode 100644 index 43097728e3..0000000000 --- a/.github/workflows/runtime_fizz.yml +++ /dev/null @@ -1,30 +0,0 @@ -name: (Runtime) Fizz - -on: - push: - branches: [main] - pull_request: - paths-ignore: - - 'compiler/**' - -jobs: - check_generated_fizz_runtime: - name: Confirm generated inline Fizz runtime is up to date - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 - with: - node-version: 18.x - cache: "yarn" - cache-dependency-path: yarn.lock - - name: Restore cached node_modules - uses: actions/cache@v4 - id: node_modules - with: - path: "**/node_modules" - key: ${{ runner.arch }}-${{ runner.os }}-modules-${{ hashFiles('yarn.lock') }} - - run: yarn install --frozen-lockfile - - run: | - yarn generate-inline-fizz-runtime - git diff --quiet || (echo "There was a change to the Fizz runtime. Run `yarn generate-inline-fizz-runtime` and check in the result." && false) diff --git a/.github/workflows/runtime_flags.yml b/.github/workflows/runtime_flags.yml deleted file mode 100644 index baf9a48242..0000000000 --- a/.github/workflows/runtime_flags.yml +++ /dev/null @@ -1,28 +0,0 @@ -name: (Runtime) Flags - -on: - push: - branches: [main] - pull_request: - paths-ignore: - - 'compiler/**' - -jobs: - flags: - name: Check flags - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 - with: - node-version: 18.x - cache: "yarn" - cache-dependency-path: yarn.lock - - name: Restore cached node_modules - uses: actions/cache@v4 - id: node_modules - with: - path: "**/node_modules" - key: ${{ runner.arch }}-${{ runner.os }}-modules-${{ hashFiles('yarn.lock') }} - - run: yarn install --frozen-lockfile - - run: yarn flags diff --git a/.github/workflows/runtime_flow.yml b/.github/workflows/runtime_flow.yml deleted file mode 100644 index 6b8eb4b774..0000000000 --- a/.github/workflows/runtime_flow.yml +++ /dev/null @@ -1,47 +0,0 @@ -name: (Runtime) Flow - -on: - push: - branches: [main] - pull_request: - paths-ignore: - - 'compiler/**' - -jobs: - discover_flow_inline_configs: - name: Discover flow inline configs - runs-on: ubuntu-latest - outputs: - matrix: ${{ steps.set-matrix.outputs.result }} - steps: - - uses: actions/checkout@v4 - - uses: actions/github-script@v7 - id: set-matrix - with: - script: | - const inlinedHostConfigs = require('./scripts/shared/inlinedHostConfigs.js'); - return inlinedHostConfigs.map(config => config.shortName); - - flow: - name: Flow check ${{ matrix.flow_inline_config_shortname }} - needs: discover_flow_inline_configs - runs-on: ubuntu-latest - continue-on-error: true - strategy: - matrix: - flow_inline_config_shortname: ${{ fromJSON(needs.discover_flow_inline_configs.outputs.matrix) }} - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 - with: - node-version: 18.x - cache: "yarn" - cache-dependency-path: yarn.lock - - name: Restore cached node_modules - uses: actions/cache@v4 - id: node_modules - with: - path: "**/node_modules" - key: ${{ runner.arch }}-${{ runner.os }}-modules-${{ hashFiles('yarn.lock') }} - - run: yarn install --frozen-lockfile - - run: node ./scripts/tasks/flow-ci ${{ matrix.flow_inline_config_shortname }} diff --git a/.github/workflows/runtime_fuzz_tests.yml b/.github/workflows/runtime_fuzz_tests.yml index 5b31d115dc..dac4dd095a 100644 --- a/.github/workflows/runtime_fuzz_tests.yml +++ b/.github/workflows/runtime_fuzz_tests.yml @@ -9,6 +9,10 @@ on: inputs: prerelease_commit_sha: required: false + +env: + TZ: /usr/share/zoneinfo/America/Los_Angeles + jobs: test_fuzz: if: inputs.prerelease_commit_sha == '' diff --git a/.github/workflows/runtime_test.yml b/.github/workflows/runtime_test.yml deleted file mode 100644 index 118a520ad5..0000000000 --- a/.github/workflows/runtime_test.yml +++ /dev/null @@ -1,85 +0,0 @@ -name: (Runtime) Test - -on: - push: - branches: [main] - pull_request: - paths-ignore: - - 'compiler/**' - -env: - # Number of workers (one per shard) to spawn - SHARD_COUNT: 5 - -jobs: - # Define the various test parameters and parallelism for this workflow - build_test_params: - name: Build test params - runs-on: ubuntu-latest - outputs: - params: ${{ steps.define-params.outputs.result }} - shard_id: ${{ steps.define-shards.outputs.result }} - steps: - - uses: actions/github-script@v7 - id: define-shards - with: - script: | - function range(from, to) { - const arr = []; - for (let n = from; n <= to; n++) { - arr.push(n); - } - return arr; - } - return range(1, process.env.SHARD_COUNT); - - uses: actions/github-script@v7 - id: define-params - with: - script: | - return [ - "-r=stable --env=development", - "-r=stable --env=production", - "-r=experimental --env=development", - "-r=experimental --env=production", - "-r=www-classic --env=development --variant=false", - "-r=www-classic --env=production --variant=false", - "-r=www-classic --env=development --variant=true", - "-r=www-classic --env=production --variant=true", - "-r=www-modern --env=development --variant=false", - "-r=www-modern --env=production --variant=false", - "-r=www-modern --env=development --variant=true", - "-r=www-modern --env=production --variant=true", - "-r=xplat --env=development --variant=false", - "-r=xplat --env=development --variant=true", - "-r=xplat --env=production --variant=false", - "-r=xplat --env=production --variant=true", - // TODO: Test more persistent configurations? - "-r=stable --env=development --persistent", - "-r=experimental --env=development --persistent" - ]; - - # Spawn a job for each shard for a given set of test params - test: - name: yarn test ${{ matrix.params }} (Shard ${{ matrix.shard_id }}) - runs-on: ubuntu-latest - needs: build_test_params - strategy: - matrix: - params: ${{ fromJSON(needs.build_test_params.outputs.params) }} - shard_id: ${{ fromJSON(needs.build_test_params.outputs.shard_id) }} - continue-on-error: true - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 - with: - node-version: 18.x - cache: "yarn" - cache-dependency-path: yarn.lock - - name: Restore cached node_modules - uses: actions/cache@v4 - id: node_modules - with: - path: "**/node_modules" - key: ${{ runner.arch }}-${{ runner.os }}-modules-${{ hashFiles('yarn.lock') }} - - run: yarn install --frozen-lockfile - - run: yarn test ${{ matrix.params }} --ci=github --shard=${{ matrix.shard_id }}/${{ env.SHARD_COUNT }} diff --git a/.github/workflows/shared_lint.yml b/.github/workflows/shared_lint.yml index ca851ff232..e06637b168 100644 --- a/.github/workflows/shared_lint.yml +++ b/.github/workflows/shared_lint.yml @@ -5,6 +5,9 @@ on: branches: [main] pull_request: +env: + TZ: /usr/share/zoneinfo/America/Los_Angeles + jobs: prettier: name: Run prettier @@ -13,8 +16,8 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: - node-version: 18.x - cache: "yarn" + node-version: 18.20.1 + cache: yarn cache-dependency-path: yarn.lock - name: Restore cached node_modules uses: actions/cache@v4 @@ -31,8 +34,8 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: - node-version: 18.x - cache: "yarn" + node-version: 18.20.1 + cache: yarn cache-dependency-path: yarn.lock - name: Restore cached node_modules uses: actions/cache@v4 @@ -49,8 +52,8 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: - node-version: 18.x - cache: "yarn" + node-version: 18.20.1 + cache: yarn cache-dependency-path: yarn.lock - name: Restore cached node_modules uses: actions/cache@v4 @@ -67,8 +70,8 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: - node-version: 18.x - cache: "yarn" + node-version: 18.20.1 + cache: yarn cache-dependency-path: yarn.lock - name: Restore cached node_modules uses: actions/cache@v4 diff --git a/.github/workflows/shared_stale.yml b/.github/workflows/shared_stale.yml index df9854c270..9135b9afca 100644 --- a/.github/workflows/shared_stale.yml +++ b/.github/workflows/shared_stale.yml @@ -5,6 +5,9 @@ on: # Run hourly - cron: '0 * * * *' +env: + TZ: /usr/share/zoneinfo/America/Los_Angeles + jobs: stale: runs-on: ubuntu-latest diff --git a/.nvmrc b/.nvmrc index 91f7588a1a..ef33d65101 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -v18.20.0 +v18.20.1 diff --git a/README.md b/README.md index 456df1f5d3..aa56f005b3 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# [React](https://react.dev/) · [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/facebook/react/blob/main/LICENSE) [![npm version](https://img.shields.io/npm/v/react.svg?style=flat)](https://www.npmjs.com/package/react) [![CircleCI Status](https://circleci.com/gh/facebook/react.svg?style=shield)](https://circleci.com/gh/facebook/react) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://legacy.reactjs.org/docs/how-to-contribute.html#your-first-pull-request) +# [React](https://react.dev/) · [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/facebook/react/blob/main/LICENSE) [![npm version](https://img.shields.io/npm/v/react.svg?style=flat)](https://www.npmjs.com/package/react) [![CircleCI Status](https://circleci.com/gh/facebook/react.svg?style=shield)](https://circleci.com/gh/facebook/react) [![(Runtime) Build and Test](https://github.com/facebook/react/actions/workflows/runtime_build_and_test.yml/badge.svg)](https://github.com/facebook/react/actions/workflows/runtime_build_and_test.yml) [![(Compiler) TypeScript](https://github.com/facebook/react/actions/workflows/compiler_typescript.yml/badge.svg?branch=main)](https://github.com/facebook/react/actions/workflows/compiler_typescript.yml) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://legacy.reactjs.org/docs/how-to-contribute.html#your-first-pull-request) React is a JavaScript library for building user interfaces. @@ -18,7 +18,7 @@ React has been designed for gradual adoption from the start, and **you can use a ## Documentation -You can find the React documentation [on the website](https://react.dev/). +You can find the React documentation [on the website](https://react.dev/). Check out the [Getting Started](https://react.dev/learn) page for a quick overview. @@ -55,7 +55,7 @@ root.render(); This example will render "Hello Taylor" into a container on the page. -You'll notice that we used an HTML-like syntax; [we call it JSX](https://react.dev/learn#writing-markup-with-jsx). JSX is not required to use React, but it makes code more readable, and writing it feels like writing HTML. +You'll notice that we used an HTML-like syntax; [we call it JSX](https://react.dev/learn#writing-markup-with-jsx). JSX is not required to use React, but it makes code more readable, and writing it feels like writing HTML. ## Contributing diff --git a/compiler/apps/playground/components/Editor/EditorImpl.tsx b/compiler/apps/playground/components/Editor/EditorImpl.tsx index 312492b48c..a96672194b 100644 --- a/compiler/apps/playground/components/Editor/EditorImpl.tsx +++ b/compiler/apps/playground/components/Editor/EditorImpl.tsx @@ -42,6 +42,8 @@ import { default as Output, PrintedCompilerPipelineValue, } from "./Output"; +import { printFunctionWithOutlined } from "babel-plugin-react-compiler/src/HIR/PrintHIR"; +import { printReactiveFunctionWithOutlined } from "babel-plugin-react-compiler/src/ReactiveScopes/PrintReactiveFunction"; function parseInput(input: string, language: "flow" | "typescript") { // Extract the first line to quickly check for custom test directives @@ -242,7 +244,7 @@ function compile(source: string): [CompilerOutput, "flow" | "typescript"] { kind: "hir", fnName, name: result.name, - value: printHIR(result.value.body), + value: printFunctionWithOutlined(result.value), }); break; } @@ -251,7 +253,7 @@ function compile(source: string): [CompilerOutput, "flow" | "typescript"] { kind: "reactive", fnName, name: result.name, - value: printReactiveFunction(result.value), + value: printReactiveFunctionWithOutlined(result.value), }); break; } diff --git a/compiler/apps/playground/package.json b/compiler/apps/playground/package.json index 55f146fe25..fdb535d2e4 100644 --- a/compiler/apps/playground/package.json +++ b/compiler/apps/playground/package.json @@ -12,6 +12,7 @@ }, "dependencies": { "@babel/core": "^7.19.1", + "@babel/generator": "^7.19.1", "@babel/parser": "^7.19.1", "@babel/plugin-syntax-typescript": "^7.18.6", "@babel/plugin-transform-block-scoping": "^7.18.9", @@ -55,9 +56,6 @@ }, "resolutions": { "./**/@babel/parser": "7.7.4", - "./**/@babel/types": "7.7.4", - "@babel/core": "7.2.0", - "@babel/traverse": "7.1.6", - "@babel/generator": "7.2.0" + "./**/@babel/types": "7.7.4" } } diff --git a/compiler/packages/babel-plugin-react-compiler/scripts/jest/makeTransform.ts b/compiler/packages/babel-plugin-react-compiler/scripts/jest/makeTransform.ts index 2f525af9c8..8062d4af77 100644 --- a/compiler/packages/babel-plugin-react-compiler/scripts/jest/makeTransform.ts +++ b/compiler/packages/babel-plugin-react-compiler/scripts/jest/makeTransform.ts @@ -28,6 +28,7 @@ import { basename } from "path"; const e2eTransformerCacheKey = 1; const forgetOptions: EnvironmentConfig = validateEnvironmentConfig({ enableAssumeHooksFollowRulesOfReact: true, + enableFunctionOutlining: false, }); const debugMode = process.env["DEBUG_FORGET_COMPILER"] != null; diff --git a/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Pipeline.ts b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Pipeline.ts index 5fdbd77675..a3d6ef1933 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Pipeline.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Pipeline.ts @@ -97,7 +97,7 @@ import { validateUseMemo, } from "../Validation"; import { validateLocalsNotReassignedAfterRender } from "../Validation/ValidateLocalsNotReassignedAfterRender"; -import { memoizeExistingUseMemos } from "../ReactiveScopes/MemoizeExistingUseMemos"; +import { outlineFunctions } from "../Optimization/OutlineFunctions"; export type CompilerPipelineValue = | { kind: "ast"; name: string; value: CodegenFunction } @@ -118,6 +118,7 @@ export function* run( ): Generator { const contextIdentifiers = findContextIdentifiers(func); const env = new Environment( + func.scope, fnType, config, contextIdentifiers, @@ -154,7 +155,7 @@ function* runWithEnvironment( validateContextVariableLValues(hir); validateUseMemo(hir); - if (env.config.enablePreserveExistingManualUseMemo !== "hook") { + if (!env.preserveManualMemo()) { dropManualMemoization(hir); yield log({ kind: "hir", name: "DropManualMemoization", value: hir }); } @@ -238,6 +239,11 @@ function* runWithEnvironment( inferReactiveScopeVariables(hir); yield log({ kind: "hir", name: "InferReactiveScopeVariables", value: hir }); + if (env.config.enableFunctionOutlining) { + outlineFunctions(hir); + yield log({ kind: "hir", name: "OutlineFunctions", value: hir }); + } + alignMethodCallScopes(hir); yield log({ kind: "hir", @@ -267,19 +273,6 @@ function* runWithEnvironment( value: hir, }); - if ( - env.config.enablePreserveExistingManualUseMemo === "scope" || - env.config.enableChangeDetection != null || - env.config.disableMemoizationForDebugging - ) { - memoizeExistingUseMemos(hir); - yield log({ - kind: "hir", - name: "MemoizeExistingUseMemos", - value: hir, - }); - } - alignReactiveScopesToBlockScopesHIR(hir); yield log({ kind: "hir", @@ -390,12 +383,14 @@ function* runWithEnvironment( value: reactiveFunction, }); - pruneNonReactiveDependencies(reactiveFunction); - yield log({ - kind: "reactive", - name: "PruneNonReactiveDependencies", - value: reactiveFunction, - }); + if (env.config.enableChangeDetection == null) { + pruneNonReactiveDependencies(reactiveFunction); + yield log({ + kind: "reactive", + name: "PruneNonReactiveDependencies", + value: reactiveFunction, + }); + } pruneUnusedScopes(reactiveFunction); yield log({ @@ -489,6 +484,9 @@ function* runWithEnvironment( const ast = codegenFunction(reactiveFunction, uniqueIdentifiers).unwrap(); yield log({ kind: "ast", name: "Codegen", value: ast }); + for (const outlined of ast.outlined) { + yield log({ kind: "ast", name: "Codegen (outlined)", value: outlined.fn }); + } /** * This flag should be only set for unit / fixture tests to check diff --git a/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Program.ts b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Program.ts index 6e6e52720d..3767cab21f 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Program.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Program.ts @@ -87,6 +87,12 @@ export type BabelFn = | NodePath; export type CompileResult = { + /** + * Distinguishes existing functions that were compiled ('original') from + * functions which were outlined. Only original functions need to be gated + * if gating mode is enabled. + */ + kind: "original" | "outlined"; originalFn: BabelFn; compiledFn: CodegenFunction; }; @@ -265,6 +271,11 @@ export function compileProgram( ); const lintError = suppressionsToCompilerError(suppressions); let hasCriticalError = lintError != null; + const queue: Array<{ + kind: "original" | "outlined"; + fn: BabelFn; + fnType: ReactFunctionType; + }> = []; const compiledFns: Array = []; const traverseFunction = (fn: BabelFn, pass: CompilerPass): void => { @@ -281,6 +292,47 @@ export function compileProgram( ALREADY_COMPILED.add(fn.node); fn.skip(); + queue.push({ kind: "original", fn, fnType }); + }; + + // Main traversal to compile with Forget + program.traverse( + { + ClassDeclaration(node: NodePath) { + /* + * Don't visit functions defined inside classes, because they + * can reference `this` which is unsafe for compilation + */ + node.skip(); + return; + }, + + ClassExpression(node: NodePath) { + /* + * Don't visit functions defined inside classes, because they + * can reference `this` which is unsafe for compilation + */ + node.skip(); + return; + }, + + FunctionDeclaration: traverseFunction, + + FunctionExpression: traverseFunction, + + ArrowFunctionExpression: traverseFunction, + }, + { + ...pass, + opts: { ...pass.opts, ...pass.opts }, + filename: pass.filename ?? null, + } + ); + + const processFn = ( + fn: BabelFn, + fnType: ReactFunctionType + ): null | CodegenFunction => { if (lintError != null) { /** * Note that Babel does not attach comment nodes to nodes; they are dangling off of the @@ -335,52 +387,59 @@ export function compileProgram( } catch (err) { hasCriticalError ||= isCriticalError(err); handleError(err, pass, fn.node.loc ?? null); - return; + return null; } if (!pass.opts.noEmit && !hasCriticalError) { - compiledFns.push({ originalFn: fn, compiledFn }); + return compiledFn; } + return null; }; - // Main traversal to compile with Forget - program.traverse( - { - ClassDeclaration(node: NodePath) { - /* - * Don't visit functions defined inside classes, because they - * can reference `this` which is unsafe for compilation - */ - node.skip(); - return; - }, - - ClassExpression(node: NodePath) { - /* - * Don't visit functions defined inside classes, because they - * can reference `this` which is unsafe for compilation - */ - node.skip(); - return; - }, - - FunctionDeclaration: traverseFunction, - - FunctionExpression: traverseFunction, - - ArrowFunctionExpression: traverseFunction, - }, - { - ...pass, - opts: { ...pass.opts, ...pass.opts }, - filename: pass.filename ?? null, + while (queue.length !== 0) { + const current = queue.shift()!; + const compiled = processFn(current.fn, current.fnType); + if (compiled === null) { + continue; } - ); + for (const outlined of compiled.outlined) { + CompilerError.invariant(outlined.fn.outlined.length === 0, { + reason: "Unexpected nested outlined functions", + loc: outlined.fn.loc, + }); + const fn = current.fn.insertAfter( + createNewFunctionNode(current.fn, outlined.fn) + )[0]!; + fn.skip(); + ALREADY_COMPILED.add(fn.node); + if (outlined.type !== null) { + CompilerError.throwTodo({ + reason: `Implement support for outlining React functions (components/hooks)`, + loc: outlined.fn.loc, + }); + /* + * Above should be as simple as the following, but needs testing: + * queue.push({ + * kind: "outlined", + * fn, + * fnType: outlined.type, + * }); + */ + } + } + compiledFns.push({ + kind: current.kind, + compiledFn: compiled, + originalFn: current.fn, + }); + } if (pass.opts.gating != null) { const error = checkFunctionReferencedBeforeDeclarationAtTopLevel( program, - compiledFns.map(({ originalFn }) => originalFn) + compiledFns.map((result) => { + return result.originalFn; + }) ); if (error) { handleError(error, pass, null); @@ -455,10 +514,11 @@ export function compileProgram( * Only insert Forget-ified functions if we have not encountered a critical * error elsewhere in the file, regardless of bailout mode. */ - for (const { originalFn, compiledFn } of compiledFns) { + for (const result of compiledFns) { + const { kind, originalFn, compiledFn } = result; const transformedFn = createNewFunctionNode(originalFn, compiledFn); - if (gating != null) { + if (gating != null && kind === "original") { insertGatedFunctionDeclaration(originalFn, transformedFn, gating); } else { originalFn.replaceWith(transformedFn); diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/BuildHIR.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/BuildHIR.ts index e5a067018d..a75c7d9f4c 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/BuildHIR.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/BuildHIR.ts @@ -129,6 +129,7 @@ export function lower( reactive: false, loc: param.node.loc ?? GeneratedSource, }; + promoteTemporary(place.identifier); params.push(place); lowerAssignment( builder, @@ -1332,6 +1333,7 @@ function lowerStatement( return; } case "TypeAlias": + case "TSInterfaceDeclaration": case "TSTypeAliasDeclaration": { // We do not preserve type annotations/syntax through transformation return; @@ -1358,7 +1360,6 @@ function lowerStatement( case "TSEnumDeclaration": case "TSExportAssignment": case "TSImportEqualsDeclaration": - case "TSInterfaceDeclaration": case "TSModuleDeclaration": case "TSNamespaceExportDeclaration": case "WithStatement": { diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts index 4188e536a4..dd17ee8a8a 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts @@ -23,14 +23,17 @@ import { BuiltInType, Effect, FunctionType, + HIRFunction, IdentifierId, NonLocalBinding, PolyType, ScopeId, Type, + ValidatedIdentifier, ValueKind, makeBlockId, makeIdentifierId, + makeIdentifierName, makeScopeId, } from "./HIR"; import { @@ -41,6 +44,7 @@ import { ShapeRegistry, addHook, } from "./ObjectShape"; +import { Scope as BabelScope } from "@babel/traverse"; export const ExternalFunctionSchema = z.object({ // Source for the imported module that exports the `importSpecifierName` functions @@ -182,9 +186,7 @@ const EnvironmentConfigSchema = z.object({ * that the memoized values remain memoized, the compiler will simply not prune existing calls to * useMemo/useCallback. */ - enablePreserveExistingManualUseMemo: z - .nullable(z.enum(["hook", "scope"])) - .default(null), + enablePreserveExistingManualUseMemo: z.boolean().default(false), // 🌲 enableForest: z.boolean().default(false), @@ -285,6 +287,12 @@ const EnvironmentConfigSchema = z.object({ */ enableInstructionReordering: z.boolean().default(false), + /** + * Enables function outlinining, where anonymous functions that do not close over + * local variables can be extracted into top-level helper functions. + */ + enableFunctionOutlining: z.boolean().default(true), + /* * Enables instrumentation codegen. This emits a dev-mode only call to an * instrumentation function, for components and hooks that Forget compiles. @@ -479,31 +487,6 @@ export function parseConfigPragma(pragma: string): EnvironmentConfig { continue; } - if ( - key === "enablePreserveExistingManualUseMemo" && - (val === undefined || val === "true" || val === "scope") - ) { - maybeConfig[key] = "scope"; - continue; - } - - if (key === "enablePreserveExistingManualUseMemo" && val === "hook") { - maybeConfig[key] = "hook"; - continue; - } - - if ( - key === "enablePreserveExistingManualUseMemo" && - !(val === "false" || val === "off") - ) { - CompilerError.throwInvalidConfig({ - reason: `Invalid setting '${val}' for 'enablePreserveExistingManualUseMemo'. Valid settings are 'hook', 'scope', or 'off'.`, - description: null, - loc: null, - suggestions: null, - }); - } - if (typeof defaultConfig[key as keyof EnvironmentConfig] !== "boolean") { // skip parsing non-boolean properties continue; @@ -552,6 +535,11 @@ export class Environment { #nextIdentifer: number = 0; #nextBlock: number = 0; #nextScope: number = 0; + #scope: BabelScope; + #outlinedFunctions: Array<{ + fn: HIRFunction; + type: ReactFunctionType | null; + }> = []; logger: Logger | null; filename: string | null; code: string | null; @@ -563,6 +551,7 @@ export class Environment { #hoistedIdentifiers: Set; constructor( + scope: BabelScope, fnType: ReactFunctionType, config: EnvironmentConfig, contextIdentifiers: Set, @@ -571,6 +560,7 @@ export class Environment { code: string | null, useMemoCacheIdentifier: string ) { + this.#scope = scope; this.fnType = fnType; this.config = config; this.filename = filename; @@ -631,6 +621,24 @@ export class Environment { return this.#hoistedIdentifiers.has(node); } + generateGloballyUniqueIdentifierName( + name: string | null + ): ValidatedIdentifier { + const identifierNode = this.#scope.generateUidIdentifier(name ?? undefined); + return makeIdentifierName(identifierNode.name); + } + + outlineFunction(fn: HIRFunction, type: ReactFunctionType | null): void { + this.#outlinedFunctions.push({ fn, type }); + } + + getOutlinedFunctions(): Array<{ + fn: HIRFunction; + type: ReactFunctionType | null; + }> { + return this.#outlinedFunctions; + } + getGlobalDeclaration(binding: NonLocalBinding): Global | null { if (this.config.hookPattern != null) { const match = new RegExp(this.config.hookPattern).exec(binding.name); @@ -766,6 +774,14 @@ export class Environment { return DefaultMutatingHook; } } + + preserveManualMemo(): boolean { + return ( + this.config.enablePreserveExistingManualUseMemo || + this.config.disableMemoizationForDebugging || + this.config.enableChangeDetection != null + ); + } } // From https://github.com/facebook/react/blob/main/packages/eslint-plugin-react-hooks/src/RulesOfHooks.js#LL18C1-L23C2 diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts index 6cfa10113f..1f4a55d248 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts @@ -771,12 +771,7 @@ export type ManualMemoDependency = { kind: "NamedLocal"; value: Place; } - | { - kind: "InlinedGlobal"; - value: Place; - name: string; - } - | { kind: "Global"; binding: LoadGlobal }; + | { kind: "Global"; identifierName: string }; path: Array; }; @@ -1173,7 +1168,7 @@ export type NonLocalBinding = imported: string; } // let, const, function, etc declared in the module but outside the current component/hook - | { kind: "ModuleLocal"; name: string } + | { kind: "ModuleLocal"; name: string; immutable: boolean } // an unresolved binding | { kind: "Global"; name: string }; @@ -1434,8 +1429,6 @@ export type ReactiveScope = { merged: Set; loc: SourceLocation; - - source: boolean; }; export type ReactiveScopeDependencies = Set; diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/HIRBuilder.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/HIRBuilder.ts index 0342d57ea3..0bab88c974 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/HIRBuilder.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/HIRBuilder.ts @@ -271,9 +271,14 @@ export default class HIRBuilder { module: importDeclaration.node.source.value, }; } else { + const immutable = + (path.isVariableDeclaration() && path.node.kind === "const") || + path.isClassDeclaration() || + path.isClassExpression(); return { kind: "ModuleLocal", name: originalName, + immutable, }; } } diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/MergeOverlappingReactiveScopesHIR.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/MergeOverlappingReactiveScopesHIR.ts index 6ce0c101fb..7bae2e0894 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/MergeOverlappingReactiveScopesHIR.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/MergeOverlappingReactiveScopesHIR.ts @@ -6,7 +6,7 @@ import { makeInstructionId, } from "."; import { getPlaceScope } from "../ReactiveScopes/BuildReactiveBlocks"; -import { isMutable } from "../ReactiveScopes/InferReactiveScopeVariables"; +import { isMutableAtInstruction } from "../ReactiveScopes/InferReactiveScopeVariables"; import DisjointSet from "../Utils/DisjointSet"; import { getOrInsertDefault } from "../Utils/utils"; import { @@ -254,7 +254,7 @@ function visitPlace( * of the stack to the mutated outer scope. */ const placeScope = getPlaceScope(id, place); - if (placeScope != null && isMutable({ id } as any, place)) { + if (placeScope != null && isMutableAtInstruction({ id } as any, place)) { const placeScopeIdx = activeScopes.indexOf(placeScope); if (placeScopeIdx !== -1 && placeScopeIdx !== activeScopes.length - 1) { joined.union([placeScope, ...activeScopes.slice(placeScopeIdx + 1)]); diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/PrintHIR.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/PrintHIR.ts index ce604386bf..837b3729dc 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/PrintHIR.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/PrintHIR.ts @@ -41,6 +41,14 @@ export type Options = { indent: number; }; +export function printFunctionWithOutlined(fn: HIRFunction): string { + const output = [printFunction(fn)]; + for (const outlined of fn.env.getOutlinedFunctions()) { + output.push(`\nfunction ${outlined.fn.id}:\n${printHIR(outlined.fn.body)}`); + } + return output.join("\n"); +} + export function printFunction(fn: HIRFunction): string { const output = []; let definition = ""; @@ -843,14 +851,7 @@ export function printManualMemoDependency( ): string { let rootStr; if (val.root.kind === "Global") { - rootStr = val.root.binding.binding.name; - } else if (val.root.kind === "InlinedGlobal") { - const nameStr = nameOnly - ? val.root.value.identifier.name != null - ? printName(val.root.value.identifier.name) - : String(val.root.value.identifier.id) - : printIdentifier(val.root.value.identifier); - rootStr = `G(${val.root.name}=${nameStr})`; + rootStr = val.root.identifierName; } else { CompilerError.invariant(val.root.value.identifier.name?.kind === "named", { reason: "DepsValidation: expected named local variable in depslist", diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/visitors.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/visitors.ts index 91f07184b0..beda6e4a20 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/visitors.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/visitors.ts @@ -229,10 +229,7 @@ export function* eachInstructionValueOperand( case "StartMemoize": { if (instrValue.deps != null) { for (const dep of instrValue.deps) { - if ( - dep.root.kind === "NamedLocal" || - dep.root.kind === "InlinedGlobal" - ) { + if (dep.root.kind === "NamedLocal") { yield dep.root.value; } } @@ -557,10 +554,7 @@ export function mapInstructionValueOperands( case "StartMemoize": { if (instrValue.deps != null) { for (const dep of instrValue.deps) { - if ( - dep.root.kind === "NamedLocal" || - dep.root.kind === "InlinedGlobal" - ) { + if (dep.root.kind === "NamedLocal") { dep.root.value = fn(dep.root.value); } } diff --git a/compiler/packages/babel-plugin-react-compiler/src/Inference/DropManualMemoization.ts b/compiler/packages/babel-plugin-react-compiler/src/Inference/DropManualMemoization.ts index d70b0c0dd1..932cb4cc80 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Inference/DropManualMemoization.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Inference/DropManualMemoization.ts @@ -58,7 +58,7 @@ export function collectMaybeMemoDependencies( return { root: { kind: "Global", - binding: value, + identifierName: value.binding.name, }, path: [], }; @@ -173,53 +173,24 @@ function makeManualMemoizationMarkers( env: Environment, depsList: Array | null, memoDecl: Place, - manualMemoId: number, - isManualUseMemoEnabled: boolean -): [ - Array | TInstruction>, - TInstruction, -] { - let globals: Array | TInstruction> = - []; - for (const dep of depsList ?? []) { - if (dep.root.kind === "Global" && isManualUseMemoEnabled) { - const place = createTemporaryPlace(env, dep.root.binding.loc); - globals.push({ - id: makeInstructionId(0), - lvalue: place, - value: { - kind: "LoadGlobal", - binding: dep.root.binding.binding, - loc: dep.root.binding.loc, - }, - loc: dep.root.binding.loc, - }); - dep.root = { - kind: "InlinedGlobal", - value: place, - name: dep.root.binding.binding.name, - }; - } - } + manualMemoId: number +): [TInstruction, TInstruction] { return [ - [ - ...globals, - { - id: makeInstructionId(0), - lvalue: createTemporaryPlace(env, fnExpr.loc), - value: { - kind: "StartMemoize", - manualMemoId, - /* - * Use deps list from source instead of inferred deps - * as dependencies - */ - deps: depsList, - loc: fnExpr.loc, - }, + { + id: makeInstructionId(0), + lvalue: createTemporaryPlace(env, fnExpr.loc), + value: { + kind: "StartMemoize", + manualMemoId, + /* + * Use deps list from source instead of inferred deps + * as dependencies + */ + deps: depsList, loc: fnExpr.loc, }, - ], + loc: fnExpr.loc, + }, { id: makeInstructionId(0), lvalue: createTemporaryPlace(env, fnExpr.loc), @@ -362,14 +333,9 @@ function extractManualMemoizationArgs( * eg `React.useMemo()`. */ export function dropManualMemoization(func: HIRFunction): void { - const isManualUseMemoEnabled = - func.env.config.enablePreserveExistingManualUseMemo === "scope" || - func.env.config.enableChangeDetection != null || - func.env.config.disableMemoizationForDebugging; const isValidationEnabled = func.env.config.validatePreserveExistingMemoizationGuarantees || - func.env.config.enablePreserveExistingMemoizationGuarantees || - isManualUseMemoEnabled; + func.env.config.enablePreserveExistingMemoizationGuarantees; const sidemap: IdentifierSidemap = { functions: new Map(), manualMemos: new Map(), @@ -390,11 +356,7 @@ export function dropManualMemoization(func: HIRFunction): void { */ const queuedInserts: Map< InstructionId, - Array< - | TInstruction - | TInstruction - | TInstruction - > + TInstruction | TInstruction > = new Map(); for (const [_, block] of func.body.blocks) { for (let i = 0; i < block.instructions.length; i++) { @@ -457,8 +419,7 @@ export function dropManualMemoization(func: HIRFunction): void { func.env, depsList, memoDecl, - nextManualMemoId++, - isManualUseMemoEnabled + nextManualMemoId++ ); /** @@ -475,7 +436,7 @@ export function dropManualMemoization(func: HIRFunction): void { * ``` */ queuedInserts.set(manualMemo.loadInstr.id, startMarker); - queuedInserts.set(instr.id, [finishMarker]); + queuedInserts.set(instr.id, finishMarker); } } } else { @@ -496,16 +457,8 @@ export function dropManualMemoization(func: HIRFunction): void { const insertInstr = queuedInserts.get(instr.id); if (insertInstr != null) { nextInstructions = nextInstructions ?? block.instructions.slice(0, i); - const postInstructions: Array = []; - insertInstr.forEach((instr) => { - if (instr.value.kind === "LoadGlobal") { - nextInstructions?.push(instr); - } else { - postInstructions.push(instr); - } - }); nextInstructions.push(instr); - postInstructions.forEach((instr) => nextInstructions?.push(instr)); + nextInstructions.push(insertInstr); } else if (nextInstructions != null) { nextInstructions.push(instr); } diff --git a/compiler/packages/babel-plugin-react-compiler/src/Inference/InferReactivePlaces.ts b/compiler/packages/babel-plugin-react-compiler/src/Inference/InferReactivePlaces.ts index 6d53c8f157..c543dba483 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Inference/InferReactivePlaces.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Inference/InferReactivePlaces.ts @@ -26,7 +26,7 @@ import { } from "../HIR/visitors"; import { findDisjointMutableValues, - isMutable, + isMutableAtInstruction, } from "../ReactiveScopes/InferReactiveScopeVariables"; import DisjointSet from "../Utils/DisjointSet"; import { assertExhaustive } from "../Utils/utils"; @@ -232,7 +232,7 @@ export function inferReactivePlaces(fn: HIRFunction): void { case Effect.Store: case Effect.ConditionallyMutate: case Effect.Mutate: { - if (isMutable(instruction, operand)) { + if (isMutableAtInstruction(instruction, operand)) { const resolvedId = identifierMapping.get(operand.identifier); if (resolvedId !== undefined) { reactiveIdentifiers.markReactiveIdentifier(resolvedId); diff --git a/compiler/packages/babel-plugin-react-compiler/src/Optimization/OutlineFunctions.ts b/compiler/packages/babel-plugin-react-compiler/src/Optimization/OutlineFunctions.ts new file mode 100644 index 0000000000..a4b97c09c2 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/Optimization/OutlineFunctions.ts @@ -0,0 +1,47 @@ +/** + * 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. + */ + +import { HIRFunction } from "../HIR"; + +export function outlineFunctions(fn: HIRFunction): void { + for (const [, block] of fn.body.blocks) { + for (const instr of block.instructions) { + const { value } = instr; + + if ( + value.kind === "FunctionExpression" || + value.kind === "ObjectMethod" + ) { + // Recurse in case there are inner functions which can be outlined + outlineFunctions(value.loweredFunc.func); + } + + if ( + value.kind === "FunctionExpression" && + value.loweredFunc.dependencies.length === 0 && + value.loweredFunc.func.context.length === 0 && + // TODO: handle outlining named functions + value.loweredFunc.func.id === null + ) { + const loweredFunc = value.loweredFunc.func; + + const id = fn.env.generateGloballyUniqueIdentifierName(loweredFunc.id); + loweredFunc.id = id.value; + + fn.env.outlineFunction(loweredFunc, null); + instr.value = { + kind: "LoadGlobal", + binding: { + kind: "Global", + name: id.value, + }, + loc: value.loc, + }; + } + } + } +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/CodegenReactiveFunction.ts b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/CodegenReactiveFunction.ts index d91284384f..b0c3a89182 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/CodegenReactiveFunction.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/CodegenReactiveFunction.ts @@ -7,7 +7,12 @@ import * as t from "@babel/types"; import { createHmac } from "crypto"; -import { pruneHoistedContexts, pruneUnusedLValues, pruneUnusedLabels } from "."; +import { + pruneHoistedContexts, + pruneUnusedLValues, + pruneUnusedLabels, + renameVariables, +} from "."; import { CompilerError, ErrorSeverity } from "../CompilerError"; import { Environment, EnvironmentConfig, ExternalFunction } from "../HIR"; import { @@ -45,6 +50,7 @@ import { assertExhaustive } from "../Utils/utils"; import { buildReactiveFunction } from "./BuildReactiveFunction"; import { SINGLE_CHILD_FBT_TAGS } from "./MemoizeFbtAndMacroOperandsInSameScope"; import { ReactiveFunctionVisitor, visitReactiveFunction } from "./visitors"; +import { ReactFunctionType } from "../HIR/Environment"; export const MEMO_CACHE_SENTINEL = "react.memo_cache_sentinel"; export const EARLY_RETURN_SENTINEL = "react.early_return_sentinel"; @@ -85,6 +91,11 @@ export type CodegenFunction = { * because they were part of a pruned memo block. */ prunedMemoValues: number; + + outlined: Array<{ + fn: CodegenFunction; + type: ReactFunctionType | null; + }>; }; export function codegenFunction( @@ -258,6 +269,29 @@ export function codegenFunction( compiled.body.body.unshift(test); } + const outlined: CodegenFunction["outlined"] = []; + for (const { fn: outlinedFunction, type } of cx.env.getOutlinedFunctions()) { + const reactiveFunction = buildReactiveFunction(outlinedFunction); + pruneUnusedLabels(reactiveFunction); + pruneUnusedLValues(reactiveFunction); + pruneHoistedContexts(reactiveFunction); + + const identifiers = renameVariables(reactiveFunction); + const codegen = codegenReactiveFunction( + new Context( + cx.env, + reactiveFunction.id ?? "[[ anonymous ]]", + identifiers + ), + reactiveFunction + ); + if (codegen.isErr()) { + return codegen; + } + outlined.push({ fn: codegen.unwrap(), type }); + } + compiled.outlined = outlined; + return compileResult; } @@ -306,6 +340,7 @@ function codegenReactiveFunction( memoValues: countMemoBlockVisitor.memoValues, prunedMemoBlocks: countMemoBlockVisitor.prunedMemoBlocks, prunedMemoValues: countMemoBlockVisitor.prunedMemoValues, + outlined: [], }); } @@ -622,11 +657,7 @@ function codegenReactiveScope( ); } - if ( - cx.env.config.disableMemoizationForDebugging && - !scope.source && - cx.env.config.enableChangeDetection == null - ) { + if (cx.env.config.disableMemoizationForDebugging) { testCondition = t.logicalExpression( "||", testCondition, @@ -636,10 +667,7 @@ function codegenReactiveScope( let computationBlock = codegenBlock(cx, block); let memoStatement; - if ( - cx.env.config.enableChangeDetection != null && - changeExpressions.length > 0 - ) { + if (cx.env.config.enableChangeDetection != null) { const loc = typeof scope.loc === "symbol" ? "unknown location" @@ -647,9 +675,9 @@ function codegenReactiveScope( const detectionFunction = cx.env.config.enableChangeDetection.structuralCheck; const cacheLoadOldValueStatements: Array = []; + const restoreOldValueStatements: Array = []; const changeDetectionStatements: Array = []; const idempotenceDetectionStatements: Array = []; - const restoreOldValueStatements: Array = []; for (const { name: { name: nameStr }, @@ -691,7 +719,7 @@ function codegenReactiveScope( t.variableDeclarator(t.identifier(loadNameStr), genSlot()), ]) ); - if (scope.source || !cx.env.config.disableMemoizationForDebugging) { + if (!cx.env.config.disableMemoizationForDebugging) { restoreOldValueStatements.push( t.expressionStatement( t.assignmentExpression("=", t.identifier(nameStr), restoredValue) @@ -740,7 +768,6 @@ function codegenReactiveScope( t.blockStatement([ ...cacheLoadOldValueStatements, ...changeDetectionStatements, - ...restoreOldValueStatements, ]) ), ...cacheStoreStatements, diff --git a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/InferReactiveScopeVariables.ts b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/InferReactiveScopeVariables.ts index aef67635f8..fbf7600145 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/InferReactiveScopeVariables.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/InferReactiveScopeVariables.ts @@ -111,13 +111,16 @@ export function inferReactiveScopeVariables(fn: HIRFunction): void { earlyReturnValue: null, merged: new Set(), loc: identifier.loc, - source: false, }; scopes.set(groupIdentifier, scope); } else { - scope.range.start = makeInstructionId( - Math.min(scope.range.start, identifier.mutableRange.start) - ); + if (scope.range.start === 0) { + scope.range.start = identifier.mutableRange.start; + } else if (identifier.mutableRange.start !== 0) { + scope.range.start = makeInstructionId( + Math.min(scope.range.start, identifier.mutableRange.start) + ); + } scope.range.end = makeInstructionId( Math.max(scope.range.end, identifier.mutableRange.end) ); @@ -162,10 +165,7 @@ export function inferReactiveScopeVariables(fn: HIRFunction): void { } } -export function mergeLocation( - l: SourceLocation, - r: SourceLocation -): SourceLocation { +function mergeLocation(l: SourceLocation, r: SourceLocation): SourceLocation { if (l === GeneratedSource) { return r; } else if (r === GeneratedSource) { @@ -185,25 +185,23 @@ export function mergeLocation( } // Is the operand mutable at this given instruction -export function isMutable({ id }: Instruction, place: Place): boolean { +export function isMutableAtInstruction( + { id }: Instruction, + place: Place +): boolean { const range = place.identifier.mutableRange; return id >= range.start && id < range.end; } -export function mayAllocate( - env: Environment, - instruction: Instruction, - conservative: boolean -): boolean { +function mayAllocate(env: Environment, instruction: Instruction): boolean { const { value } = instruction; switch (value.kind) { case "Destructure": { - return ( - doesPatternContainSpreadElement(value.lvalue.pattern) || conservative - ); + return doesPatternContainSpreadElement(value.lvalue.pattern); } case "PostfixUpdate": case "PrefixUpdate": + case "Await": case "DeclareLocal": case "DeclareContext": case "StoreLocal": @@ -214,31 +212,26 @@ export function mayAllocate( case "LoadContext": case "StoreContext": case "PropertyDelete": + case "ComputedLoad": case "ComputedDelete": case "JSXText": case "TemplateLiteral": case "Primitive": case "GetIterator": case "IteratorNext": + case "NextPropertyOf": case "Debugger": case "StartMemoize": case "FinishMemoize": case "UnaryExpression": case "BinaryExpression": + case "PropertyLoad": case "StoreGlobal": { return false; } - case "PropertyLoad": - case "NextPropertyOf": - case "ComputedLoad": - case "Await": { - return conservative; - } case "CallExpression": case "MethodCall": { - return ( - conservative || instruction.lvalue.identifier.type.kind !== "Primitive" - ); + return instruction.lvalue.identifier.type.kind !== "Primitive"; } case "RegExpLiteral": case "PropertyStore": @@ -263,80 +256,85 @@ export function mayAllocate( } } -export function collectMutableOperands( - fn: HIRFunction, - instr: Instruction, - conservative: boolean -): Array { - const operands: Array = []; - const range = instr.lvalue.identifier.mutableRange; - if (range.end > range.start + 1 || mayAllocate(fn.env, instr, conservative)) { - operands.push(instr.lvalue!.identifier); +/* + * These instructions may pick up external changes due to rules of react violations. + * Instructions should be included here if they may change without their inputs changing. + * For example, PostfixUpdate is not included because it only has a changed lval if + * it has a changed argument, but LoadProperty is included because the argument can be + * mutated elsewhere. + */ +function mayHaveChanged(env: Environment, instruction: Instruction): boolean { + if (env.config.enableChangeDetection == null) { + return false; } - if ( - instr.value.kind === "StoreLocal" || - instr.value.kind === "StoreContext" - ) { - if ( - instr.value.lvalue.place.identifier.mutableRange.end > - instr.value.lvalue.place.identifier.mutableRange.start + 1 - ) { - operands.push(instr.value.lvalue.place.identifier); + switch (instruction.value.kind) { + case "Await": + case "ComputedLoad": + case "Destructure": + case "GetIterator": + case "IteratorNext": + case "NextPropertyOf": + case "PropertyLoad": + case "CallExpression": + case "MethodCall": + case "NewExpression": { + return true; } - if ( - isMutable(instr, instr.value.value) && - instr.value.value.identifier.mutableRange.start > 0 - ) { - operands.push(instr.value.value.identifier); + case "LoadGlobal": { + return ( + instruction.value.binding.kind === "ModuleLocal" && + !instruction.value.binding.immutable + ); } - } else if (instr.value.kind === "Destructure") { - for (const place of eachPatternOperand(instr.value.lvalue.pattern)) { - if ( - place.identifier.mutableRange.end > - place.identifier.mutableRange.start + 1 - ) { - operands.push(place.identifier); - } + case "PostfixUpdate": + case "PrefixUpdate": + case "DeclareLocal": + case "DeclareContext": + case "StoreLocal": + case "MetaProperty": + case "TypeCastExpression": + case "LoadLocal": + case "LoadContext": + case "StoreContext": + case "PropertyDelete": + case "ComputedDelete": + case "JSXText": + case "TemplateLiteral": + case "Primitive": + case "Debugger": + case "StartMemoize": + case "FinishMemoize": + case "UnaryExpression": + case "BinaryExpression": + case "StoreGlobal": + case "RegExpLiteral": + case "PropertyStore": + case "ComputedStore": + case "ArrayExpression": + case "JsxExpression": + case "JsxFragment": + case "ObjectExpression": + case "UnsupportedNode": + case "ObjectMethod": + case "FunctionExpression": + case "TaggedTemplateExpression": { + return false; } - if ( - isMutable(instr, instr.value.value) && - instr.value.value.identifier.mutableRange.start > 0 - ) { - operands.push(instr.value.value.identifier); - } - } else if (instr.value.kind === "MethodCall") { - for (const operand of eachInstructionOperand(instr)) { - if ( - isMutable(instr, operand) && - /* - * exclude global variables from being added to scopes, we can't recreate them! - * TODO: improve handling of module-scoped variables and globals - */ - operand.identifier.mutableRange.start > 0 - ) { - operands.push(operand.identifier); - } - } - /* - * Ensure that the ComputedLoad to resolve the method is in the same scope as the - * call itself - */ - operands.push(instr.value.property.identifier); - } else { - for (const operand of eachInstructionOperand(instr)) { - if ( - isMutable(instr, operand) && - /* - * exclude global variables from being added to scopes, we can't recreate them! - * TODO: improve handling of module-scoped variables and globals - */ - operand.identifier.mutableRange.start > 0 - ) { - operands.push(operand.identifier); - } + default: { + assertExhaustive( + instruction.value, + `Unexpected value kind \`${(instruction.value as any).kind}\`` + ); } } - return operands; +} + +function isIdentifierMutable(id: Identifier): boolean { + return id.mutableRange.end > id.mutableRange.start + 1; +} + +function identifierHasMutableRange(id: Identifier): boolean { + return id.mutableRange.start > 0; } export function findDisjointMutableValues( @@ -351,7 +349,7 @@ export function findDisjointMutableValues( for (const phi of block.phis) { if ( // The phi was reset because it was not mutated after creation - phi.id.mutableRange.start + 1 !== phi.id.mutableRange.end && + isIdentifierMutable(phi.id) && phi.id.mutableRange.end > (block.instructions.at(0)?.id ?? block.terminal.id) ) { @@ -366,11 +364,77 @@ export function findDisjointMutableValues( } for (const instr of block.instructions) { - const operands = collectMutableOperands( - fn, - instr, - fn.env.config.enableChangeDetection != null - ); + const operands: Array = []; + if ( + isIdentifierMutable(instr.lvalue.identifier) || + mayAllocate(fn.env, instr) || + mayHaveChanged(fn.env, instr) + ) { + operands.push(instr.lvalue!.identifier); + } + if ( + instr.value.kind === "StoreLocal" || + instr.value.kind === "StoreContext" + ) { + if (isIdentifierMutable(instr.value.lvalue.place.identifier)) { + operands.push(instr.value.lvalue.place.identifier); + } + if ( + isMutableAtInstruction(instr, instr.value.value) && + identifierHasMutableRange(instr.value.value.identifier) + ) { + operands.push(instr.value.value.identifier); + } + } else if (instr.value.kind === "Destructure") { + for (const place of eachPatternOperand(instr.value.lvalue.pattern)) { + if ( + isIdentifierMutable(place.identifier) || + mayHaveChanged(fn.env, instr) + ) { + operands.push(place.identifier); + } + } + if ( + (isMutableAtInstruction(instr, instr.value.value) && + identifierHasMutableRange(instr.value.value.identifier)) || + mayHaveChanged(fn.env, instr) + ) { + operands.push(instr.value.value.identifier); + } + } else if (instr.value.kind === "MethodCall") { + for (const operand of eachInstructionOperand(instr)) { + if ( + (isMutableAtInstruction(instr, operand) && + /* + * exclude global variables from being added to scopes, we can't recreate them! + * TODO: improve handling of module-scoped variables and globals + */ + identifierHasMutableRange(operand.identifier)) || + mayHaveChanged(fn.env, instr) + ) { + operands.push(operand.identifier); + } + } + /* + * Ensure that the ComputedLoad to resolve the method is in the same scope as the + * call itself + */ + operands.push(instr.value.property.identifier); + } else { + for (const operand of eachInstructionOperand(instr)) { + if ( + (isMutableAtInstruction(instr, operand) && + /* + * exclude global variables from being added to scopes, we can't recreate them! + * TODO: improve handling of module-scoped variables and globals + */ + identifierHasMutableRange(operand.identifier)) || + mayHaveChanged(fn.env, instr) + ) { + operands.push(operand.identifier); + } + } + } if (operands.length !== 0) { scopeIdentifiers.union(operands); } diff --git a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/MemoizeExistingUseMemos.ts b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/MemoizeExistingUseMemos.ts deleted file mode 100644 index 8db9c29ff8..0000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/MemoizeExistingUseMemos.ts +++ /dev/null @@ -1,121 +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. - */ - -import { CompilerError } from "../CompilerError"; -import { - BasicBlock, - HIRFunction, - Identifier, - makeInstructionId, - ReactiveScope, - ReactiveScopeDependencies, -} from "../HIR"; -import { eachTerminalSuccessor } from "../HIR/visitors"; -import { - collectMutableOperands, - mergeLocation, -} from "./InferReactiveScopeVariables"; - -export function memoizeExistingUseMemos(fn: HIRFunction): void { - visitBlock(fn, fn.body.blocks.get(fn.body.entry)!, null, new Map()); -} - -let ctr = 0; -function nextId(): number { - return ctr++; -} - -type CurrentScope = - | null - | { kind: "pending"; deps: ReactiveScopeDependencies; id: number } - | { kind: "available"; scope: ReactiveScope; id: number }; - -function visitBlock( - fn: HIRFunction, - block: BasicBlock, - scope: CurrentScope, - seen: Map -): void { - const visited = seen.get(block.id); - if (visited === undefined) { - seen.set(block.id, scope); - } else { - CompilerError.invariant( - visited === null ? scope === null : visited.id === scope?.id, - { - reason: - "MemoizeExistingUseMemos: visiting the same block with different scopes", - loc: null, - suggestions: null, - } - ); - return; - } - - function extend( - currentScope: ReactiveScope, - operands: Iterable - ): void { - for (const operand of operands) { - currentScope.range.start = makeInstructionId( - Math.min(currentScope.range.start, operand.mutableRange.start) - ); - currentScope.range.end = makeInstructionId( - Math.max(currentScope.range.end, operand.mutableRange.end) - ); - currentScope.loc = mergeLocation(currentScope.loc, operand.loc); - operand.scope = currentScope; - operand.mutableRange = currentScope.range; - } - } - - let currentScope = scope; - for (const instruction of block.instructions) { - if (instruction.value.kind === "StartMemoize") { - const deps: ReactiveScopeDependencies = new Set(); - for (const dep of instruction.value.deps ?? []) { - CompilerError.invariant(dep.root.kind !== "Global", { - reason: - "MemoizeExistingUseMemos: Globals should have been replaced with InlineGlobals", - loc: instruction.loc, - suggestions: null, - description: null, - }); - deps.add({ identifier: dep.root.value.identifier, path: dep.path }); - } - currentScope = { kind: "pending", id: nextId(), deps }; - } else if (instruction.value.kind === "FinishMemoize") { - currentScope = null; - } else if (currentScope != null) { - const operands = collectMutableOperands(fn, instruction, true); - if (operands.length > 0) { - if (currentScope.kind === "pending") { - currentScope = { - kind: "available", - id: currentScope.id, - scope: { - id: fn.env.nextScopeId, - range: { start: instruction.id, end: instruction.id }, - dependencies: currentScope.deps, - declarations: new Map(), - reassignments: new Set(), - earlyReturnValue: null, - merged: new Set(), - loc: instruction.loc, - source: true, - }, - }; - } - extend(currentScope.scope, operands); - } - } - } - - for (const successor of eachTerminalSuccessor(block.terminal)) { - visitBlock(fn, fn.body.blocks.get(successor)!, currentScope, seen); - } -} diff --git a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/MemoizeFbtAndMacroOperandsInSameScope.ts b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/MemoizeFbtAndMacroOperandsInSameScope.ts index 91121cd9bf..6d034cd72d 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/MemoizeFbtAndMacroOperandsInSameScope.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/MemoizeFbtAndMacroOperandsInSameScope.ts @@ -43,6 +43,7 @@ export function memoizeFbtAndMacroOperandsInSameScope(fn: HIRFunction): void { const fbtMacroTags = new Set([ ...FBT_TAGS, ...(fn.env.config.customMacros ?? []), + ...(fn.env.preserveManualMemo() ? ["useMemo", "useCallback"] : []), ]); const fbtValues: Set = new Set(); while (true) { diff --git a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/MergeReactiveScopesThatInvalidateTogether.ts b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/MergeReactiveScopesThatInvalidateTogether.ts index 61378689ae..e26fca1353 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/MergeReactiveScopesThatInvalidateTogether.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/MergeReactiveScopesThatInvalidateTogether.ts @@ -15,6 +15,7 @@ import { ReactiveFunction, ReactiveScope, ReactiveScopeBlock, + ReactiveScopeDependencies, ReactiveScopeDependency, ReactiveStatement, Type, @@ -108,7 +109,7 @@ class FindLastUsageVisitor extends ReactiveFunctionVisitor { } } -class Transform extends ReactiveFunctionTransform { +class Transform extends ReactiveFunctionTransform { lastUsage: Map; constructor(lastUsage: Map) { @@ -118,13 +119,12 @@ class Transform extends ReactiveFunctionTransform { override transformScope( scopeBlock: ReactiveScopeBlock, - state: ReactiveScope | null + state: ReactiveScopeDependencies | null ): Transformed { - this.visitScope(scopeBlock, scopeBlock.scope); + this.visitScope(scopeBlock, scopeBlock.scope.dependencies); if ( state !== null && - areEqualDependencies(state.dependencies, scopeBlock.scope.dependencies) && - state.source === scopeBlock.scope.source + areEqualDependencies(state, scopeBlock.scope.dependencies) ) { return { kind: "replace-many", value: scopeBlock.instructions }; } else { @@ -132,7 +132,10 @@ class Transform extends ReactiveFunctionTransform { } } - override visitBlock(block: ReactiveBlock, state: ReactiveScope | null): void { + override visitBlock( + block: ReactiveBlock, + state: ReactiveScopeDependencies | null + ): void { // Pass 1: visit nested blocks to potentially merge their scopes this.traverseBlock(block, state); @@ -414,9 +417,6 @@ function canMergeScopes( current: ReactiveScopeBlock, next: ReactiveScopeBlock ): boolean { - if (current.scope.source !== next.scope.source) { - return false; - } // Don't merge scopes with reassignments if ( current.scope.reassignments.size !== 0 || diff --git a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/PrintReactiveFunction.ts b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/PrintReactiveFunction.ts index aedd3d7d22..7f6e347fe7 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/PrintReactiveFunction.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/PrintReactiveFunction.ts @@ -17,6 +17,7 @@ import { ReactiveValue, } from "../HIR/HIR"; import { + printFunction, printIdentifier, printInstructionValue, printPlace, @@ -24,8 +25,24 @@ import { } from "../HIR/PrintHIR"; import { assertExhaustive } from "../Utils/utils"; +export function printReactiveFunctionWithOutlined( + fn: ReactiveFunction +): string { + const writer = new Writer(); + writeReactiveFunction(fn, writer); + for (const outlined of fn.env.getOutlinedFunctions()) { + writer.writeLine("\nfunction " + printFunction(outlined.fn)); + } + return writer.complete(); +} + export function printReactiveFunction(fn: ReactiveFunction): string { const writer = new Writer(); + writeReactiveFunction(fn, writer); + return writer.complete(); +} + +function writeReactiveFunction(fn: ReactiveFunction, writer: Writer): void { writer.writeLine(`function ${fn.id !== null ? fn.id : ""}(`); writer.indented(() => { for (const param of fn.params) { @@ -39,7 +56,6 @@ export function printReactiveFunction(fn: ReactiveFunction): string { writer.writeLine(") {"); writeReactiveInstructions(writer, fn.body); writer.writeLine("}"); - return writer.complete(); } export function printReactiveScopeSummary(scope: ReactiveScope): string { diff --git a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/PropagateEarlyReturns.ts b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/PropagateEarlyReturns.ts index ef2c217e25..3012314e9e 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/PropagateEarlyReturns.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/PropagateEarlyReturns.ts @@ -24,8 +24,6 @@ import { EARLY_RETURN_SENTINEL } from "./CodegenReactiveFunction"; import { ReactiveFunctionTransform, Transformed } from "./visitors"; /** - * TODO: Actualy propagate early return information, for now we throw a Todo bailout. - * * This pass ensures that reactive blocks honor the control flow behavior of the * original code including early return semantics. Specifically, if a reactive * scope early returned during the previous execution and the inputs to that block @@ -135,6 +133,14 @@ class Transform extends ReactiveFunctionTransform { scopeBlock: ReactiveScopeBlock, parentState: State ): void { + /** + * Exit early if an earlier pass has already created an early return, + * which may happen in alternate compiler configurations. + */ + if (scopeBlock.scope.earlyReturnValue !== null) { + return; + } + const innerState: State = { withinReactiveScope: true, earlyReturnValue: parentState.earlyReturnValue, diff --git a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/PropagateScopeDependencies.ts b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/PropagateScopeDependencies.ts index b7cabe7f54..bc684ffe36 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/PropagateScopeDependencies.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/PropagateScopeDependencies.ts @@ -685,9 +685,7 @@ class PropagationVisitor extends ReactiveFunctionVisitor { const scopeDependencies = context.enter(scope.scope, () => { this.visitBlock(scope.instructions, context); }); - if (!scope.scope.source) { - scope.scope.dependencies = scopeDependencies; - } + scope.scope.dependencies = scopeDependencies; } override visitPrunedScope( diff --git a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/PruneInitializationDependencies.ts b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/PruneInitializationDependencies.ts index 50ec6099ad..b9939addcf 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/PruneInitializationDependencies.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/PruneInitializationDependencies.ts @@ -183,7 +183,7 @@ class Visitor extends ReactiveFunctionVisitor { ident.path.forEach((key) => { target &&= this.paths.get(target)?.get(key); }); - if (target && this.map.get(target) === "Create" && !scope.scope.source) { + if (target && this.map.get(target) === "Create") { scope.scope.dependencies.delete(ident); } }); diff --git a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/PruneNonEscapingScopes.ts b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/PruneNonEscapingScopes.ts index 7714599719..86f4e27f5a 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/PruneNonEscapingScopes.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/PruneNonEscapingScopes.ts @@ -934,10 +934,14 @@ class PruneScopesTransform extends ReactiveFunctionTransform< * is early-returned from within the scope. For now we intentionaly keep * these scopes, and let them get pruned later by PruneUnusedScopes * _after_ handling the early-return case in PropagateEarlyReturns. + * + * Also keep the scope if an early return was created by some earlier pass, + * which may happen in alternate compiler configurations. */ if ( - scopeBlock.scope.declarations.size === 0 && - scopeBlock.scope.reassignments.size === 0 + (scopeBlock.scope.declarations.size === 0 && + scopeBlock.scope.reassignments.size === 0) || + scopeBlock.scope.earlyReturnValue !== null ) { return { kind: "keep" }; } @@ -949,7 +953,7 @@ class PruneScopesTransform extends ReactiveFunctionTransform< Array.from(scopeBlock.scope.reassignments).some((identifier) => state.has(identifier.id) ); - if (hasMemoizedOutput || scopeBlock.scope.source) { + if (hasMemoizedOutput) { return { kind: "keep" }; } else { this.prunedScopes.add(scopeBlock.scope.id); diff --git a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/PruneNonReactiveDependencies.ts b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/PruneNonReactiveDependencies.ts index b6cb3453a8..2e1748fc6a 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/PruneNonReactiveDependencies.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/PruneNonReactiveDependencies.ts @@ -97,7 +97,7 @@ class Visitor extends ReactiveFunctionVisitor { this.traverseScope(scopeBlock, state); for (const dep of scopeBlock.scope.dependencies) { const isReactive = state.has(dep.identifier.id); - if (!isReactive && !scopeBlock.scope.source) { + if (!isReactive) { scopeBlock.scope.dependencies.delete(dep); } } diff --git a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/PruneUnusedScopes.ts b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/PruneUnusedScopes.ts index 5e4ba867e4..11cc928874 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/PruneUnusedScopes.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/PruneUnusedScopes.ts @@ -43,7 +43,6 @@ class Transform extends ReactiveFunctionTransform { this.visitScope(scopeBlock, scopeState); if ( !scopeState.hasReturnStatement && - !scopeBlock.scope.source && scopeBlock.scope.reassignments.size === 0 && (scopeBlock.scope.declarations.size === 0 || /* diff --git a/compiler/packages/babel-plugin-react-compiler/src/Utils/Result.ts b/compiler/packages/babel-plugin-react-compiler/src/Utils/Result.ts index 48205829a5..85fb3922d6 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Utils/Result.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Utils/Result.ts @@ -124,11 +124,11 @@ class OkImpl implements Result { return this; } - isOk(): boolean { + isOk(): this is OkImpl { return true; } - isErr(): boolean { + isErr(): this is ErrImpl { return false; } @@ -199,11 +199,11 @@ class ErrImpl implements Result { return fn(this.val); } - isOk(): boolean { + isOk(): this is OkImpl { return false; } - isErr(): boolean { + isErr(): this is ErrImpl { return true; } diff --git a/compiler/packages/babel-plugin-react-compiler/src/Utils/logger.ts b/compiler/packages/babel-plugin-react-compiler/src/Utils/logger.ts index 125c291602..def921aeb0 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Utils/logger.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Utils/logger.ts @@ -9,8 +9,9 @@ import generate from "@babel/generator"; import * as t from "@babel/types"; import chalk from "chalk"; import { HIR, HIRFunction, ReactiveFunction } from "../HIR/HIR"; -import { printFunction, printHIR } from "../HIR/PrintHIR"; -import { CodegenFunction, printReactiveFunction } from "../ReactiveScopes"; +import { printFunctionWithOutlined, printHIR } from "../HIR/PrintHIR"; +import { CodegenFunction } from "../ReactiveScopes"; +import { printReactiveFunctionWithOutlined } from "../ReactiveScopes/PrintReactiveFunction"; let ENABLED: boolean = false; @@ -79,7 +80,7 @@ export function logCodegenFunction(step: string, fn: CodegenFunction): void { export function logHIRFunction(step: string, fn: HIRFunction): void { if (ENABLED) { - const printed = printFunction(fn); + const printed = printFunctionWithOutlined(fn); if (printed !== lastLogged) { lastLogged = printed; process.stdout.write(`${chalk.green(step)}:\n${printed}\n\n`); @@ -91,7 +92,7 @@ export function logHIRFunction(step: string, fn: HIRFunction): void { export function logReactiveFunction(step: string, fn: ReactiveFunction): void { if (ENABLED) { - const printed = printReactiveFunction(fn); + const printed = printReactiveFunctionWithOutlined(fn); if (printed !== lastLogged) { lastLogged = printed; process.stdout.write(`${chalk.green(step)}:\n${printed}\n\n`); diff --git a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateMemoizedEffectDependencies.ts b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateMemoizedEffectDependencies.ts index a1c7cb736a..0df91e7493 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateMemoizedEffectDependencies.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateMemoizedEffectDependencies.ts @@ -17,7 +17,7 @@ import { isUseInsertionEffectHookType, isUseLayoutEffectHookType, } from "../HIR"; -import { isMutable } from "../ReactiveScopes/InferReactiveScopeVariables"; +import { isMutableAtInstruction } from "../ReactiveScopes/InferReactiveScopeVariables"; import { ReactiveFunctionVisitor, visitReactiveFunction, @@ -99,7 +99,7 @@ class Visitor extends ReactiveFunctionVisitor { const deps = instruction.value.args[1]!; if ( deps.kind === "Identifier" && - (isMutable(instruction as Instruction, deps) || + (isMutableAtInstruction(instruction as Instruction, deps) || isUnmemoized(deps.identifier, this.scopes)) ) { state.push({ diff --git a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidatePreservedManualMemoization.ts b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidatePreservedManualMemoization.ts index 7e4f21004f..829edcfdc9 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidatePreservedManualMemoization.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidatePreservedManualMemoization.ts @@ -25,7 +25,7 @@ import { import { printManualMemoDependency } from "../HIR/PrintHIR"; import { eachInstructionValueOperand } from "../HIR/visitors"; import { collectMaybeMemoDependencies } from "../Inference/DropManualMemoization"; -import { isMutable } from "../ReactiveScopes/InferReactiveScopeVariables"; +import { isMutableAtInstruction } from "../ReactiveScopes/InferReactiveScopeVariables"; import { ReactiveFunctionVisitor, visitReactiveFunction, @@ -146,13 +146,9 @@ function compareDeps( const rootsEqual = (inferred.root.kind === "Global" && source.root.kind === "Global" && - inferred.root.binding.binding.name === - source.root.binding.binding.name) || - ((inferred.root.kind === "NamedLocal" || - inferred.root.kind === "InlinedGlobal") && - (source.root.kind === "NamedLocal" || - source.root.kind === "InlinedGlobal") && - source.root.kind === inferred.root.kind && + inferred.root.identifierName === source.root.identifierName) || + (inferred.root.kind === "NamedLocal" && + source.root.kind === "NamedLocal" && inferred.root.value.identifier.id === source.root.value.identifier.id); if (!rootsEqual) { return CompareDependencyResult.RootDifference; @@ -382,8 +378,7 @@ class Visitor extends ReactiveFunctionVisitor { if ( state.manualMemoState != null && - state.manualMemoState.depsFromSource != null && - !scopeBlock.scope.source + state.manualMemoState.depsFromSource != null ) { for (const dep of scopeBlock.scope.dependencies) { validateInferredDep( @@ -469,7 +464,7 @@ class Visitor extends ReactiveFunctionVisitor { instruction.value as InstructionValue )) { if ( - isMutable(instruction as Instruction, value) || + isMutableAtInstruction(instruction as Instruction, value) || (isDecl && isUnmemoized(value.identifier, this.scopes)) ) { state.errors.push({ diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-global-mutation-in-effect-indirect-usecallback.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-global-mutation-in-effect-indirect-usecallback.expect.md index 7a75355609..0cdf31bbfb 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-global-mutation-in-effect-indirect-usecallback.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-global-mutation-in-effect-indirect-usecallback.expect.md @@ -40,57 +40,52 @@ import { useCallback, useEffect, useState } from "react"; let someGlobal = {}; function Component() { - const $ = _c(7); + const $ = _c(6); const [state, setState] = useState(someGlobal); + + const setGlobal = _temp; let t0; + let t1; if ($[0] === Symbol.for("react.memo_cache_sentinel")) { t0 = () => { - someGlobal.value = true; - }; - $[0] = t0; - } else { - t0 = $[0]; - } - const setGlobal = t0; - let t1; - let t2; - if ($[1] === Symbol.for("react.memo_cache_sentinel")) { - t1 = () => { setGlobal(); }; - t2 = []; + t1 = []; + $[0] = t0; $[1] = t1; - $[2] = t2; } else { + t0 = $[0]; t1 = $[1]; - t2 = $[2]; } - useEffect(t1, t2); + useEffect(t0, t1); + let t2; let t3; - let t4; - if ($[3] === Symbol.for("react.memo_cache_sentinel")) { - t3 = () => { + if ($[2] === Symbol.for("react.memo_cache_sentinel")) { + t2 = () => { setState(someGlobal.value); }; - t4 = [someGlobal]; + t3 = [someGlobal]; + $[2] = t2; $[3] = t3; - $[4] = t4; } else { + t2 = $[2]; t3 = $[3]; - t4 = $[4]; } - useEffect(t3, t4); + useEffect(t2, t3); - const t5 = String(state); - let t6; - if ($[5] !== t5) { - t6 =
{t5}
; + const t4 = String(state); + let t5; + if ($[4] !== t4) { + t5 =
{t4}
; + $[4] = t4; $[5] = t5; - $[6] = t6; } else { - t6 = $[6]; + t5 = $[5]; } - return t6; + return t5; +} +function _temp() { + someGlobal.value = true; } export const FIXTURE_ENTRYPOINT = { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-global-mutation-in-effect-indirect.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-global-mutation-in-effect-indirect.expect.md index 43503170c2..25a9943706 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-global-mutation-in-effect-indirect.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-global-mutation-in-effect-indirect.expect.md @@ -39,57 +39,52 @@ import { useEffect, useState } from "react"; let someGlobal = {}; function Component() { - const $ = _c(7); + const $ = _c(6); const [state, setState] = useState(someGlobal); + + const setGlobal = _temp; let t0; + let t1; if ($[0] === Symbol.for("react.memo_cache_sentinel")) { t0 = () => { - someGlobal.value = true; - }; - $[0] = t0; - } else { - t0 = $[0]; - } - const setGlobal = t0; - let t1; - let t2; - if ($[1] === Symbol.for("react.memo_cache_sentinel")) { - t1 = () => { setGlobal(); }; - t2 = []; + t1 = []; + $[0] = t0; $[1] = t1; - $[2] = t2; } else { + t0 = $[0]; t1 = $[1]; - t2 = $[2]; } - useEffect(t1, t2); + useEffect(t0, t1); + let t2; let t3; - let t4; - if ($[3] === Symbol.for("react.memo_cache_sentinel")) { - t3 = () => { + if ($[2] === Symbol.for("react.memo_cache_sentinel")) { + t2 = () => { setState(someGlobal.value); }; - t4 = [someGlobal]; + t3 = [someGlobal]; + $[2] = t2; $[3] = t3; - $[4] = t4; } else { + t2 = $[2]; t3 = $[3]; - t4 = $[4]; } - useEffect(t3, t4); + useEffect(t2, t3); - const t5 = String(state); - let t6; - if ($[5] !== t5) { - t6 =
{t5}
; + const t4 = String(state); + let t5; + if ($[4] !== t4) { + t5 =
{t4}
; + $[4] = t4; $[5] = t5; - $[6] = t6; } else { - t6 = $[6]; + t5 = $[5]; } - return t6; + return t5; +} +function _temp() { + someGlobal.value = true; } export const FIXTURE_ENTRYPOINT = { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-global-reassignment-in-effect-indirect.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-global-reassignment-in-effect-indirect.expect.md index fec286b265..47db96561e 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-global-reassignment-in-effect-indirect.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-global-reassignment-in-effect-indirect.expect.md @@ -39,57 +39,52 @@ import { useEffect, useState } from "react"; let someGlobal = false; function Component() { - const $ = _c(7); + const $ = _c(6); const [state, setState] = useState(someGlobal); + + const setGlobal = _temp; let t0; + let t1; if ($[0] === Symbol.for("react.memo_cache_sentinel")) { t0 = () => { - someGlobal = true; - }; - $[0] = t0; - } else { - t0 = $[0]; - } - const setGlobal = t0; - let t1; - let t2; - if ($[1] === Symbol.for("react.memo_cache_sentinel")) { - t1 = () => { setGlobal(); }; - t2 = []; + t1 = []; + $[0] = t0; $[1] = t1; - $[2] = t2; } else { + t0 = $[0]; t1 = $[1]; - t2 = $[2]; } - useEffect(t1, t2); + useEffect(t0, t1); + let t2; let t3; - let t4; - if ($[3] === Symbol.for("react.memo_cache_sentinel")) { - t3 = () => { + if ($[2] === Symbol.for("react.memo_cache_sentinel")) { + t2 = () => { setState(someGlobal); }; - t4 = [someGlobal]; + t3 = [someGlobal]; + $[2] = t2; $[3] = t3; - $[4] = t4; } else { + t2 = $[2]; t3 = $[3]; - t4 = $[4]; } - useEffect(t3, t4); + useEffect(t2, t3); - const t5 = String(state); - let t6; - if ($[5] !== t5) { - t6 =
{t5}
; + const t4 = String(state); + let t5; + if ($[4] !== t4) { + t5 =
{t4}
; + $[4] = t4; $[5] = t5; - $[6] = t6; } else { - t6 = $[6]; + t5 = $[5]; } - return t6; + return t5; +} +function _temp() { + someGlobal = true; } export const FIXTURE_ENTRYPOINT = { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-global-reassignment-in-effect.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-global-reassignment-in-effect.expect.md index e4cd0e22c1..1ec48d6eda 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-global-reassignment-in-effect.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-global-reassignment-in-effect.expect.md @@ -36,48 +36,44 @@ import { useEffect, useState } from "react"; let someGlobal = false; function Component() { - const $ = _c(6); + const $ = _c(5); const [state, setState] = useState(someGlobal); let t0; - let t1; if ($[0] === Symbol.for("react.memo_cache_sentinel")) { - t0 = () => { - someGlobal = true; - }; - - t1 = []; + t0 = []; $[0] = t0; - $[1] = t1; } else { t0 = $[0]; - t1 = $[1]; } - useEffect(t0, t1); + useEffect(_temp, t0); + let t1; let t2; - let t3; - if ($[2] === Symbol.for("react.memo_cache_sentinel")) { - t2 = () => { + if ($[1] === Symbol.for("react.memo_cache_sentinel")) { + t1 = () => { setState(someGlobal); }; - t3 = [someGlobal]; + t2 = [someGlobal]; + $[1] = t1; $[2] = t2; - $[3] = t3; } else { + t1 = $[1]; t2 = $[2]; - t3 = $[3]; } - useEffect(t2, t3); + useEffect(t1, t2); - const t4 = String(state); - let t5; - if ($[4] !== t4) { - t5 =
{t4}
; + const t3 = String(state); + let t4; + if ($[3] !== t3) { + t4 =
{t3}
; + $[3] = t3; $[4] = t4; - $[5] = t5; } else { - t5 = $[5]; + t4 = $[4]; } - return t5; + return t4; +} +function _temp() { + someGlobal = true; } export const FIXTURE_ENTRYPOINT = { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-reassignment-to-global-function-jsx-prop.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-reassignment-to-global-function-jsx-prop.expect.md index eaa2834a49..60945024f2 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-reassignment-to-global-function-jsx-prop.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-reassignment-to-global-function-jsx-prop.expect.md @@ -27,13 +27,9 @@ export const FIXTURE_ENTRYPOINT = { import { c as _c } from "react/compiler-runtime"; function Component() { const $ = _c(1); + const onClick = _temp; let t0; if ($[0] === Symbol.for("react.memo_cache_sentinel")) { - const onClick = () => { - someUnknownGlobal = true; - moduleLocal = true; - }; - t0 =
; $[0] = t0; } else { @@ -41,6 +37,10 @@ function Component() { } return t0; } +function _temp() { + someUnknownGlobal = true; + moduleLocal = true; +} export const FIXTURE_ENTRYPOINT = { fn: Component, diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/array-join.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/array-join.expect.md index 2d06f084ff..53d7ccea7d 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/array-join.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/array-join.expect.md @@ -16,7 +16,7 @@ function Component(props) { ```javascript import { c as _c } from "react/compiler-runtime"; function Component(props) { - const $ = _c(8); + const $ = _c(7); let t0; let t1; if ($[0] === Symbol.for("react.memo_cache_sentinel")) { @@ -37,25 +37,21 @@ function Component(props) { t2 = $[3]; } const x = t2; - let t3; - if ($[4] === Symbol.for("react.memo_cache_sentinel")) { - t3 = () => "this closure gets stringified, not called"; - $[4] = t3; - } else { - t3 = $[4]; - } - const y = x.join(t3); + const y = x.join(_temp); foo(y); - let t4; - if ($[5] !== x || $[6] !== y) { - t4 = [x, y]; - $[5] = x; - $[6] = y; - $[7] = t4; + let t3; + if ($[4] !== x || $[5] !== y) { + t3 = [x, y]; + $[4] = x; + $[5] = y; + $[6] = t3; } else { - t4 = $[7]; + t3 = $[6]; } - return t4; + return t3; +} +function _temp() { + return "this closure gets stringified, not called"; } ``` diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/array-map-captures-receiver-noAlias.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/array-map-captures-receiver-noAlias.expect.md index e045437987..7b99a06518 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/array-map-captures-receiver-noAlias.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/array-map-captures-receiver-noAlias.expect.md @@ -44,7 +44,7 @@ function Component(props) { const items = t1; let t2; if ($[4] !== items) { - t2 = items.map((item_0) => item_0); + t2 = items.map(_temp); $[4] = items; $[5] = t2; } else { @@ -53,6 +53,9 @@ function Component(props) { const mapped = t2; return mapped; } +function _temp(item_0) { + return item_0; +} export const FIXTURE_ENTRYPOINT = { fn: Component, diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/array-map-frozen-array-noAlias.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/array-map-frozen-array-noAlias.expect.md index 375556bbea..2826a5c8db 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/array-map-frozen-array-noAlias.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/array-map-frozen-array-noAlias.expect.md @@ -33,7 +33,7 @@ function Component(props) { const x = t0; let t1; if ($[1] === Symbol.for("react.memo_cache_sentinel")) { - const y = x.map((item) => item); + const y = x.map(_temp); t1 = [x, y]; $[1] = t1; } else { @@ -41,6 +41,9 @@ function Component(props) { } return t1; } +function _temp(item) { + return item; +} export const FIXTURE_ENTRYPOINT = { fn: Component, diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/array-map-frozen-array.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/array-map-frozen-array.expect.md index 375556bbea..2826a5c8db 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/array-map-frozen-array.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/array-map-frozen-array.expect.md @@ -33,7 +33,7 @@ function Component(props) { const x = t0; let t1; if ($[1] === Symbol.for("react.memo_cache_sentinel")) { - const y = x.map((item) => item); + const y = x.map(_temp); t1 = [x, y]; $[1] = t1; } else { @@ -41,6 +41,9 @@ function Component(props) { } return t1; } +function _temp(item) { + return item; +} export const FIXTURE_ENTRYPOINT = { fn: Component, diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/array-map-mutable-array-mutating-lambda-noAlias.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/array-map-mutable-array-mutating-lambda-noAlias.expect.md index 96f3b935e1..c302e82eb1 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/array-map-mutable-array-mutating-lambda-noAlias.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/array-map-mutable-array-mutating-lambda-noAlias.expect.md @@ -28,10 +28,7 @@ function Component(props) { let t0; if ($[0] === Symbol.for("react.memo_cache_sentinel")) { const x = []; - const y = x.map((item) => { - item.updated = true; - return item; - }); + const y = x.map(_temp); t0 = [x, y]; $[0] = t0; } else { @@ -39,6 +36,10 @@ function Component(props) { } return t0; } +function _temp(item) { + item.updated = true; + return item; +} export const FIXTURE_ENTRYPOINT = { fn: Component, diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/array-map-mutable-array-mutating-lambda.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/array-map-mutable-array-mutating-lambda.expect.md index 96f3b935e1..c302e82eb1 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/array-map-mutable-array-mutating-lambda.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/array-map-mutable-array-mutating-lambda.expect.md @@ -28,10 +28,7 @@ function Component(props) { let t0; if ($[0] === Symbol.for("react.memo_cache_sentinel")) { const x = []; - const y = x.map((item) => { - item.updated = true; - return item; - }); + const y = x.map(_temp); t0 = [x, y]; $[0] = t0; } else { @@ -39,6 +36,10 @@ function Component(props) { } return t0; } +function _temp(item) { + item.updated = true; + return item; +} export const FIXTURE_ENTRYPOINT = { fn: Component, diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/array-map-mutable-array-non-mutating-lambda-mutated-result.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/array-map-mutable-array-non-mutating-lambda-mutated-result.expect.md index ee209592dc..bc83ac1770 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/array-map-mutable-array-non-mutating-lambda-mutated-result.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/array-map-mutable-array-non-mutating-lambda-mutated-result.expect.md @@ -28,7 +28,7 @@ function Component(props) { let t0; if ($[0] === Symbol.for("react.memo_cache_sentinel")) { const x = [{}]; - const y = x.map((item) => item); + const y = x.map(_temp); y[0].flag = true; t0 = [x, y]; $[0] = t0; @@ -37,6 +37,9 @@ function Component(props) { } return t0; } +function _temp(item) { + return item; +} export const FIXTURE_ENTRYPOINT = { fn: Component, diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/array-map-noAlias-escaping-function.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/array-map-noAlias-escaping-function.expect.md index b5a581ce14..4960b17e4b 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/array-map-noAlias-escaping-function.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/array-map-noAlias-escaping-function.expect.md @@ -21,33 +21,29 @@ export const FIXTURE_ENTRYPOINT = { ```javascript import { c as _c } from "react/compiler-runtime"; function Component(props) { - const $ = _c(5); + const $ = _c(4); + const f = _temp; let t0; - if ($[0] === Symbol.for("react.memo_cache_sentinel")) { - t0 = (item) => item; - $[0] = t0; + if ($[0] !== props.items) { + t0 = [...props.items].map(f); + $[0] = props.items; + $[1] = t0; } else { - t0 = $[0]; + t0 = $[1]; } - const f = t0; + const x = t0; let t1; - if ($[1] !== props.items) { - t1 = [...props.items].map(f); - $[1] = props.items; - $[2] = t1; + if ($[2] !== x) { + t1 = [x, f]; + $[2] = x; + $[3] = t1; } else { - t1 = $[2]; + t1 = $[3]; } - const x = t1; - let t2; - if ($[3] !== x) { - t2 = [x, f]; - $[3] = x; - $[4] = t2; - } else { - t2 = $[4]; - } - return t2; + return t1; +} +function _temp(item) { + return item; } export const FIXTURE_ENTRYPOINT = { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/arrow-function-one-line-directive.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/arrow-function-one-line-directive.expect.md index e752d771e4..f6fb5afa03 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/arrow-function-one-line-directive.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/arrow-function-one-line-directive.expect.md @@ -21,22 +21,14 @@ export const FIXTURE_ENTRYPOINT = { ## Code ```javascript -import { c as _c } from "react/compiler-runtime"; function useFoo() { - const $ = _c(1); - let t0; - if ($[0] === Symbol.for("react.memo_cache_sentinel")) { - t0 = () => { - "worklet"; - return 1; - }; - $[0] = t0; - } else { - t0 = $[0]; - } - const update = t0; + const update = _temp; return update; } +function _temp() { + "worklet"; + return 1; +} export const FIXTURE_ENTRYPOINT = { fn: useFoo, diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-function-shadow-captured.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-function-shadow-captured.expect.md index cfb776900b..68113a0d73 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-function-shadow-captured.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-function-shadow-captured.expect.md @@ -16,22 +16,14 @@ function component(a) { ## Code ```javascript -import { c as _c } from "react/compiler-runtime"; function component(a) { - const $ = _c(1); - let t0; - if ($[0] === Symbol.for("react.memo_cache_sentinel")) { - t0 = function () { - let z_0; - mutate(z_0); - }; - $[0] = t0; - } else { - t0 = $[0]; - } - const x = t0; + const x = _temp; return x; } +function _temp() { + let z_0; + mutate(z_0); +} ``` \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/change-detect-reassign.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/change-detect-reassign.expect.md index c1235a8668..50d11862fa 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/change-detect-reassign.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/change-detect-reassign.expect.md @@ -30,7 +30,6 @@ function Component(props) { if (!condition) { let old$x = $[1]; $structuralCheck(old$x, x, "x", "Component", "cached", "(3:6)"); - x = old$x; } $[0] = props.value; $[1] = x; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/change-detect-wrapper.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/change-detect-wrapper.expect.md index 433b12bafd..57d0ab46cf 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/change-detect-wrapper.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/change-detect-wrapper.expect.md @@ -30,7 +30,6 @@ function Component(props) { if (!condition) { let old$x = $[1]; $structuralCheck(old$x, x, "x", "Component", "cached", "(3:6)"); - x = $restore(old$x); } $[0] = props.value; $[1] = $store(x); diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/change-detect.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/change-detect.expect.md new file mode 100644 index 0000000000..07055afe6e --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/change-detect.expect.md @@ -0,0 +1,126 @@ + +## Input + +```javascript +// @enableChangeDetection +let glob = 1; + +function Component(props) { + const a = props.x; + const { b, ...c } = props.y; + const d = glob; + return ( +
+ {a} + {b} + {c} + {d} +
+ ); +} + +``` + +## Code + +```javascript +import { $structuralCheck } from "react-compiler-runtime"; +import { c as _c } from "react/compiler-runtime"; // @enableChangeDetection +let glob = 1; + +function Component(props) { + const $ = _c(11); + let t0; + { + t0 = props.x; + let condition = $[0] !== props.x; + if (!condition) { + let old$t0 = $[1]; + $structuralCheck(old$t0, t0, "t0", "Component", "cached", "(5:5)"); + } + $[0] = props.x; + $[1] = t0; + if (condition) { + t0 = props.x; + $structuralCheck($[1], t0, "t0", "Component", "recomputed", "(5:5)"); + t0 = $[1]; + } + } + const a = t0; + let b; + let c; + { + ({ b, ...c } = props.y); + let condition = $[2] !== props.y; + if (!condition) { + let old$b = $[3]; + let old$c = $[4]; + $structuralCheck(old$b, b, "b", "Component", "cached", "(6:6)"); + $structuralCheck(old$c, c, "c", "Component", "cached", "(6:6)"); + } + $[2] = props.y; + $[3] = b; + $[4] = c; + if (condition) { + ({ b, ...c } = props.y); + $structuralCheck($[3], b, "b", "Component", "recomputed", "(6:6)"); + b = $[3]; + $structuralCheck($[4], c, "c", "Component", "recomputed", "(6:6)"); + c = $[4]; + } + } + let t1; + { + t1 = glob; + let condition = $[5] === Symbol.for("react.memo_cache_sentinel"); + if (!condition) { + let old$t1 = $[5]; + $structuralCheck(old$t1, t1, "t1", "Component", "cached", "(13:13)"); + } + $[5] = t1; + if (condition) { + t1 = glob; + $structuralCheck($[5], t1, "t1", "Component", "recomputed", "(13:13)"); + t1 = $[5]; + } + } + let t2; + { + t2 = ( +
+ {a} + {b} + {c} + {t1} +
+ ); + let condition = $[6] !== a || $[7] !== b || $[8] !== c || $[9] !== t1; + if (!condition) { + let old$t2 = $[10]; + $structuralCheck(old$t2, t2, "t2", "Component", "cached", "(9:14)"); + } + $[6] = a; + $[7] = b; + $[8] = c; + $[9] = t1; + $[10] = t2; + if (condition) { + t2 = ( +
+ {a} + {b} + {c} + {t1} +
+ ); + $structuralCheck($[10], t2, "t2", "Component", "recomputed", "(9:14)"); + t2 = $[10]; + } + } + return t2; +} + +``` + +### Eval output +(kind: exception) Fixture not implemented \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/change-detect.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/change-detect.js new file mode 100644 index 0000000000..c6c50356a1 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/change-detect.js @@ -0,0 +1,16 @@ +// @enableChangeDetection +let glob = 1; + +function Component(props) { + const a = props.x; + const { b, ...c } = props.y; + const d = glob; + return ( +
+ {a} + {b} + {c} + {d} +
+ ); +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/component-inner-function-with-many-args.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/component-inner-function-with-many-args.expect.md index bfc1d31ffd..8b3c406c5c 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/component-inner-function-with-many-args.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/component-inner-function-with-many-args.expect.md @@ -22,24 +22,20 @@ export const FIXTURE_ENTRYPOINT = { import { c as _c } from "react/compiler-runtime"; import { Stringify } from "shared-runtime"; function Component(props) { - const $ = _c(3); + const $ = _c(2); + const cb = _temp; let t0; - if ($[0] === Symbol.for("react.memo_cache_sentinel")) { - t0 = (x, y, z) => x + y + z; - $[0] = t0; + if ($[0] !== props.id) { + t0 = ; + $[0] = props.id; + $[1] = t0; } else { - t0 = $[0]; + t0 = $[1]; } - const cb = t0; - let t1; - if ($[1] !== props.id) { - t1 = ; - $[1] = props.id; - $[2] = t1; - } else { - t1 = $[2]; - } - return t1; + return t0; +} +function _temp(x, y, z) { + return x + y + z; } export const FIXTURE_ENTRYPOINT = { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/computed-call-evaluation-order.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/computed-call-evaluation-order.expect.md index 76bdcf1efe..a0c23f26eb 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/computed-call-evaluation-order.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/computed-call-evaluation-order.expect.md @@ -30,30 +30,30 @@ export const FIXTURE_ENTRYPOINT = { ```javascript import { c as _c } from "react/compiler-runtime"; // Should print A, B, arg, original function Component() { - const $ = _c(2); - let t0; - if ($[0] === Symbol.for("react.memo_cache_sentinel")) { - t0 = (o) => { - o.f = () => console.log("new"); - }; - $[0] = t0; - } else { - t0 = $[0]; - } - const changeF = t0; + const $ = _c(1); + const changeF = _temp2; let x; - if ($[1] === Symbol.for("react.memo_cache_sentinel")) { - x = { f: () => console.log("original") }; + if ($[0] === Symbol.for("react.memo_cache_sentinel")) { + x = { f: _temp3 }; (console.log("A"), x)[(console.log("B"), "f")]( (changeF(x), console.log("arg"), 1), ); - $[1] = x; + $[0] = x; } else { - x = $[1]; + x = $[0]; } return x; } +function _temp3() { + return console.log("original"); +} +function _temp2(o) { + o.f = _temp; +} +function _temp() { + return console.log("new"); +} export const FIXTURE_ENTRYPOINT = { fn: Component, diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/constant-prop-colliding-identifier.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/constant-prop-colliding-identifier.expect.md index 7f42633ce5..b1ac15391c 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/constant-prop-colliding-identifier.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/constant-prop-colliding-identifier.expect.md @@ -27,11 +27,14 @@ export const FIXTURE_ENTRYPOINT = { import { invoke } from "shared-runtime"; function Component() { - const fn = () => ({ x: "value" }); + const fn = _temp; invoke(fn); return 3; } +function _temp() { + return { x: "value" }; +} export const FIXTURE_ENTRYPOINT = { fn: Component, diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/deeply-nested-function-expressions-with-params.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/deeply-nested-function-expressions-with-params.expect.md index a93d5b49d6..880c158b72 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/deeply-nested-function-expressions-with-params.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/deeply-nested-function-expressions-with-params.expect.md @@ -31,7 +31,7 @@ function Foo() { let t1; if ($[0] === Symbol.for("react.memo_cache_sentinel")) { t1 = function a(t2) { - const x_0 = t2 === undefined ? () => {} : t2; + const x_0 = t2 === undefined ? _temp : t2; return (function b(t3) { const y_0 = t3 === undefined ? [] : t3; return [x_0, y_0]; @@ -44,6 +44,7 @@ function Foo() { t0 = t1; return t0; } +function _temp() {} export const FIXTURE_ENTRYPOINT = { fn: Foo, diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/default-param-calls-global-function.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/default-param-calls-global-function.expect.md index 1bbfb1b7ec..fd6716f115 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/default-param-calls-global-function.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/default-param-calls-global-function.expect.md @@ -25,7 +25,7 @@ function Component(t0) { const $ = _c(2); let t1; if ($[0] !== t0) { - t1 = t0 === undefined ? identity([() => {}, true, 42, "hello"]) : t0; + t1 = t0 === undefined ? identity([_temp, true, 42, "hello"]) : t0; $[0] = t0; $[1] = t1; } else { @@ -34,6 +34,7 @@ function Component(t0) { const x = t1; return x; } +function _temp() {} export const FIXTURE_ENTRYPOINT = { fn: Component, diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/default-param-with-empty-callback.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/default-param-with-empty-callback.expect.md index cc96a95553..43249cfc1e 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/default-param-with-empty-callback.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/default-param-with-empty-callback.expect.md @@ -16,20 +16,11 @@ export const FIXTURE_ENTRYPOINT = { ## Code ```javascript -import { c as _c } from "react/compiler-runtime"; function Component(t0) { - const $ = _c(2); - let t1; - if ($[0] !== t0) { - t1 = t0 === undefined ? () => {} : t0; - $[0] = t0; - $[1] = t1; - } else { - t1 = $[1]; - } - const x = t1; + const x = t0 === undefined ? _temp : t0; return x; } +function _temp() {} export const FIXTURE_ENTRYPOINT = { fn: Component, diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/default-param-with-reorderable-callback.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/default-param-with-reorderable-callback.expect.md index 624ed6f67a..73268e85d8 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/default-param-with-reorderable-callback.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/default-param-with-reorderable-callback.expect.md @@ -16,20 +16,13 @@ export const FIXTURE_ENTRYPOINT = { ## Code ```javascript -import { c as _c } from "react/compiler-runtime"; function Component(t0) { - const $ = _c(2); - let t1; - if ($[0] !== t0) { - t1 = t0 === undefined ? () => [-1, true, 42, "hello"] : t0; - $[0] = t0; - $[1] = t1; - } else { - t1 = $[1]; - } - const x = t1; + const x = t0 === undefined ? _temp : t0; return x; } +function _temp() { + return [-1, true, 42, "hello"]; +} export const FIXTURE_ENTRYPOINT = { fn: Component, diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/fast-refresh-dont-refresh-const-changes-prod.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/fast-refresh-dont-refresh-const-changes-prod.expect.md index 87084a1402..3dad23aba1 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/fast-refresh-dont-refresh-const-changes-prod.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/fast-refresh-dont-refresh-const-changes-prod.expect.md @@ -58,37 +58,31 @@ function unsafeUpdateConst() { } function Component() { - const $ = _c(3); - let t0; - if ($[0] === Symbol.for("react.memo_cache_sentinel")) { - t0 = () => { - unsafeResetConst(); - }; - $[0] = t0; - } else { - t0 = $[0]; - } - useState(t0); + const $ = _c(2); + useState(_temp); unsafeUpdateConst(); + let t0; let t1; + if ($[0] === Symbol.for("react.memo_cache_sentinel")) { + t1 = [{ pretendConst }]; + $[0] = t1; + } else { + t1 = $[0]; + } + t0 = t1; + const value = t0; let t2; if ($[1] === Symbol.for("react.memo_cache_sentinel")) { - t2 = [{ pretendConst }]; + t2 = ; $[1] = t2; } else { t2 = $[1]; } - t1 = t2; - const value = t1; - let t3; - if ($[2] === Symbol.for("react.memo_cache_sentinel")) { - t3 = ; - $[2] = t3; - } else { - t3 = $[2]; - } - return t3; + return t2; +} +function _temp() { + unsafeResetConst(); } export const FIXTURE_ENTRYPOINT = { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/fast-refresh-refresh-on-const-changes-dev.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/fast-refresh-refresh-on-const-changes-dev.expect.md index 19317de47e..556fdaab38 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/fast-refresh-refresh-on-const-changes-dev.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/fast-refresh-refresh-on-const-changes-dev.expect.md @@ -61,45 +61,39 @@ function unsafeUpdateConst() { } function Component() { - const $ = _c(4); + const $ = _c(3); if ( $[0] !== "4bf230b116dd95f382060ad17350e116395e41ed757e51fd074ea0b4ed281272" ) { - for (let $i = 0; $i < 4; $i += 1) { + for (let $i = 0; $i < 3; $i += 1) { $[$i] = Symbol.for("react.memo_cache_sentinel"); } $[0] = "4bf230b116dd95f382060ad17350e116395e41ed757e51fd074ea0b4ed281272"; } - let t0; - if ($[1] === Symbol.for("react.memo_cache_sentinel")) { - t0 = () => { - unsafeResetConst(); - }; - $[1] = t0; - } else { - t0 = $[1]; - } - useState(t0); + useState(_temp); unsafeUpdateConst(); + let t0; let t1; + if ($[1] === Symbol.for("react.memo_cache_sentinel")) { + t1 = [{ pretendConst }]; + $[1] = t1; + } else { + t1 = $[1]; + } + t0 = t1; + const value = t0; let t2; if ($[2] === Symbol.for("react.memo_cache_sentinel")) { - t2 = [{ pretendConst }]; + t2 = ; $[2] = t2; } else { t2 = $[2]; } - t1 = t2; - const value = t1; - let t3; - if ($[3] === Symbol.for("react.memo_cache_sentinel")) { - t3 = ; - $[3] = t3; - } else { - t3 = $[3]; - } - return t3; + return t2; +} +function _temp() { + unsafeResetConst(); } export const FIXTURE_ENTRYPOINT = { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/fbt/lambda-with-fbt.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/fbt/lambda-with-fbt.expect.md index 5ac0ad17a0..473849be4e 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/fbt/lambda-with-fbt.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/fbt/lambda-with-fbt.expect.md @@ -42,47 +42,41 @@ import { c as _c } from "react/compiler-runtime"; import { fbt } from "fbt"; function Component() { - const $ = _c(2); + const $ = _c(1); + const buttonLabel = _temp; let t0; if ($[0] === Symbol.for("react.memo_cache_sentinel")) { - t0 = () => { - if (!someCondition) { - return fbt._("Purchase as a gift", null, { hk: "1gHj4g" }); - } else { - if ( - !iconOnly && - showPrice && - item?.current_gift_offer?.price?.formatted != null - ) { - return fbt._( - "Gift | {price}", - [fbt._param("price", item?.current_gift_offer?.price?.formatted)], - { hk: "3GTnGE" }, - ); - } else { - if (!iconOnly && !showPrice) { - return fbt._("Gift", null, { hk: "3fqfrk" }); - } - } - } - }; - $[0] = t0; - } else { - t0 = $[0]; - } - const buttonLabel = t0; - let t1; - if ($[1] === Symbol.for("react.memo_cache_sentinel")) { - t1 = ( + t0 = ( ; + $[1] = deeplinkItemId; + $[2] = t1; } else { - t1 = $[1]; + t1 = $[2]; } - let t2; - if ($[2] !== deeplinkItemId) { - t2 = ; - $[2] = deeplinkItemId; - $[3] = t2; - } else { - t2 = $[3]; - } - return t2; + return t1; } +function _temp() {} export const FIXTURE_ENTRYPOINT = { fn: Component, diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-memoize-array-with-immutable-map-after-hook.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-memoize-array-with-immutable-map-after-hook.expect.md index 33f1330d7e..2b96328236 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-memoize-array-with-immutable-map-after-hook.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-memoize-array-with-immutable-map-after-hook.expect.md @@ -34,7 +34,7 @@ import { c as _c } from "react/compiler-runtime"; import { useEffect, useState } from "react"; function Component(props) { - const $ = _c(11); + const $ = _c(10); let t0; if ($[0] !== props.value) { t0 = [props.value]; @@ -45,47 +45,47 @@ function Component(props) { } const x = t0; let t1; - let t2; if ($[2] === Symbol.for("react.memo_cache_sentinel")) { - t1 = () => {}; - t2 = []; + t1 = []; $[2] = t1; - $[3] = t2; } else { t1 = $[2]; - t2 = $[3]; } - useEffect(t1, t2); - let t3; - if ($[4] !== x.length) { - t3 = () => { + useEffect(_temp, t1); + let t2; + if ($[3] !== x.length) { + t2 = () => { console.log(x.length); }; - $[4] = x.length; - $[5] = t3; + $[3] = x.length; + $[4] = t2; } else { - t3 = $[5]; + t2 = $[4]; + } + const onClick = t2; + let t3; + if ($[5] !== x) { + t3 = x.map(_temp2); + $[5] = x; + $[6] = t3; + } else { + t3 = $[6]; } - const onClick = t3; let t4; - if ($[6] !== x) { - t4 = x.map((item) => {item}); - $[6] = x; - $[7] = t4; - } else { - t4 = $[7]; - } - let t5; - if ($[8] !== onClick || $[9] !== t4) { - t5 =
{t4}
; - $[8] = onClick; + if ($[7] !== onClick || $[8] !== t3) { + t4 =
{t3}
; + $[7] = onClick; + $[8] = t3; $[9] = t4; - $[10] = t5; } else { - t5 = $[10]; + t4 = $[9]; } - return t5; + return t4; } +function _temp2(item) { + return {item}; +} +function _temp() {} export const FIXTURE_ENTRYPOINT = { fn: Component, diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-mutable-range-extending-into-ternary.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-mutable-range-extending-into-ternary.expect.md index 4b9822fdb8..72ba9ab675 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-mutable-range-extending-into-ternary.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-mutable-range-extending-into-ternary.expect.md @@ -57,15 +57,10 @@ import { useState } from "react"; function Component(props) { const items = props.items ? props.items.slice() : []; const [state] = useState(""); - return props.cond ? ( -
{state}
- ) : ( -
- {items.map((item) => ( -
{item.name}
- ))} -
- ); + return props.cond ?
{state}
:
{items.map(_temp)}
; +} +function _temp(item) { + return
{item.name}
; } export const FIXTURE_ENTRYPOINT = { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-no-declarations-in-reactive-scope-with-early-return.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-no-declarations-in-reactive-scope-with-early-return.expect.md index ffd18500f1..0c1bf1cd70 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-no-declarations-in-reactive-scope-with-early-return.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-no-declarations-in-reactive-scope-with-early-return.expect.md @@ -39,7 +39,7 @@ function Component() { ```javascript import { c as _c } from "react/compiler-runtime"; // @enableAssumeHooksFollowRulesOfReact @enableTransitivelyFreezeFunctionExpressions function Component() { - const $ = _c(9); + const $ = _c(8); const items = useItems(); let t0; let t1; @@ -74,17 +74,8 @@ function Component() { t2 = t4; break bb0; } - let t4; - if ($[6] === Symbol.for("react.memo_cache_sentinel")) { - t4 = (t5) => { - const [item_0] = t5; - return ; - }; - $[6] = t4; - } else { - t4 = $[6]; - } - t1 = filteredItems.map(t4); + + t1 = filteredItems.map(_temp); } $[0] = items; $[1] = t1; @@ -99,15 +90,19 @@ function Component() { return t2; } let t3; - if ($[7] !== t1) { + if ($[6] !== t1) { t3 = <>{t1}; - $[7] = t1; - $[8] = t3; + $[6] = t1; + $[7] = t3; } else { - t3 = $[8]; + t3 = $[7]; } return t3; } +function _temp(t0) { + const [item_0] = t0; + return ; +} ``` diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-undefined-expression-of-jsxexpressioncontainer.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-undefined-expression-of-jsxexpressioncontainer.expect.md index aa5a55dfec..f215141be5 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-undefined-expression-of-jsxexpressioncontainer.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-undefined-expression-of-jsxexpressioncontainer.expect.md @@ -48,7 +48,7 @@ import { c as _c } from "react/compiler-runtime"; import { StaticText1, Stringify, Text } from "shared-runtime"; function Component(props) { - const $ = _c(7); + const $ = _c(6); const { buttons } = props; let nonPrimaryButtons; if ($[0] !== buttons) { @@ -61,24 +61,7 @@ function Component(props) { } let t0; if ($[2] !== nonPrimaryButtons) { - let t1; - if ($[4] === Symbol.for("react.memo_cache_sentinel")) { - t1 = (buttonProps, i) => ( - - ); - $[4] = t1; - } else { - t1 = $[4]; - } - t0 = nonPrimaryButtons.map(t1); + t0 = nonPrimaryButtons.map(_temp); $[2] = nonPrimaryButtons; $[3] = t0; } else { @@ -86,15 +69,26 @@ function Component(props) { } const renderedNonPrimaryButtons = t0; let t1; - if ($[5] !== renderedNonPrimaryButtons) { + if ($[4] !== renderedNonPrimaryButtons) { t1 = {renderedNonPrimaryButtons}; - $[5] = renderedNonPrimaryButtons; - $[6] = t1; + $[4] = renderedNonPrimaryButtons; + $[5] = t1; } else { - t1 = $[6]; + t1 = $[5]; } return t1; } +function _temp(buttonProps, i) { + return ( + + ); +} const styles = { leftSecondaryButton: { left: true }, diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/rules-of-hooks/rules-of-hooks-93dc5d5e538a.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/rules-of-hooks/rules-of-hooks-93dc5d5e538a.expect.md index 753c8156db..626091be1e 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/rules-of-hooks/rules-of-hooks-93dc5d5e538a.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/rules-of-hooks/rules-of-hooks-93dc5d5e538a.expect.md @@ -17,22 +17,16 @@ function RegressionTest() { ## Code ```javascript -import { c as _c } from "react/compiler-runtime"; // Valid because the loop doesn't change the order of hooks calls. +// Valid because the loop doesn't change the order of hooks calls. function RegressionTest() { - const $ = _c(1); const res = []; for (let i = 0; i !== 10 && true; ++i) { res.push(i); } - let t0; - if ($[0] === Symbol.for("react.memo_cache_sentinel")) { - t0 = () => {}; - $[0] = t0; - } else { - t0 = $[0]; - } - React.useLayoutEffect(t0); + + React.useLayoutEffect(_temp); } +function _temp() {} ``` \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/simple-function-1.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/simple-function-1.expect.md index 508d0f66ba..586b610399 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/simple-function-1.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/simple-function-1.expect.md @@ -20,21 +20,13 @@ export const FIXTURE_ENTRYPOINT = { ## Code ```javascript -import { c as _c } from "react/compiler-runtime"; function component() { - const $ = _c(1); - let t0; - if ($[0] === Symbol.for("react.memo_cache_sentinel")) { - t0 = function (a) { - a.foo(); - }; - $[0] = t0; - } else { - t0 = $[0]; - } - const x = t0; + const x = _temp; return x; } +function _temp(a) { + a.foo(); +} export const FIXTURE_ENTRYPOINT = { fn: component, diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/todo.unnecessary-lambda-memoization.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/todo.unnecessary-lambda-memoization.expect.md index e5ece01a5e..a5de1b45fa 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/todo.unnecessary-lambda-memoization.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/todo.unnecessary-lambda-memoization.expect.md @@ -24,18 +24,11 @@ function Component(props) { ```javascript import { c as _c } from "react/compiler-runtime"; function Component(props) { - const $ = _c(5); + const $ = _c(4); const data = useFreeze(); let t0; if ($[0] !== data.items) { - let t1; - if ($[2] === Symbol.for("react.memo_cache_sentinel")) { - t1 = (item) => ; - $[2] = t1; - } else { - t1 = $[2]; - } - t0 = data.items.map(t1); + t0 = data.items.map(_temp); $[0] = data.items; $[1] = t0; } else { @@ -43,15 +36,18 @@ function Component(props) { } const items = t0; let t1; - if ($[3] !== items) { + if ($[2] !== items) { t1 =
{items}
; - $[3] = items; - $[4] = t1; + $[2] = items; + $[3] = t1; } else { - t1 = $[4]; + t1 = $[3]; } return t1; } +function _temp(item) { + return ; +} ``` \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transitive-freeze-function-expressions.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transitive-freeze-function-expressions.expect.md index ff1a2e16e7..ca472cc6b6 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transitive-freeze-function-expressions.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transitive-freeze-function-expressions.expect.md @@ -33,7 +33,7 @@ function Component(props) { ```javascript import { c as _c } from "react/compiler-runtime"; // @enableTransitivelyFreezeFunctionExpressions function Component(props) { - const $ = _c(10); + const $ = _c(9); const { data, loadNext, isLoadingNext } = usePaginationFragment(props.key).items ?? []; let t0; @@ -74,14 +74,7 @@ function Component(props) { useEffect(t1, t2); let t3; if ($[7] !== data) { - let t4; - if ($[9] === Symbol.for("react.memo_cache_sentinel")) { - t4 = (x) => x; - $[9] = t4; - } else { - t4 = $[9]; - } - t3 = data.map(t4); + t3 = data.map(_temp); $[7] = data; $[8] = t3; } else { @@ -90,6 +83,9 @@ function Component(props) { const items = t3; return items; } +function _temp(x) { + return x; +} ``` \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/try-catch-within-function-expression.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/try-catch-within-function-expression.expect.md index 4e9dbd5fed..5f55ec6142 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/try-catch-within-function-expression.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/try-catch-within-function-expression.expect.md @@ -26,16 +26,9 @@ export const FIXTURE_ENTRYPOINT = { import { c as _c } from "react/compiler-runtime"; function Component(props) { const $ = _c(1); + const callback = _temp; let t0; if ($[0] === Symbol.for("react.memo_cache_sentinel")) { - const callback = () => { - try { - return []; - } catch (t1) { - return; - } - }; - t0 = callback(); $[0] = t0; } else { @@ -43,6 +36,13 @@ function Component(props) { } return t0; } +function _temp() { + try { + return []; + } catch (t0) { + return; + } +} export const FIXTURE_ENTRYPOINT = { fn: Component, diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/type-alias-used-as-annotation.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/type-alias-used-as-annotation.expect.md index 02a31ec938..c5bafae2b7 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/type-alias-used-as-annotation.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/type-alias-used-as-annotation.expect.md @@ -25,12 +25,13 @@ export const FIXTURE_ENTRYPOINT = { // @enableAssumeHooksFollowRulesOfReact @enableTransitivelyFreezeFunctionExpressions type Bar = string; function TypeAliasUsedAsParamAnnotation() { - const fun = (f) => { - console.log(f); - }; + const fun = _temp; fun("hello, world"); } +function _temp(f) { + console.log(f); +} export const FIXTURE_ENTRYPOINT = { fn: TypeAliasUsedAsParamAnnotation, diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/type-alias-used-as-annotation_.flow.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/type-alias-used-as-annotation_.flow.expect.md index 92c522c450..52a2217bea 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/type-alias-used-as-annotation_.flow.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/type-alias-used-as-annotation_.flow.expect.md @@ -23,12 +23,13 @@ export const FIXTURE_ENTRYPOINT = { ```javascript type Bar = string; function TypeAliasUsedAsAnnotation() { - const fun = (f) => { - console.log(f); - }; + const fun = _temp; fun("hello, world"); } +function _temp(f) { + console.log(f); +} export const FIXTURE_ENTRYPOINT = { fn: TypeAliasUsedAsAnnotation, diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/type-alias-used-as-variable-annotation.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/type-alias-used-as-variable-annotation.expect.md index f97bcbb3a6..a8b58d4030 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/type-alias-used-as-variable-annotation.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/type-alias-used-as-variable-annotation.expect.md @@ -26,13 +26,14 @@ export const FIXTURE_ENTRYPOINT = { // @enableAssumeHooksFollowRulesOfReact @enableTransitivelyFreezeFunctionExpressions type Bar = string; function TypeAliasUsedAsVariableAnnotation() { - const fun = (f) => { - const g = f; - console.log(g); - }; + const fun = _temp; fun("hello, world"); } +function _temp(f) { + const g = f; + console.log(g); +} export const FIXTURE_ENTRYPOINT = { fn: TypeAliasUsedAsVariableAnnotation, diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/type-alias-used-as-variable-annotation_.flow.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/type-alias-used-as-variable-annotation_.flow.expect.md index 9362ad8655..3b5af06a5d 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/type-alias-used-as-variable-annotation_.flow.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/type-alias-used-as-variable-annotation_.flow.expect.md @@ -25,13 +25,14 @@ export const FIXTURE_ENTRYPOINT = { ```javascript type Bar = string; function TypeAliasUsedAsAnnotation() { - const fun = (f) => { - const g = f; - console.log(g); - }; + const fun = _temp; fun("hello, world"); } +function _temp(f) { + const g = f; + console.log(g); +} export const FIXTURE_ENTRYPOINT = { fn: TypeAliasUsedAsAnnotation, diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/type-annotations/todo_type-annotations-props.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/type-annotations/todo_type-annotations-props.expect.md index 709f75ddb2..ee502bc342 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/type-annotations/todo_type-annotations-props.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/type-annotations/todo_type-annotations-props.expect.md @@ -22,17 +22,10 @@ export const FIXTURE_ENTRYPOINT = { ```javascript import { c as _c } from "react/compiler-runtime"; // @enableUseTypeAnnotations function useArray(items) { - const $ = _c(3); + const $ = _c(2); let t0; if ($[0] !== items) { - let t1; - if ($[2] === Symbol.for("react.memo_cache_sentinel")) { - t1 = (x) => x !== 0; - $[2] = t1; - } else { - t1 = $[2]; - } - t0 = items.filter(t1); + t0 = items.filter(_temp); $[0] = items; $[1] = t0; } else { @@ -40,6 +33,9 @@ function useArray(items) { } return t0; } +function _temp(x) { + return x !== 0; +} export const FIXTURE_ENTRYPOINT = { fn: useArray, diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useEffect-external-mutate.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useEffect-external-mutate.expect.md index 4481197c91..6deb76d188 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useEffect-external-mutate.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useEffect-external-mutate.expect.md @@ -22,23 +22,15 @@ export const FIXTURE_ENTRYPOINT = { ## Code ```javascript -import { c as _c } from "react/compiler-runtime"; import { useEffect } from "react"; let x = { a: 42 }; function Component(props) { - const $ = _c(1); - let t0; - if ($[0] === Symbol.for("react.memo_cache_sentinel")) { - t0 = () => { - x.a = 10; - }; - $[0] = t0; - } else { - t0 = $[0]; - } - useEffect(t0); + useEffect(_temp); +} +function _temp() { + x.a = 10; } export const FIXTURE_ENTRYPOINT = { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useEffect-global-pruned.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useEffect-global-pruned.expect.md index c598ff8367..c81175fde9 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useEffect-global-pruned.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useEffect-global-pruned.expect.md @@ -36,35 +36,29 @@ import { useEffect } from "react"; function someGlobal() {} function useFoo() { - const $ = _c(3); + const $ = _c(2); let t0; - let t1; - if ($[0] === Symbol.for("react.memo_cache_sentinel")) { - t1 = function () { - someGlobal(); - }; - $[0] = t1; - } else { - t1 = $[0]; - } - t0 = t1; + t0 = _temp; const fn = t0; + let t1; let t2; - let t3; - if ($[1] === Symbol.for("react.memo_cache_sentinel")) { - t2 = () => { + if ($[0] === Symbol.for("react.memo_cache_sentinel")) { + t1 = () => { fn(); }; - t3 = [fn]; + t2 = [fn]; + $[0] = t1; $[1] = t2; - $[2] = t3; } else { + t1 = $[0]; t2 = $[1]; - t3 = $[2]; } - useEffect(t2, t3); + useEffect(t1, t2); return null; } +function _temp() { + someGlobal(); +} export const FIXTURE_ENTRYPOINT = { fn: useFoo, diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useEffect-method-call.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useEffect-method-call.expect.md index c275453aff..638ea42aa5 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useEffect-method-call.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useEffect-method-call.expect.md @@ -19,20 +19,12 @@ export const FIXTURE_ENTRYPOINT = { ## Code ```javascript -import { c as _c } from "react/compiler-runtime"; let x = {}; function Component() { - const $ = _c(1); - let t0; - if ($[0] === Symbol.for("react.memo_cache_sentinel")) { - t0 = () => { - x.foo = 1; - }; - $[0] = t0; - } else { - t0 = $[0]; - } - React.useEffect(t0); + React.useEffect(_temp); +} +function _temp() { + x.foo = 1; } export const FIXTURE_ENTRYPOINT = { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useEffect-namespace-pruned.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useEffect-namespace-pruned.expect.md index 0c6f4d582f..8af9ad082e 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useEffect-namespace-pruned.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useEffect-namespace-pruned.expect.md @@ -36,35 +36,29 @@ import * as React from "react"; function someGlobal() {} function useFoo() { - const $ = _c(3); + const $ = _c(2); let t0; - let t1; - if ($[0] === Symbol.for("react.memo_cache_sentinel")) { - t1 = function () { - someGlobal(); - }; - $[0] = t1; - } else { - t1 = $[0]; - } - t0 = t1; + t0 = _temp; const fn = t0; + let t1; let t2; - let t3; - if ($[1] === Symbol.for("react.memo_cache_sentinel")) { - t2 = () => { + if ($[0] === Symbol.for("react.memo_cache_sentinel")) { + t1 = () => { fn(); }; - t3 = [fn]; + t2 = [fn]; + $[0] = t1; $[1] = t2; - $[2] = t3; } else { + t1 = $[0]; t2 = $[1]; - t3 = $[2]; } - React.useEffect(t2, t3); + React.useEffect(t1, t2); return null; } +function _temp() { + someGlobal(); +} export const FIXTURE_ENTRYPOINT = { fn: useFoo, diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-preserve-non-idempotent.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-preserve-non-idempotent.expect.md deleted file mode 100644 index deb36c578b..0000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-preserve-non-idempotent.expect.md +++ /dev/null @@ -1,77 +0,0 @@ - -## Input - -```javascript -// @enablePreserveExistingManualUseMemo -import { useMemo } from "react"; -let cur = 99; -function random(id) { - "use no forget"; - cur = cur + 1; - return cur; -} - -export default function C(id) { - const r = useMemo(() => random(id.id), [id.id]); - const a = r + 1; - return <>{a}; -} - -export const FIXTURE_ENTRYPOINT = { - fn: C, - params: [{ id: 1 }], - sequentialRenders: [{ id: 1 }, { id: 1 }, { id: 1 }, { id: 1 }], -}; - -``` - -## Code - -```javascript -import { c as _c } from "react/compiler-runtime"; // @enablePreserveExistingManualUseMemo -import { useMemo } from "react"; -let cur = 99; -function random(id) { - "use no forget"; - cur = cur + 1; - return cur; -} - -export default function C(id) { - const $ = _c(4); - let t0; - let t1; - if ($[0] !== id.id) { - t1 = random(id.id); - $[0] = id.id; - $[1] = t1; - } else { - t1 = $[1]; - } - t0 = t1; - const r = t0; - const a = r + 1; - let t2; - if ($[2] !== a) { - t2 = <>{a}; - $[2] = a; - $[3] = t2; - } else { - t2 = $[3]; - } - return t2; -} - -export const FIXTURE_ENTRYPOINT = { - fn: C, - params: [{ id: 1 }], - sequentialRenders: [{ id: 1 }, { id: 1 }, { id: 1 }, { id: 1 }], -}; - -``` - -### Eval output -(kind: ok) 101 -101 -101 -101 \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-preserve-non-idempotent.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-preserve-non-idempotent.js deleted file mode 100644 index 63a5be8eee..0000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-preserve-non-idempotent.js +++ /dev/null @@ -1,20 +0,0 @@ -// @enablePreserveExistingManualUseMemo -import { useMemo } from "react"; -let cur = 99; -function random(id) { - "use no forget"; - cur = cur + 1; - return cur; -} - -export default function C(id) { - const r = useMemo(() => random(id.id), [id.id]); - const a = r + 1; - return <>{a}; -} - -export const FIXTURE_ENTRYPOINT = { - fn: C, - params: [{ id: 1 }], - sequentialRenders: [{ id: 1 }, { id: 1 }, { id: 1 }, { id: 1 }], -}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-simple-preserved-nomemo.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-simple-preserved-nomemo.expect.md index d1c7afdfd0..182d792e9e 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-simple-preserved-nomemo.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-simple-preserved-nomemo.expect.md @@ -25,27 +25,18 @@ import { c as _c } from "react/compiler-runtime"; // @disableMemoizationForDebug import { useMemo } from "react"; function Component(t0) { - const $ = _c(3); + const $ = _c(2); const { a } = t0; + const x = useMemo(() => [a], []); let t1; - let t2; - if ($[0] === Symbol.for("react.memo_cache_sentinel")) { - t2 = [a]; - $[0] = t2; + if ($[0] !== x || true) { + t1 =
{x}
; + $[0] = x; + $[1] = t1; } else { - t2 = $[0]; + t1 = $[1]; } - t1 = t2; - const x = t1; - let t3; - if ($[1] !== x || true) { - t3 =
{x}
; - $[1] = x; - $[2] = t3; - } else { - t3 = $[2]; - } - return t3; + return t1; } export const FIXTURE_ENTRYPOINT = { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-simple-preserved.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-simple-preserved.expect.md index c72e3a140d..3fbd742464 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-simple-preserved.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-simple-preserved.expect.md @@ -25,27 +25,18 @@ import { c as _c } from "react/compiler-runtime"; // @enablePreserveExistingManu import { useMemo } from "react"; function Component(t0) { - const $ = _c(3); + const $ = _c(2); const { a } = t0; + const x = useMemo(() => [a], []); let t1; - let t2; - if ($[0] === Symbol.for("react.memo_cache_sentinel")) { - t2 = [a]; - $[0] = t2; + if ($[0] !== x) { + t1 =
{x}
; + $[0] = x; + $[1] = t1; } else { - t2 = $[0]; + t1 = $[1]; } - t1 = t2; - const x = t1; - let t3; - if ($[1] !== x) { - t3 =
{x}
; - $[1] = x; - $[2] = t3; - } else { - t3 = $[2]; - } - return t3; + return t1; } export const FIXTURE_ENTRYPOINT = { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useState-and-other-hook-unpruned-dependency.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useState-and-other-hook-unpruned-dependency.expect.md index 0323d4ffff..badc550e29 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useState-and-other-hook-unpruned-dependency.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useState-and-other-hook-unpruned-dependency.expect.md @@ -39,79 +39,43 @@ function useOther(x) { } function Component(props) { - const $ = _c(8); + const $ = _c(4); let t0; { - t0 = props.x; + t0 = f(props.x); let condition = $[0] !== props.x; if (!condition) { let old$t0 = $[1]; $structuralCheck(old$t0, t0, "t0", "Component", "cached", "(8:8)"); - t0 = old$t0; } $[0] = props.x; $[1] = t0; if (condition) { - t0 = props.x; + t0 = f(props.x); $structuralCheck($[1], t0, "t0", "Component", "recomputed", "(8:8)"); t0 = $[1]; } } + const w = t0; + const z = useOther(w); + const [x] = useState(z); let t1; { - t1 = f(t0); - let condition = $[2] !== t0; + t1 =
{x}
; + let condition = $[2] !== x; if (!condition) { let old$t1 = $[3]; - $structuralCheck(old$t1, t1, "t1", "Component", "cached", "(8:8)"); - t1 = old$t1; + $structuralCheck(old$t1, t1, "t1", "Component", "cached", "(11:11)"); } - $[2] = t0; + $[2] = x; $[3] = t1; if (condition) { - t1 = f(t0); - $structuralCheck($[3], t1, "t1", "Component", "recomputed", "(8:8)"); + t1 =
{x}
; + $structuralCheck($[3], t1, "t1", "Component", "recomputed", "(11:11)"); t1 = $[3]; } } - const w = t1; - const z = useOther(w); - const t2 = useState(z); - let x; - { - [x] = t2; - let condition = $[4] !== t2; - if (!condition) { - let old$x = $[5]; - $structuralCheck(old$x, x, "x", "Component", "cached", "(10:10)"); - x = old$x; - } - $[4] = t2; - $[5] = x; - if (condition) { - [x] = t2; - $structuralCheck($[5], x, "x", "Component", "recomputed", "(10:10)"); - x = $[5]; - } - } - let t3; - { - t3 =
{x}
; - let condition = $[6] !== x; - if (!condition) { - let old$t3 = $[7]; - $structuralCheck(old$t3, t3, "t3", "Component", "cached", "(11:11)"); - t3 = old$t3; - } - $[6] = x; - $[7] = t3; - if (condition) { - t3 =
{x}
; - $structuralCheck($[7], t3, "t3", "Component", "recomputed", "(11:11)"); - t3 = $[7]; - } - } - return t3; + return t1; } function f(x) { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useState-pruned-dependency-change-detect.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useState-pruned-dependency-change-detect.expect.md index da07a56e96..8299bc7751 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useState-pruned-dependency-change-detect.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useState-pruned-dependency-change-detect.expect.md @@ -20,57 +20,25 @@ import { c as _c } from "react/compiler-runtime"; // @enableChangeDetection import { useState } from "react"; function Component(props) { - const $ = _c(6); + const $ = _c(2); + const [x] = useState(f(props.x)); let t0; - if ($[0] === Symbol.for("react.memo_cache_sentinel")) { - t0 = props.x; - $[0] = t0; - } else { - t0 = $[0]; - } - let t1; - if ($[1] === Symbol.for("react.memo_cache_sentinel")) { - t1 = f(t0); - $[1] = t1; - } else { - t1 = $[1]; - } - const t2 = useState(t1); - let x; { - [x] = t2; - let condition = $[2] !== t2; + t0 =
{x}
; + let condition = $[0] !== x; if (!condition) { - let old$x = $[3]; - $structuralCheck(old$x, x, "x", "Component", "cached", "(5:5)"); - x = old$x; + let old$t0 = $[1]; + $structuralCheck(old$t0, t0, "t0", "Component", "cached", "(6:6)"); } - $[2] = t2; - $[3] = x; + $[0] = x; + $[1] = t0; if (condition) { - [x] = t2; - $structuralCheck($[3], x, "x", "Component", "recomputed", "(5:5)"); - x = $[3]; + t0 =
{x}
; + $structuralCheck($[1], t0, "t0", "Component", "recomputed", "(6:6)"); + t0 = $[1]; } } - let t3; - { - t3 =
{x}
; - let condition = $[4] !== x; - if (!condition) { - let old$t3 = $[5]; - $structuralCheck(old$t3, t3, "t3", "Component", "cached", "(6:6)"); - t3 = old$t3; - } - $[4] = x; - $[5] = t3; - if (condition) { - t3 =
{x}
; - $structuralCheck($[5], t3, "t3", "Component", "recomputed", "(6:6)"); - t3 = $[5]; - } - } - return t3; + return t0; } ``` diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useState-unpruned-dependency.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useState-unpruned-dependency.expect.md index dec1cd615d..d303641180 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useState-unpruned-dependency.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useState-unpruned-dependency.expect.md @@ -35,89 +35,53 @@ import { c as _c } from "react/compiler-runtime"; import { useState } from "react"; // @enableChangeDetection function Component(props) { - const $ = _c(9); + const $ = _c(5); let t0; { - t0 = props.x; + t0 = f(props.x); let condition = $[0] !== props.x; if (!condition) { let old$t0 = $[1]; $structuralCheck(old$t0, t0, "t0", "Component", "cached", "(4:4)"); - t0 = old$t0; } $[0] = props.x; $[1] = t0; if (condition) { - t0 = props.x; + t0 = f(props.x); $structuralCheck($[1], t0, "t0", "Component", "recomputed", "(4:4)"); t0 = $[1]; } } + const w = t0; + const [x] = useState(w); let t1; { - t1 = f(t0); - let condition = $[2] !== t0; - if (!condition) { - let old$t1 = $[3]; - $structuralCheck(old$t1, t1, "t1", "Component", "cached", "(4:4)"); - t1 = old$t1; - } - $[2] = t0; - $[3] = t1; - if (condition) { - t1 = f(t0); - $structuralCheck($[3], t1, "t1", "Component", "recomputed", "(4:4)"); - t1 = $[3]; - } - } - const w = t1; - const t2 = useState(w); - let x; - { - [x] = t2; - let condition = $[4] !== t2; - if (!condition) { - let old$x = $[5]; - $structuralCheck(old$x, x, "x", "Component", "cached", "(5:5)"); - x = old$x; - } - $[4] = t2; - $[5] = x; - if (condition) { - [x] = t2; - $structuralCheck($[5], x, "x", "Component", "recomputed", "(5:5)"); - x = $[5]; - } - } - let t3; - { - t3 = ( + t1 = (
{x} {w}
); - let condition = $[6] !== x || $[7] !== w; + let condition = $[2] !== x || $[3] !== w; if (!condition) { - let old$t3 = $[8]; - $structuralCheck(old$t3, t3, "t3", "Component", "cached", "(7:10)"); - t3 = old$t3; + let old$t1 = $[4]; + $structuralCheck(old$t1, t1, "t1", "Component", "cached", "(7:10)"); } - $[6] = x; - $[7] = w; - $[8] = t3; + $[2] = x; + $[3] = w; + $[4] = t1; if (condition) { - t3 = ( + t1 = (
{x} {w}
); - $structuralCheck($[8], t3, "t3", "Component", "recomputed", "(7:10)"); - t3 = $[8]; + $structuralCheck($[4], t1, "t1", "Component", "recomputed", "(7:10)"); + t1 = $[4]; } } - return t3; + return t1; } function f(x) { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/value-block-mutates-outer-value.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/value-block-mutates-outer-value.expect.md index 29d19decd5..c57a307951 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/value-block-mutates-outer-value.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/value-block-mutates-outer-value.expect.md @@ -34,7 +34,6 @@ export const FIXTURE_ENTRYPOINT = { ## Code ```javascript -import { c as _c } from "react/compiler-runtime"; import { makeArray, useHook } from "shared-runtime"; /** @@ -46,16 +45,8 @@ import { makeArray, useHook } from "shared-runtime"; * merged with the scope producing customList */ function Foo(t0) { - const $ = _c(1); const { defaultList, cond } = t0; - let t1; - if ($[0] === Symbol.for("react.memo_cache_sentinel")) { - t1 = (a, b) => a - b; - $[0] = t1; - } else { - t1 = $[0]; - } - const comparator = t1; + const comparator = _temp; useHook(); const customList = makeArray(1, 5, 2); useHook(); @@ -64,6 +55,9 @@ function Foo(t0) { : defaultList; return result; } +function _temp(a, b) { + return a - b; +} export const FIXTURE_ENTRYPOINT = { fn: Foo, diff --git a/compiler/packages/eslint-plugin-react-compiler/__tests__/ReactCompilerRule-test.ts b/compiler/packages/eslint-plugin-react-compiler/__tests__/ReactCompilerRule-test.ts index 3955746176..55690a67cd 100644 --- a/compiler/packages/eslint-plugin-react-compiler/__tests__/ReactCompilerRule-test.ts +++ b/compiler/packages/eslint-plugin-react-compiler/__tests__/ReactCompilerRule-test.ts @@ -15,7 +15,7 @@ import ReactCompilerRule from "../src/rules/ReactCompilerRule"; */ function normalizeIndent(strings: TemplateStringsArray): string { const codeLines = strings[0].split("\n"); - const leftPadding = codeLines[1].match(/\s+/)[0]; + const leftPadding = codeLines[1].match(/\s+/)![0]; return codeLines.map((line) => line.slice(leftPadding.length)).join("\n"); } @@ -149,6 +149,56 @@ const tests: CompilerTestCases = { }, ], }, + { + name: "Multiple diagnostics are surfaced", + options: [ + { + reportableLevels: new Set([ + ErrorSeverity.Todo, + ErrorSeverity.InvalidReact, + ]), + }, + ], + code: normalizeIndent` + function Foo(x) { + var y = 1; + return
{y * x}
; + } + function Bar(props) { + props.a.b = 2; + return
{props.c}
+ }`, + errors: [ + { + message: + "(BuildHIR::lowerStatement) Handle var kinds in VariableDeclaration", + }, + { + message: + "Mutating component props or hook arguments is not allowed. Consider using a local variable instead", + }, + ], + }, + { + name: "Test experimental/unstable report all bailouts mode", + options: [ + { + reportableLevels: new Set([ErrorSeverity.InvalidReact]), + __unstable_donotuse_reportAllBailouts: true, + }, + ], + code: normalizeIndent` + function Foo(x) { + var y = 1; + return
{y * x}
; + }`, + errors: [ + { + message: + "[ReactCompilerBailout] (BuildHIR::lowerStatement) Handle var kinds in VariableDeclaration (@:3:2)", + }, + ], + }, ], }; diff --git a/compiler/packages/eslint-plugin-react-compiler/src/rules/ReactCompilerRule.ts b/compiler/packages/eslint-plugin-react-compiler/src/rules/ReactCompilerRule.ts index 7c46cf0b88..a407748898 100644 --- a/compiler/packages/eslint-plugin-react-compiler/src/rules/ReactCompilerRule.ts +++ b/compiler/packages/eslint-plugin-react-compiler/src/rules/ReactCompilerRule.ts @@ -6,22 +6,22 @@ */ import { transformFromAstSync } from "@babel/core"; -// @ts-expect-error +// @ts-expect-error: no types available import PluginProposalPrivateMethods from "@babel/plugin-proposal-private-methods"; import type { SourceLocation as BabelSourceLocation } from "@babel/types"; import BabelPluginReactCompiler, { + CompilerErrorDetailOptions, CompilerSuggestionOperation, ErrorSeverity, parsePluginOptions, validateEnvironmentConfig, - type CompilerError, - type CompilerErrorDetail, type PluginOptions, } from "babel-plugin-react-compiler/src"; +import { Logger } from "babel-plugin-react-compiler/src/Entrypoint"; import type { Rule } from "eslint"; import * as HermesParser from "hermes-parser"; -type CompilerErrorDetailWithLoc = Omit & { +type CompilerErrorDetailWithLoc = Omit & { loc: BabelSourceLocation; }; @@ -29,10 +29,6 @@ function assertExhaustive(_: never, errorMsg: string): never { throw new Error(errorMsg); } -function isReactCompilerError(err: Error): err is CompilerError { - return err.name === "ReactCompilerError"; -} - const DEFAULT_REPORTABLE_LEVELS = new Set([ ErrorSeverity.InvalidReact, ErrorSeverity.InvalidJS, @@ -40,7 +36,7 @@ const DEFAULT_REPORTABLE_LEVELS = new Set([ let reportableLevels = DEFAULT_REPORTABLE_LEVELS; function isReportableDiagnostic( - detail: CompilerErrorDetail + detail: CompilerErrorDetailOptions ): detail is CompilerErrorDetailWithLoc { return ( reportableLevels.has(detail.severity) && @@ -49,10 +45,63 @@ function isReportableDiagnostic( ); } +function makeSuggestions( + detail: CompilerErrorDetailOptions +): Array { + let suggest: Array = []; + if (Array.isArray(detail.suggestions)) { + for (const suggestion of detail.suggestions) { + switch (suggestion.op) { + case CompilerSuggestionOperation.InsertBefore: + suggest.push({ + desc: suggestion.description, + fix(fixer) { + return fixer.insertTextBeforeRange( + suggestion.range, + suggestion.text + ); + }, + }); + break; + case CompilerSuggestionOperation.InsertAfter: + suggest.push({ + desc: suggestion.description, + fix(fixer) { + return fixer.insertTextAfterRange( + suggestion.range, + suggestion.text + ); + }, + }); + break; + case CompilerSuggestionOperation.Replace: + suggest.push({ + desc: suggestion.description, + fix(fixer) { + return fixer.replaceTextRange(suggestion.range, suggestion.text); + }, + }); + break; + case CompilerSuggestionOperation.Remove: + suggest.push({ + desc: suggestion.description, + fix(fixer) { + return fixer.removeRange(suggestion.range); + }, + }); + break; + default: + assertExhaustive(suggestion, "Unhandled suggestion operation"); + } + } + } + return suggest; +} + const COMPILER_OPTIONS: Partial = { noEmit: true, compilationMode: "infer", - panicThreshold: "all_errors", + panicThreshold: "none", }; const rule: Rule.RuleModule = { @@ -80,10 +129,66 @@ const rule: Rule.RuleModule = { } else { reportableLevels = DEFAULT_REPORTABLE_LEVELS; } + /** + * Experimental setting to report all compilation bailouts on the compilation + * unit (e.g. function or hook) instead of the offensive line. + * Intended to be used when a codebase is 100% reliant on the compiler for + * memoization (i.e. deleted all manual memo) and needs compilation success + * signals for perf debugging. + */ + let __unstable_donotuse_reportAllBailouts: boolean = false; + if ( + userOpts["__unstable_donotuse_reportAllBailouts"] != null && + typeof userOpts["__unstable_donotuse_reportAllBailouts"] === "boolean" + ) { + __unstable_donotuse_reportAllBailouts = + userOpts["__unstable_donotuse_reportAllBailouts"]; + } + const options: PluginOptions = { ...parsePluginOptions(userOpts), ...COMPILER_OPTIONS, }; + const userLogger: Logger | null = options.logger; + options.logger = { + logEvent: (filename, event): void => { + userLogger?.logEvent(filename, event); + if (event.kind === "CompileError") { + const detail = event.detail; + const suggest = makeSuggestions(detail); + if (__unstable_donotuse_reportAllBailouts && event.fnLoc != null) { + const locStr = + detail.loc != null && typeof detail.loc !== "symbol" + ? ` (@:${detail.loc.start.line}:${detail.loc.start.column})` + : ""; + context.report({ + message: `[ReactCompilerBailout] ${detail.reason}${locStr}`, + loc: event.fnLoc, + suggest, + }); + } + + if (!isReportableDiagnostic(detail)) { + return; + } + if (hasFlowSuppression(detail.loc, "react-rule-hook")) { + // If Flow already caught this error, we don't need to report it again. + return; + } + const loc = + detail.loc == null || typeof detail.loc == "symbol" + ? event.fnLoc + : detail.loc; + if (loc != null) { + context.report({ + message: detail.reason, + loc, + suggest, + }); + } + } + }, + }; try { options.environment = validateEnvironmentConfig( @@ -96,7 +201,7 @@ const rule: Rule.RuleModule = { function hasFlowSuppression( nodeLoc: BabelSourceLocation, suppression: string - ) { + ): boolean { const sourceCode = context.getSourceCode(); const comments = sourceCode.getAllComments(); const flowSuppressionRegex = new RegExp( @@ -122,7 +227,9 @@ const rule: Rule.RuleModule = { sourceType: "unambiguous", plugins: ["typescript", "jsx"], }); - } catch {} + } catch { + /* empty */ + } } else { try { babelAST = HermesParser.parse(sourceCode, { @@ -131,7 +238,9 @@ const rule: Rule.RuleModule = { sourceFilename: filename, sourceType: "module", }); - } catch {} + } catch { + /* empty */ + } } if (babelAST != null) { @@ -149,77 +258,7 @@ const rule: Rule.RuleModule = { babelrc: false, }); } catch (err) { - if (isReactCompilerError(err) && Array.isArray(err.details)) { - for (const detail of err.details) { - if (!isReportableDiagnostic(detail)) { - continue; - } - if (hasFlowSuppression(detail.loc, "react-rule-hook")) { - // If Flow already caught this error, we don't need to report it again. - continue; - } - let suggest: Array = []; - if (Array.isArray(detail.suggestions)) { - for (const suggestion of detail.suggestions) { - switch (suggestion.op) { - case CompilerSuggestionOperation.InsertBefore: - suggest.push({ - desc: suggestion.description, - fix(fixer) { - return fixer.insertTextBeforeRange( - suggestion.range, - suggestion.text - ); - }, - }); - break; - case CompilerSuggestionOperation.InsertAfter: - suggest.push({ - desc: suggestion.description, - fix(fixer) { - return fixer.insertTextAfterRange( - suggestion.range, - suggestion.text - ); - }, - }); - break; - case CompilerSuggestionOperation.Replace: - suggest.push({ - desc: suggestion.description, - fix(fixer) { - return fixer.replaceTextRange( - suggestion.range, - suggestion.text - ); - }, - }); - break; - case CompilerSuggestionOperation.Remove: - suggest.push({ - desc: suggestion.description, - fix(fixer) { - return fixer.removeRange(suggestion.range); - }, - }); - break; - default: - assertExhaustive( - suggestion, - "Unhandled suggestion operation" - ); - } - } - } - context.report({ - message: detail.reason, - loc: detail.loc, - suggest, - }); - } - } else { - options.logger?.logEvent("", err); - } + /* errors handled by injected logger */ } } return {}; diff --git a/compiler/packages/snap/package.json b/compiler/packages/snap/package.json index 60ccff029c..d6bb00e841 100644 --- a/compiler/packages/snap/package.json +++ b/compiler/packages/snap/package.json @@ -19,7 +19,6 @@ }, "dependencies": { "@babel/code-frame": "^7.22.5", - "@babel/generator": "7.2.0", "@babel/plugin-syntax-jsx": "^7.18.6", "@babel/preset-flow": "^7.7.4", "@babel/preset-typescript": "^7.18.6", @@ -54,8 +53,6 @@ "resolutions": { "./**/@babel/parser": "7.7.4", "./**/@babel/types": "7.7.4", - "@babel/core": "7.2.0", - "@babel/traverse": "7.1.6", "@babel/preset-flow": "7.22.5" } } diff --git a/compiler/packages/snap/src/compiler.ts b/compiler/packages/snap/src/compiler.ts index 8afbecde8a..e7e23cd487 100644 --- a/compiler/packages/snap/src/compiler.ts +++ b/compiler/packages/snap/src/compiler.ts @@ -46,7 +46,6 @@ function makePluginOptions( // TODO(@mofeiZ) rewrite snap fixtures to @validatePreserveExistingMemo:false let validatePreserveExistingMemoizationGuarantees = false; let enableChangeDetection = null; - let enablePreserveExistingManualUseMemo: "hook" | "scope" | null = null; let customMacros = null; if (firstLine.indexOf("@compilationMode(annotation)") !== -1) { @@ -141,28 +140,6 @@ function makePluginOptions( }, }; } - - const useMemoMatch = /@enablePreserveExistingManualUseMemo:"([^"]+)"/.exec( - firstLine - ); - if ( - useMemoMatch && - (useMemoMatch[1] === "hook" || useMemoMatch[1] === "scope") - ) { - enablePreserveExistingManualUseMemo = useMemoMatch[1]; - } else if ( - useMemoMatch && - (useMemoMatch[1] === "false" || useMemoMatch[1] === "off") - ) { - enablePreserveExistingManualUseMemo = null; - } else if (useMemoMatch) { - throw new Error( - `Invalid setting '${useMemoMatch[1]}' for 'enablePreserveExistingManualUseMemo'. Valid settings are 'hook', 'scope', or 'off'.` - ); - } else if (firstLine.includes("@enablePreserveExistingManualUseMemo")) { - enablePreserveExistingManualUseMemo = "scope"; - } - const hookPatternMatch = /@hookPattern:"([^"]+)"/.exec(firstLine); if ( hookPatternMatch && @@ -240,7 +217,6 @@ function makePluginOptions( hookPattern, validatePreserveExistingMemoizationGuarantees, enableChangeDetection, - enablePreserveExistingManualUseMemo, }, compilationMode, logger, diff --git a/compiler/yarn.lock b/compiler/yarn.lock index f54b9be27d..b7b92782b0 100644 --- a/compiler/yarn.lock +++ b/compiler/yarn.lock @@ -111,6 +111,16 @@ source-map "^0.5.0" trim-right "^1.0.1" +"@babel/generator@^7.19.1": + version "7.24.9" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.24.9.tgz#5c2575a1070e661bbbc9df82a853989c9a656f12" + integrity sha512-G8v3jRg+z8IwY1jHFxvCNhOPYPterE4XljNgdGTYfSTtzzwjIswIzIaSPSLs3R7yFuqnqNeay5rjICfqVr+/6A== + dependencies: + "@babel/types" "^7.24.9" + "@jridgewell/gen-mapping" "^0.3.5" + "@jridgewell/trace-mapping" "^0.3.25" + jsesc "^2.5.1" + "@babel/generator@^7.24.5", "@babel/generator@^7.7.2": version "7.24.5" resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.24.5.tgz#e5afc068f932f05616b66713e28d0f04e99daeb3" @@ -553,6 +563,11 @@ resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.24.1.tgz#f99c36d3593db9540705d0739a1f10b5e20c696e" integrity sha512-2ofRCjnnA9y+wk8b9IAREroeUP02KHp431N2mhKniy2yKIDKpbrHv9eXwm8cBeWQYcJmzv5qKCu65P47eCF7CQ== +"@babel/helper-string-parser@^7.24.8": + version "7.24.8" + resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.24.8.tgz#5b3329c9a58803d5df425e5785865881a81ca48d" + integrity sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ== + "@babel/helper-validator-identifier@^7.18.6", "@babel/helper-validator-identifier@^7.19.1": version "7.19.1" resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz#7eea834cf32901ffdc1a7ee555e2f9c27e249ca2" @@ -573,6 +588,11 @@ resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.5.tgz#918b1a7fa23056603506370089bd990d8720db62" integrity sha512-3q93SSKX2TWCG30M2G2kwaKeTYgEUp5Snjuj8qm729SObL6nbtUldAi37qbxkD5gg3xnBio+f9nqpSepGZMvxA== +"@babel/helper-validator-identifier@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz#75b889cfaf9e35c2aaf42cf0d72c8e91719251db" + integrity sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w== + "@babel/helper-validator-option@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.18.6.tgz#bf0d2b5a509b1f336099e4ff36e1a63aa5db4db8" @@ -1690,6 +1710,15 @@ "@babel/helper-validator-identifier" "^7.24.5" to-fast-properties "^2.0.0" +"@babel/types@^7.24.9": + version "7.24.9" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.24.9.tgz#228ce953d7b0d16646e755acf204f4cf3d08cc73" + integrity sha512-xm8XrMKz0IlUdocVbYJe0Z9xEgidU7msskG8BbhnTPK/HZ2z/7FP7ykqPgrUH+C+r414mNfNWam1f2vqOjqjYQ== + dependencies: + "@babel/helper-string-parser" "^7.24.8" + "@babel/helper-validator-identifier" "^7.24.7" + to-fast-properties "^2.0.0" + "@bcoe/v8-coverage@^0.2.3": version "0.2.3" resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" @@ -9144,16 +9173,7 @@ string-length@^4.0.1: char-regex "^1.0.2" strip-ansi "^6.0.0" -"string-width-cjs@npm:string-width@^4.2.0": - version "4.2.3" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - -string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -9246,14 +9266,7 @@ string_decoder@^1.1.1: dependencies: safe-buffer "~5.2.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1": - version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - -strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -9904,7 +9917,7 @@ wordwrap@>=0.0.2: resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" integrity sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -9922,15 +9935,6 @@ wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" -wrap-ansi@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" diff --git a/fixtures/flight/src/Counter.js b/fixtures/flight/src/Counter.js index f89840de69..30170bed3f 100644 --- a/fixtures/flight/src/Counter.js +++ b/fixtures/flight/src/Counter.js @@ -1,12 +1,11 @@ 'use client'; import * as React from 'react'; -import {useFormState} from 'react-dom'; import Container from './Container.js'; export function Counter({incrementAction}) { - const [count, incrementFormAction] = useFormState(incrementAction, 0); + const [count, incrementFormAction] = React.useActionState(incrementAction, 0); return (
diff --git a/package.json b/package.json index 663538b506..e310883cc8 100644 --- a/package.json +++ b/package.json @@ -63,14 +63,14 @@ "eslint-plugin-react-internal": "link:./scripts/eslint-rules", "fbjs-scripts": "^3.0.1", "filesize": "^6.0.1", - "flow-bin": "^0.232.0", - "flow-remove-types": "^2.232.0", + "flow-bin": "^0.235.0", + "flow-remove-types": "^2.235.0", "glob": "^7.1.6", "glob-stream": "^6.1.0", "google-closure-compiler": "^20230206.0.0", "gzip-size": "^5.1.1", - "hermes-eslint": "^0.20.1", - "hermes-parser": "^0.20.1", + "hermes-eslint": "^0.22.0", + "hermes-parser": "^0.22.0", "jest": "^29.4.2", "jest-cli": "^29.4.2", "jest-diff": "^29.4.2", @@ -112,6 +112,7 @@ "build-for-devtools": "cross-env RELEASE_CHANNEL=experimental yarn build react/index,react/jsx,react/compiler-runtime,react-dom/index,react-dom/client,react-dom/unstable_testing,react-dom/test-utils,react-is,react-debug-tools,scheduler,react-test-renderer,react-refresh,react-art --type=NODE", "build-for-devtools-dev": "yarn build-for-devtools --type=NODE_DEV", "build-for-devtools-prod": "yarn build-for-devtools --type=NODE_PROD", + "build-for-flight-dev": "cross-env RELEASE_CHANNEL=experimental node ./scripts/rollup/build.js react/index,react/jsx,react-dom/index,react-dom/client,react-dom/server,react-dom-server.node,react-dom-server-legacy.node,scheduler,react-server-dom-webpack/ --type=NODE_DEV && mv ./build/node_modules ./build/oss-experimental", "linc": "node ./scripts/tasks/linc.js", "lint": "node ./scripts/tasks/eslint.js", "lint-build": "node ./scripts/rollup/validate/index.js", diff --git a/packages/eslint-plugin-react-hooks/__tests__/ESLintRuleExhaustiveDeps-test.js b/packages/eslint-plugin-react-hooks/__tests__/ESLintRuleExhaustiveDeps-test.js index 235d60349b..c9ba00f213 100644 --- a/packages/eslint-plugin-react-hooks/__tests__/ESLintRuleExhaustiveDeps-test.js +++ b/packages/eslint-plugin-react-hooks/__tests__/ESLintRuleExhaustiveDeps-test.js @@ -607,6 +607,8 @@ const tests = { const [state4, dispatch2] = React.useReducer(); const [state5, maybeSetState] = useFunnyState(); const [state6, maybeDispatch] = useFunnyReducer(); + const [state9, dispatch5] = useActionState(); + const [state10, dispatch6] = React.useActionState(); const [isPending1] = useTransition(); const [isPending2, startTransition2] = useTransition(); const [isPending3] = React.useTransition(); @@ -624,6 +626,8 @@ const tests = { setState2(); dispatch1(); dispatch2(); + dispatch5(); + dispatch6(); startTransition1(); startTransition2(); startTransition3(); @@ -646,7 +650,7 @@ const tests = { maybeDispatch(); }, [ // Dynamic - state1, state2, state3, state4, state5, state6, + state1, state2, state3, state4, state5, state6, state9, state10, maybeRef1, maybeRef2, isPending2, isPending4, @@ -1494,6 +1498,51 @@ const tests = { }, ], }, + { + // Affected code should use React.useActionState instead + code: normalizeIndent` + function ComponentUsingFormState(props) { + const [state7, dispatch3] = useFormState(); + const [state8, dispatch4] = ReactDOM.useFormState(); + useEffect(() => { + dispatch3(); + dispatch4(); + + // dynamic + console.log(state7); + console.log(state8); + + }, [state7, state8]); + } + `, + errors: [ + { + message: + "React Hook useEffect has missing dependencies: 'dispatch3' and 'dispatch4'. " + + 'Either include them or remove the dependency array.', + suggestions: [ + { + desc: 'Update the dependencies array to be: [dispatch3, dispatch4, state7, state8]', + output: normalizeIndent` + function ComponentUsingFormState(props) { + const [state7, dispatch3] = useFormState(); + const [state8, dispatch4] = ReactDOM.useFormState(); + useEffect(() => { + dispatch3(); + dispatch4(); + + // dynamic + console.log(state7); + console.log(state8); + + }, [dispatch3, dispatch4, state7, state8]); + } + `, + }, + ], + }, + ], + }, { code: normalizeIndent` function MyComponent(props) { diff --git a/packages/eslint-plugin-react-hooks/src/ExhaustiveDeps.js b/packages/eslint-plugin-react-hooks/src/ExhaustiveDeps.js index f012428961..48ccc1e6bb 100644 --- a/packages/eslint-plugin-react-hooks/src/ExhaustiveDeps.js +++ b/packages/eslint-plugin-react-hooks/src/ExhaustiveDeps.js @@ -179,6 +179,8 @@ export default { // ^^^ true for this reference // const [state, dispatch] = useReducer() / React.useReducer() // ^^^ true for this reference + // const [state, dispatch] = useActionState() / React.useActionState() + // ^^^ true for this reference // const ref = useRef() // ^^^ true for this reference // const onStuff = useEffectEvent(() => {}) @@ -260,7 +262,11 @@ export default { } // useEffectEvent() return value is always unstable. return true; - } else if (name === 'useState' || name === 'useReducer') { + } else if ( + name === 'useState' || + name === 'useReducer' || + name === 'useActionState' + ) { // Only consider second value in initializing tuple stable. if ( id.type === 'ArrayPattern' && diff --git a/packages/internal-test-utils/__tests__/ReactInternalTestUtils-test.js b/packages/internal-test-utils/__tests__/ReactInternalTestUtils-test.js index e2a8f34cc9..befbdc617d 100644 --- a/packages/internal-test-utils/__tests__/ReactInternalTestUtils-test.js +++ b/packages/internal-test-utils/__tests__/ReactInternalTestUtils-test.js @@ -866,16 +866,40 @@ describe('ReactInternalTestUtils console assertions', () => { const message = expectToThrowFailure(() => { expect(root).toMatchRenderedOutput(
foobarbaz
); }); - expect(message).toMatchInlineSnapshot(` - "asserConsoleLogsCleared(expected) + if (!__DEV__) { + expect(message).toMatchInlineSnapshot(` + "asserConsoleLogsCleared(expected) - console.log was called without assertConsoleLogDev: - + Not asserted - + Not asserted - + Not asserted + console.log was called without assertConsoleLogDev: + + Not asserted + + Not asserted + + Not asserted - You must call one of the assertConsoleDev helpers between each act call." - `); + You must call one of the assertConsoleDev helpers between each act call." + `); + } else if (gate(flags => flags.enableOwnerStacks)) { + expect(message).toMatchInlineSnapshot(` + "asserConsoleLogsCleared(expected) + + console.log was called without assertConsoleLogDev: + + Not asserted + + Not asserted + + Not asserted + + You must call one of the assertConsoleDev helpers between each act call." + `); + } else { + expect(message).toMatchInlineSnapshot(` + "asserConsoleLogsCleared(expected) + + console.log was called without assertConsoleLogDev: + + Not asserted + + Not asserted + + Not asserted + + You must call one of the assertConsoleDev helpers between each act call." + `); + } expect(root).toMatchRenderedOutput(
foobarbaz
); }); @@ -922,16 +946,52 @@ describe('ReactInternalTestUtils console assertions', () => { }); }); - expect(message).toMatchInlineSnapshot(` - "asserConsoleLogsCleared(expected) + if (!__DEV__) { + expect(message).toMatchInlineSnapshot(` + "asserConsoleLogsCleared(expected) - console.warn was called without assertConsoleWarnDev: - + A - + B - + C + console.warn was called without assertConsoleWarnDev: + + A + + B + + C - You must call one of the assertConsoleDev helpers between each act call." - `); + You must call one of the assertConsoleDev helpers between each act call." + `); + } else if (gate(flags => flags.enableOwnerStacks)) { + expect(message).toMatchInlineSnapshot(` + "asserConsoleLogsCleared(expected) + + console.warn was called without assertConsoleWarnDev: + + A%s, + + in App (at **) + + B%s, + + in App (at **) + + C%s, + + in App (at **) + + You must call one of the assertConsoleDev helpers between each act call." + `); + } else { + expect(message).toMatchInlineSnapshot(` + "asserConsoleLogsCleared(expected) + + console.warn was called without assertConsoleWarnDev: + + A%s, + + in Yield (at **) + + in div (at **) + + in App (at **) + + B%s, + + in Yield (at **) + + in div (at **) + + in App (at **) + + C%s, + + in Yield (at **) + + in div (at **) + + in App (at **) + + You must call one of the assertConsoleDev helpers between each act call." + `); + } }); it('fails if act is called without any assertConsoleDev helpers', async () => { @@ -962,26 +1022,94 @@ describe('ReactInternalTestUtils console assertions', () => { }); }); - expect(message).toMatchInlineSnapshot(` - "asserConsoleLogsCleared(expected) + if (!__DEV__) { + expect(message).toMatchInlineSnapshot(` + "asserConsoleLogsCleared(expected) - console.log was called without assertConsoleLogDev: - + A - + B - + C + console.log was called without assertConsoleLogDev: + + A + + B + + C - console.warn was called without assertConsoleWarnDev: - + A - + B - + C + console.warn was called without assertConsoleWarnDev: + + A + + B + + C - console.error was called without assertConsoleErrorDev: - + A - + B - + C + console.error was called without assertConsoleErrorDev: + + A + + B + + C - You must call one of the assertConsoleDev helpers between each act call." - `); + You must call one of the assertConsoleDev helpers between each act call." + `); + } else if (gate(flags => flags.enableOwnerStacks)) { + expect(message).toMatchInlineSnapshot(` + "asserConsoleLogsCleared(expected) + + console.log was called without assertConsoleLogDev: + + A + + B + + C + + console.warn was called without assertConsoleWarnDev: + + A%s, + + in App (at **) + + B%s, + + in App (at **) + + C%s, + + in App (at **) + + console.error was called without assertConsoleErrorDev: + + A%s, + + in App (at **) + + B%s, + + in App (at **) + + C%s, + + in App (at **) + + You must call one of the assertConsoleDev helpers between each act call." + `); + } else { + expect(message).toMatchInlineSnapshot(` + "asserConsoleLogsCleared(expected) + + console.log was called without assertConsoleLogDev: + + A + + B + + C + + console.warn was called without assertConsoleWarnDev: + + A%s, + + in Yield (at **) + + in div (at **) + + in App (at **) + + B%s, + + in Yield (at **) + + in div (at **) + + in App (at **) + + C%s, + + in Yield (at **) + + in div (at **) + + in App (at **) + + console.error was called without assertConsoleErrorDev: + + A%s, + + in Yield (at **) + + in div (at **) + + in App (at **) + + B%s, + + in Yield (at **) + + in div (at **) + + in App (at **) + + C%s, + + in Yield (at **) + + in div (at **) + + in App (at **) + + You must call one of the assertConsoleDev helpers between each act call." + `); + } }); // @gate __DEV__ @@ -1002,8 +1130,8 @@ describe('ReactInternalTestUtils console assertions', () => { - Hi - Wow - Bye - + Wow - + Bye " + + Wow in div (at **) + + Bye in div (at **)" `); }); @@ -1025,8 +1153,8 @@ describe('ReactInternalTestUtils console assertions', () => { - Hi - Wow - Bye - + Hi - + Bye " + + Hi in div (at **) + + Bye in div (at **)" `); }); @@ -1048,8 +1176,8 @@ describe('ReactInternalTestUtils console assertions', () => { - Hi - Wow - Bye - + Hi - + Wow " + + Hi in div (at **) + + Wow in div (at **)" `); }); @@ -1071,9 +1199,9 @@ describe('ReactInternalTestUtils console assertions', () => { - Wow - Bye - + Hi - + Wow - + Bye " + + Hi in div (at **) + + Wow in div (at **) + + Bye in div (at **)" `); }); @@ -1095,9 +1223,9 @@ describe('ReactInternalTestUtils console assertions', () => { - Hi - Bye - + Hi - + Wow - + Bye " + + Hi in div (at **) + + Wow in div (at **) + + Bye in div (at **)" `); }); @@ -1119,9 +1247,9 @@ describe('ReactInternalTestUtils console assertions', () => { - Hi - Wow - + Hi - + Wow - + Bye " + + Hi in div (at **) + + Wow in div (at **) + + Bye in div (at **)" `); }); @@ -1297,7 +1425,8 @@ describe('ReactInternalTestUtils console assertions', () => { "assertConsoleWarnDev(expected) Unexpected component stack for: - "Hello " + "Hello + in div (at **)" If this warning should include a component stack, remove {withoutStack: true} from this warning. If all warnings should include the component stack, you may need to remove {withoutStack: true} from the assertConsoleWarnDev call." @@ -1318,10 +1447,12 @@ describe('ReactInternalTestUtils console assertions', () => { "assertConsoleWarnDev(expected) Unexpected component stack for: - "Hello " + "Hello + in div (at **)" Unexpected component stack for: - "Bye " + "Bye + in div (at **)" If this warning should include a component stack, remove {withoutStack: true} from this warning. If all warnings should include the component stack, you may need to remove {withoutStack: true} from the assertConsoleWarnDev call." @@ -1444,7 +1575,8 @@ describe('ReactInternalTestUtils console assertions', () => { "assertConsoleWarnDev(expected) Unexpected component stack for: - "Hello " + "Hello + in div (at **)" If this warning should include a component stack, remove {withoutStack: true} from this warning. If all warnings should include the component stack, you may need to remove {withoutStack: true} from the assertConsoleWarnDev call." @@ -1477,10 +1609,12 @@ describe('ReactInternalTestUtils console assertions', () => { "assertConsoleWarnDev(expected) Unexpected component stack for: - "Hello " + "Hello + in div (at **)" Unexpected component stack for: - "Bye " + "Bye + in div (at **)" If this warning should include a component stack, remove {withoutStack: true} from this warning. If all warnings should include the component stack, you may need to remove {withoutStack: true} from the assertConsoleWarnDev call." @@ -1798,16 +1932,49 @@ describe('ReactInternalTestUtils console assertions', () => { const message = expectToThrowFailure(() => { expect(root).toMatchRenderedOutput(
foobarbaz
); }); - expect(message).toMatchInlineSnapshot(` - "asserConsoleLogsCleared(expected) + if (!__DEV__) { + expect(message).toMatchInlineSnapshot(` + "asserConsoleLogsCleared(expected) - console.warn was called without assertConsoleWarnDev: - + Not asserted - + Not asserted - + Not asserted + console.warn was called without assertConsoleWarnDev: + + Not asserted + + Not asserted + + Not asserted - You must call one of the assertConsoleDev helpers between each act call." - `); + You must call one of the assertConsoleDev helpers between each act call." + `); + } else if (gate(flags => flags.enableOwnerStacks)) { + expect(message).toMatchInlineSnapshot(` + "asserConsoleLogsCleared(expected) + + console.warn was called without assertConsoleWarnDev: + + Not asserted%s, + + in Yield (at **) + + Not asserted%s, + + in Yield (at **) + + Not asserted%s, + + in Yield (at **) + + You must call one of the assertConsoleDev helpers between each act call." + `); + } else { + expect(message).toMatchInlineSnapshot(` + "asserConsoleLogsCleared(expected) + + console.warn was called without assertConsoleWarnDev: + + Not asserted%s, + + in Yield (at **) + + in div (at **) + + Not asserted%s, + + in Yield (at **) + + in div (at **) + + Not asserted%s, + + in Yield (at **) + + in div (at **) + + You must call one of the assertConsoleDev helpers between each act call." + `); + } expect(root).toMatchRenderedOutput(
foobarbaz
); }); @@ -1854,16 +2021,52 @@ describe('ReactInternalTestUtils console assertions', () => { }); }); - expect(message).toMatchInlineSnapshot(` - "asserConsoleLogsCleared(expected) + if (!__DEV__) { + expect(message).toMatchInlineSnapshot(` + "asserConsoleLogsCleared(expected) - console.error was called without assertConsoleErrorDev: - + A - + B - + C + console.error was called without assertConsoleErrorDev: + + A + + B + + C - You must call one of the assertConsoleDev helpers between each act call." - `); + You must call one of the assertConsoleDev helpers between each act call." + `); + } else if (gate(flags => flags.enableOwnerStacks)) { + expect(message).toMatchInlineSnapshot(` + "asserConsoleLogsCleared(expected) + + console.error was called without assertConsoleErrorDev: + + A%s, + + in App (at **) + + B%s, + + in App (at **) + + C%s, + + in App (at **) + + You must call one of the assertConsoleDev helpers between each act call." + `); + } else { + expect(message).toMatchInlineSnapshot(` + "asserConsoleLogsCleared(expected) + + console.error was called without assertConsoleErrorDev: + + A%s, + + in Yield (at **) + + in div (at **) + + in App (at **) + + B%s, + + in Yield (at **) + + in div (at **) + + in App (at **) + + C%s, + + in Yield (at **) + + in div (at **) + + in App (at **) + + You must call one of the assertConsoleDev helpers between each act call." + `); + } }); it('fails if act is called without any assertConsoleDev helpers', async () => { @@ -1894,26 +2097,94 @@ describe('ReactInternalTestUtils console assertions', () => { }); }); - expect(message).toMatchInlineSnapshot(` - "asserConsoleLogsCleared(expected) + if (!__DEV__) { + expect(message).toMatchInlineSnapshot(` + "asserConsoleLogsCleared(expected) - console.log was called without assertConsoleLogDev: - + A - + B - + C + console.log was called without assertConsoleLogDev: + + A + + B + + C - console.warn was called without assertConsoleWarnDev: - + A - + B - + C + console.warn was called without assertConsoleWarnDev: + + A + + B + + C - console.error was called without assertConsoleErrorDev: - + A - + B - + C + console.error was called without assertConsoleErrorDev: + + A + + B + + C - You must call one of the assertConsoleDev helpers between each act call." - `); + You must call one of the assertConsoleDev helpers between each act call." + `); + } else if (gate(flags => flags.enableOwnerStacks)) { + expect(message).toMatchInlineSnapshot(` + "asserConsoleLogsCleared(expected) + + console.log was called without assertConsoleLogDev: + + A + + B + + C + + console.warn was called without assertConsoleWarnDev: + + A%s, + + in App (at **) + + B%s, + + in App (at **) + + C%s, + + in App (at **) + + console.error was called without assertConsoleErrorDev: + + A%s, + + in App (at **) + + B%s, + + in App (at **) + + C%s, + + in App (at **) + + You must call one of the assertConsoleDev helpers between each act call." + `); + } else { + expect(message).toMatchInlineSnapshot(` + "asserConsoleLogsCleared(expected) + + console.log was called without assertConsoleLogDev: + + A + + B + + C + + console.warn was called without assertConsoleWarnDev: + + A%s, + + in Yield (at **) + + in div (at **) + + in App (at **) + + B%s, + + in Yield (at **) + + in div (at **) + + in App (at **) + + C%s, + + in Yield (at **) + + in div (at **) + + in App (at **) + + console.error was called without assertConsoleErrorDev: + + A%s, + + in Yield (at **) + + in div (at **) + + in App (at **) + + B%s, + + in Yield (at **) + + in div (at **) + + in App (at **) + + C%s, + + in Yield (at **) + + in div (at **) + + in App (at **) + + You must call one of the assertConsoleDev helpers between each act call." + `); + } }); // @gate __DEV__ @@ -1934,8 +2205,8 @@ describe('ReactInternalTestUtils console assertions', () => { - Hi - Wow - Bye - + Wow - + Bye " + + Wow in div (at **) + + Bye in div (at **)" `); }); @@ -1957,8 +2228,8 @@ describe('ReactInternalTestUtils console assertions', () => { - Hi - Wow - Bye - + Hi - + Bye " + + Hi in div (at **) + + Bye in div (at **)" `); }); @@ -1980,8 +2251,8 @@ describe('ReactInternalTestUtils console assertions', () => { - Hi - Wow - Bye - + Hi - + Wow " + + Hi in div (at **) + + Wow in div (at **)" `); }); @@ -2003,9 +2274,9 @@ describe('ReactInternalTestUtils console assertions', () => { - Wow - Bye - + Hi - + Wow - + Bye " + + Hi in div (at **) + + Wow in div (at **) + + Bye in div (at **)" `); }); @@ -2027,9 +2298,9 @@ describe('ReactInternalTestUtils console assertions', () => { - Hi - Bye - + Hi - + Wow - + Bye " + + Hi in div (at **) + + Wow in div (at **) + + Bye in div (at **)" `); }); @@ -2051,9 +2322,9 @@ describe('ReactInternalTestUtils console assertions', () => { - Hi - Wow - + Hi - + Wow - + Bye " + + Hi in div (at **) + + Wow in div (at **) + + Bye in div (at **)" `); }); // @gate __DEV__ @@ -2170,7 +2441,7 @@ describe('ReactInternalTestUtils console assertions', () => { + Received errors - This is a completely different message that happens to start with "T" - + Message that happens to contain a "T" " + + Message that happens to contain a "T" in div (at **)" `); }); @@ -2247,7 +2518,8 @@ describe('ReactInternalTestUtils console assertions', () => { "assertConsoleErrorDev(expected) Unexpected component stack for: - "Hello " + "Hello + in div (at **)" If this error should include a component stack, remove {withoutStack: true} from this error. If all errors should include the component stack, you may need to remove {withoutStack: true} from the assertConsoleErrorDev call." @@ -2268,10 +2540,12 @@ describe('ReactInternalTestUtils console assertions', () => { "assertConsoleErrorDev(expected) Unexpected component stack for: - "Hello " + "Hello + in div (at **)" Unexpected component stack for: - "Bye " + "Bye + in div (at **)" If this error should include a component stack, remove {withoutStack: true} from this error. If all errors should include the component stack, you may need to remove {withoutStack: true} from the assertConsoleErrorDev call." @@ -2394,7 +2668,8 @@ describe('ReactInternalTestUtils console assertions', () => { "assertConsoleErrorDev(expected) Unexpected component stack for: - "Hello " + "Hello + in div (at **)" If this error should include a component stack, remove {withoutStack: true} from this error. If all errors should include the component stack, you may need to remove {withoutStack: true} from the assertConsoleErrorDev call." @@ -2427,10 +2702,12 @@ describe('ReactInternalTestUtils console assertions', () => { "assertConsoleErrorDev(expected) Unexpected component stack for: - "Hello " + "Hello + in div (at **)" Unexpected component stack for: - "Bye " + "Bye + in div (at **)" If this error should include a component stack, remove {withoutStack: true} from this error. If all errors should include the component stack, you may need to remove {withoutStack: true} from the assertConsoleErrorDev call." @@ -2459,7 +2736,7 @@ describe('ReactInternalTestUtils console assertions', () => { + Received errors - Hello - + Bye " + + Bye in div (at **)" `); }); }); @@ -2774,16 +3051,49 @@ describe('ReactInternalTestUtils console assertions', () => { const message = expectToThrowFailure(() => { expect(root).toMatchRenderedOutput(
foobarbaz
); }); - expect(message).toMatchInlineSnapshot(` - "asserConsoleLogsCleared(expected) + if (!__DEV__) { + expect(message).toMatchInlineSnapshot(` + "asserConsoleLogsCleared(expected) - console.error was called without assertConsoleErrorDev: - + Not asserted - + Not asserted - + Not asserted + console.error was called without assertConsoleErrorDev: + + Not asserted + + Not asserted + + Not asserted - You must call one of the assertConsoleDev helpers between each act call." - `); + You must call one of the assertConsoleDev helpers between each act call." + `); + } else if (gate(flags => flags.enableOwnerStacks)) { + expect(message).toMatchInlineSnapshot(` + "asserConsoleLogsCleared(expected) + + console.error was called without assertConsoleErrorDev: + + Not asserted%s, + + in Yield (at **) + + Not asserted%s, + + in Yield (at **) + + Not asserted%s, + + in Yield (at **) + + You must call one of the assertConsoleDev helpers between each act call." + `); + } else { + expect(message).toMatchInlineSnapshot(` + "asserConsoleLogsCleared(expected) + + console.error was called without assertConsoleErrorDev: + + Not asserted%s, + + in Yield (at **) + + in div (at **) + + Not asserted%s, + + in Yield (at **) + + in div (at **) + + Not asserted%s, + + in Yield (at **) + + in div (at **) + + You must call one of the assertConsoleDev helpers between each act call." + `); + } expect(root).toMatchRenderedOutput(
foobarbaz
); }); diff --git a/packages/internal-test-utils/consoleMock.js b/packages/internal-test-utils/consoleMock.js index 328cf3d90d..6d03c74c1d 100644 --- a/packages/internal-test-utils/consoleMock.js +++ b/packages/internal-test-utils/consoleMock.js @@ -44,6 +44,34 @@ const patchConsoleMethod = ( return; } + // Append Component Stacks. Simulates a framework or DevTools appending them. + if ( + typeof format === 'string' && + (methodName === 'error' || methodName === 'warn') + ) { + const React = require('react'); + if (React.captureOwnerStack) { + // enableOwnerStacks enabled. When it's always on, we can assume this case. + const stack = React.captureOwnerStack(); + if (stack) { + format += '%s'; + args.push(stack); + } + } else { + // Otherwise we have to use internals to emulate parent stacks. + const ReactSharedInternals = + React.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE || + React.__SERVER_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE; + if (ReactSharedInternals && ReactSharedInternals.getCurrentStack) { + const stack = ReactSharedInternals.getCurrentStack(); + if (stack !== '') { + format += '%s'; + args.push(stack); + } + } + } + } + // Capture the call stack now so we can warn about it later. // The call stack has helpful information for the test author. // Don't throw yet though b'c it might be accidentally caught and suppressed. @@ -204,7 +232,7 @@ export function assertConsoleLogsCleared() { if (warnings.length > 0) { message += `\nconsole.warn was called without assertConsoleWarnDev:\n${diff( '', - warnings.join('\n'), + warnings.map(normalizeComponentStack).join('\n'), { omitAnnotationLines: true, }, @@ -213,7 +241,7 @@ export function assertConsoleLogsCleared() { if (errors.length > 0) { message += `\nconsole.error was called without assertConsoleErrorDev:\n${diff( '', - errors.join('\n'), + errors.map(normalizeComponentStack).join('\n'), { omitAnnotationLines: true, }, @@ -228,7 +256,7 @@ export function assertConsoleLogsCleared() { } } -function replaceComponentStack(str) { +function normalizeCodeLocInfo(str) { if (typeof str !== 'string') { return str; } @@ -239,11 +267,29 @@ function replaceComponentStack(str) { // at Component (/path/filename.js:123:45) // React format: // in Component (at filename.js:123) - return str.replace(/\n +(?:at|in) ([\S]+)[^\n]*.*/, function (m, name) { - return chalk.dim(' '); + return str.replace(/\n +(?:at|in) ([\S]+)[^\n]*/g, function (m, name) { + if (name.endsWith('.render')) { + // Class components will have the `render` method as part of their stack trace. + // We strip that out in our normalization to make it look more like component stacks. + name = name.slice(0, name.length - 7); + } + return '\n in ' + name + ' (at **)'; }); } +function normalizeComponentStack(entry) { + if ( + typeof entry[0] === 'string' && + entry[0].endsWith('%s') && + isLikelyAComponentStack(entry[entry.length - 1]) + ) { + const clone = entry.slice(0); + clone[clone.length - 1] = normalizeCodeLocInfo(entry[entry.length - 1]); + return clone; + } + return entry; +} + const isLikelyAComponentStack = message => typeof message === 'string' && (message.indexOf('') > -1 || @@ -382,11 +428,11 @@ export function createLogAssertion( ); } - expectedMessage = replaceComponentStack(currentExpectedMessage); + expectedMessage = normalizeCodeLocInfo(currentExpectedMessage); expectedWithoutStack = expectedMessageOrArray[1].withoutStack; } else if (typeof expectedMessageOrArray === 'string') { // Should be in the form assert(['log']) or assert(['log'], {withoutStack: true}) - expectedMessage = replaceComponentStack(expectedMessageOrArray); + expectedMessage = normalizeCodeLocInfo(expectedMessageOrArray); if (consoleMethod === 'log') { expectedWithoutStack = true; } else { @@ -410,7 +456,7 @@ export function createLogAssertion( ); } - const normalizedMessage = replaceComponentStack(message); + const normalizedMessage = normalizeCodeLocInfo(message); receivedLogs.push(normalizedMessage); // Check the number of %s interpolations. diff --git a/packages/react-client/src/ReactClientConsoleConfigBrowser.js b/packages/react-client/src/ReactClientConsoleConfigBrowser.js index da87324b6d..c1f525d28a 100644 --- a/packages/react-client/src/ReactClientConsoleConfigBrowser.js +++ b/packages/react-client/src/ReactClientConsoleConfigBrowser.js @@ -7,8 +7,6 @@ * @flow */ -import {warn, error} from 'shared/consoleWithStackDev'; - const badgeFormat = '%c%s%c '; // Same badge styling as DevTools. const badgeStyle = @@ -65,12 +63,6 @@ export function printToConsole( ); } - if (methodName === 'error') { - error.apply(console, newArgs); - } else if (methodName === 'warn') { - warn.apply(console, newArgs); - } else { - // eslint-disable-next-line react-internal/no-production-logging - console[methodName].apply(console, newArgs); - } + // $FlowFixMe[invalid-computed-prop] + console[methodName].apply(console, newArgs); // eslint-disable-line react-internal/no-production-logging } diff --git a/packages/react-client/src/ReactClientConsoleConfigPlain.js b/packages/react-client/src/ReactClientConsoleConfigPlain.js index a4e7c3c6d7..f2ec996b6a 100644 --- a/packages/react-client/src/ReactClientConsoleConfigPlain.js +++ b/packages/react-client/src/ReactClientConsoleConfigPlain.js @@ -7,8 +7,6 @@ * @flow */ -import {warn, error} from 'shared/consoleWithStackDev'; - const badgeFormat = '[%s] '; const pad = ' '; @@ -46,12 +44,6 @@ export function printToConsole( newArgs.splice(offset, 0, badgeFormat, pad + badgeName + pad); } - if (methodName === 'error') { - error.apply(console, newArgs); - } else if (methodName === 'warn') { - warn.apply(console, newArgs); - } else { - // eslint-disable-next-line react-internal/no-production-logging - console[methodName].apply(console, newArgs); - } + // $FlowFixMe[invalid-computed-prop] + console[methodName].apply(console, newArgs); // eslint-disable-line react-internal/no-production-logging } diff --git a/packages/react-client/src/ReactClientConsoleConfigServer.js b/packages/react-client/src/ReactClientConsoleConfigServer.js index f6ecad92f3..19da98a176 100644 --- a/packages/react-client/src/ReactClientConsoleConfigServer.js +++ b/packages/react-client/src/ReactClientConsoleConfigServer.js @@ -7,8 +7,6 @@ * @flow */ -import {warn, error} from 'shared/consoleWithStackDev'; - // This flips color using ANSI, then sets a color styling, then resets. const badgeFormat = '\x1b[0m\x1b[7m%c%s\x1b[0m%c '; // Same badge styling as DevTools. @@ -66,12 +64,6 @@ export function printToConsole( ); } - if (methodName === 'error') { - error.apply(console, newArgs); - } else if (methodName === 'warn') { - warn.apply(console, newArgs); - } else { - // eslint-disable-next-line react-internal/no-production-logging - console[methodName].apply(console, newArgs); - } + // $FlowFixMe[invalid-computed-prop] + console[methodName].apply(console, newArgs); // eslint-disable-line react-internal/no-production-logging } diff --git a/packages/react-client/src/ReactFlightClient.js b/packages/react-client/src/ReactFlightClient.js index 7d421f0422..c0d5992978 100644 --- a/packages/react-client/src/ReactFlightClient.js +++ b/packages/react-client/src/ReactFlightClient.js @@ -250,6 +250,7 @@ export type Response = { _tempRefs: void | TemporaryReferenceSet, // the set temporary references can be resolved from _debugRootTask?: null | ConsoleTask, // DEV-only _debugFindSourceMapURL?: void | FindSourceMapURLCallback, // DEV-only + _replayConsole: boolean, // DEV-only }; function readChunk(chunk: SomeChunk): T { @@ -1230,6 +1231,29 @@ function parseModelString( } // Fallthrough } + case 'Y': { + if (__DEV__) { + // In DEV mode we encode omitted objects in logs as a getter that throws + // so that when you try to access it on the client, you know why that + // happened. + Object.defineProperty(parentObject, key, { + get: function () { + // We intentionally don't throw an error object here because it looks better + // without the stack in the console which isn't useful anyway. + // eslint-disable-next-line no-throw-literal + throw ( + 'This object has been omitted by React in the console log ' + + 'to avoid sending too much data from the server. Try logging smaller ' + + 'or more specific objects.' + ); + }, + enumerable: true, + configurable: false, + }); + return null; + } + // Fallthrough + } default: { // We assume that anything else is a reference ID. const ref = value.slice(1); @@ -1278,6 +1302,7 @@ function ResponseInstance( nonce: void | string, temporaryReferences: void | TemporaryReferenceSet, findSourceMapURL: void | FindSourceMapURLCallback, + replayConsole: boolean, ) { const chunks: Map> = new Map(); this._bundlerConfig = bundlerConfig; @@ -1304,6 +1329,7 @@ function ResponseInstance( } if (__DEV__) { this._debugFindSourceMapURL = findSourceMapURL; + this._replayConsole = replayConsole; } // Don't inline this call because it causes closure to outline the call above. this._fromJSON = createFromJSONCallback(this); @@ -1317,6 +1343,7 @@ export function createResponse( nonce: void | string, temporaryReferences: void | TemporaryReferenceSet, findSourceMapURL: void | FindSourceMapURLCallback, + replayConsole: boolean, ): Response { // $FlowFixMe[invalid-constructor]: the shapes are exact here but Flow doesn't like constructors return new ResponseInstance( @@ -1327,6 +1354,7 @@ export function createResponse( nonce, temporaryReferences, findSourceMapURL, + replayConsole, ); } @@ -1863,19 +1891,33 @@ function createFakeFunction( const comment = '/* This module was rendered by a Server Component. Turn on Source Maps to see the server source. */'; + if (!name) { + // An eval:ed function with no name gets the name "eval". We give it something more descriptive. + name = '(anonymous)'; + } + const encodedName = JSON.stringify(name); // We generate code where the call is at the line and column of the server executed code. // This allows us to use the original source map as the source map of this fake file to // point to the original source. let code; if (line <= 1) { - code = '_=>' + ' '.repeat(col < 4 ? 0 : col - 4) + '_()\n' + comment; + const minSize = encodedName.length + 7; + code = + '({' + + encodedName + + ':_=>' + + ' '.repeat(col < minSize ? 0 : col - minSize) + + '_()})\n' + + comment; } else { code = comment + '\n'.repeat(line - 2) + - '_=>\n' + + '({' + + encodedName + + ':_=>\n' + ' '.repeat(col < 1 ? 0 : col - 1) + - '_()'; + '_()})'; } if (filename.startsWith('/')) { @@ -1903,7 +1945,7 @@ function createFakeFunction( let fn: FakeFunction; try { // eslint-disable-next-line no-eval - fn = (0, eval)(code); + fn = (0, eval)(code)[name]; } catch (x) { // If eval fails, such as if in an environment that doesn't support it, // we fallback to creating a function here. It'll still have the right @@ -1912,10 +1954,6 @@ function createFakeFunction( return _(); }; } - // $FlowFixMe[cannot-write] - Object.defineProperty(fn, 'name', {value: name || '(anonymous)'}); - // $FlowFixMe[prop-missing] - fn.displayName = name; return fn; } @@ -2034,6 +2072,10 @@ function resolveConsoleEntry( ); } + if (!response._replayConsole) { + return; + } + const payload: [string, string, null | ReactComponentInfo, string, mixed] = parseModel(response, value); const methodName = payload[0]; @@ -2058,7 +2100,7 @@ function resolveConsoleEntry( task.run(callStack); return; } - // TODO: Set the current owner so that consoleWithStackDev adds the component + // TODO: Set the current owner so that captureOwnerStack() adds the component // stack during the replay - if needed. } const rootTask = response._debugRootTask; @@ -2121,7 +2163,7 @@ function resolveTypedArray( resolveBuffer(response, id, view); } -function processFullRow( +function processFullBinaryRow( response: Response, id: number, tag: number, @@ -2183,6 +2225,15 @@ function processFullRow( row += readPartialStringChunk(stringDecoder, buffer[i]); } row += readFinalStringChunk(stringDecoder, chunk); + processFullStringRow(response, id, tag, row); +} + +function processFullStringRow( + response: Response, + id: number, + tag: number, + row: string, +): void { switch (tag) { case 73 /* "I" */: { resolveModule(response, id, row); @@ -2385,7 +2436,7 @@ export function processBinaryChunk( // We found the last chunk of the row const length = lastIdx - i; const lastChunk = new Uint8Array(chunk.buffer, offset, length); - processFullRow(response, rowID, rowTag, buffer, lastChunk); + processFullBinaryRow(response, rowID, rowTag, buffer, lastChunk); // Reset state machine for a new row i = lastIdx; if (rowState === ROW_CHUNK_BY_NEWLINE) { @@ -2415,6 +2466,151 @@ export function processBinaryChunk( response._rowLength = rowLength; } +export function processStringChunk(response: Response, chunk: string): void { + // This is a fork of processBinaryChunk that takes a string as input. + // This can't be just any binary chunk coverted to a string. It needs to be + // in the same offsets given from the Flight Server. E.g. if it's shifted by + // one byte then it won't line up to the UCS-2 encoding. It also needs to + // be valid Unicode. Also binary chunks cannot use this even if they're + // value Unicode. Large strings are encoded as binary and cannot be passed + // here. Basically, only if Flight Server gave you this string as a chunk, + // you can use it here. + let i = 0; + let rowState = response._rowState; + let rowID = response._rowID; + let rowTag = response._rowTag; + let rowLength = response._rowLength; + const buffer = response._buffer; + const chunkLength = chunk.length; + while (i < chunkLength) { + let lastIdx = -1; + switch (rowState) { + case ROW_ID: { + const byte = chunk.charCodeAt(i++); + if (byte === 58 /* ":" */) { + // Finished the rowID, next we'll parse the tag. + rowState = ROW_TAG; + } else { + rowID = (rowID << 4) | (byte > 96 ? byte - 87 : byte - 48); + } + continue; + } + case ROW_TAG: { + const resolvedRowTag = chunk.charCodeAt(i); + if ( + resolvedRowTag === 84 /* "T" */ || + (enableBinaryFlight && + (resolvedRowTag === 65 /* "A" */ || + resolvedRowTag === 79 /* "O" */ || + resolvedRowTag === 111 /* "o" */ || + resolvedRowTag === 85 /* "U" */ || + resolvedRowTag === 83 /* "S" */ || + resolvedRowTag === 115 /* "s" */ || + resolvedRowTag === 76 /* "L" */ || + resolvedRowTag === 108 /* "l" */ || + resolvedRowTag === 71 /* "G" */ || + resolvedRowTag === 103 /* "g" */ || + resolvedRowTag === 77 /* "M" */ || + resolvedRowTag === 109 /* "m" */ || + resolvedRowTag === 86)) /* "V" */ + ) { + rowTag = resolvedRowTag; + rowState = ROW_LENGTH; + i++; + } else if ( + (resolvedRowTag > 64 && resolvedRowTag < 91) /* "A"-"Z" */ || + resolvedRowTag === 114 /* "r" */ || + resolvedRowTag === 120 /* "x" */ + ) { + rowTag = resolvedRowTag; + rowState = ROW_CHUNK_BY_NEWLINE; + i++; + } else { + rowTag = 0; + rowState = ROW_CHUNK_BY_NEWLINE; + // This was an unknown tag so it was probably part of the data. + } + continue; + } + case ROW_LENGTH: { + const byte = chunk.charCodeAt(i++); + if (byte === 44 /* "," */) { + // Finished the rowLength, next we'll buffer up to that length. + rowState = ROW_CHUNK_BY_LENGTH; + } else { + rowLength = (rowLength << 4) | (byte > 96 ? byte - 87 : byte - 48); + } + continue; + } + case ROW_CHUNK_BY_NEWLINE: { + // We're looking for a newline + lastIdx = chunk.indexOf('\n', i); + break; + } + case ROW_CHUNK_BY_LENGTH: { + if (rowTag !== 84) { + throw new Error( + 'Binary RSC chunks cannot be encoded as strings. ' + + 'This is a bug in the wiring of the React streams.', + ); + } + // For a large string by length, we don't know how many unicode characters + // we are looking for but we can assume that the raw string will be its own + // chunk. We add extra validation that the length is at least within the + // possible byte range it could possibly be to catch mistakes. + if (rowLength < chunk.length || chunk.length > rowLength * 3) { + throw new Error( + 'String chunks need to be passed in their original shape. ' + + 'Not split into smaller string chunks. ' + + 'This is a bug in the wiring of the React streams.', + ); + } + lastIdx = chunk.length; + break; + } + } + if (lastIdx > -1) { + // We found the last chunk of the row + if (buffer.length > 0) { + // If we had a buffer already, it means that this chunk was split up into + // binary chunks preceeding it. + throw new Error( + 'String chunks need to be passed in their original shape. ' + + 'Not split into smaller string chunks. ' + + 'This is a bug in the wiring of the React streams.', + ); + } + const lastChunk = chunk.slice(i, lastIdx); + processFullStringRow(response, rowID, rowTag, lastChunk); + // Reset state machine for a new row + i = lastIdx; + if (rowState === ROW_CHUNK_BY_NEWLINE) { + // If we're trailing by a newline we need to skip it. + i++; + } + rowState = ROW_ID; + rowTag = 0; + rowID = 0; + rowLength = 0; + buffer.length = 0; + } else if (chunk.length !== i) { + // The rest of this row is in a future chunk. We only support passing the + // string from chunks in their entirety. Not split up into smaller string chunks. + // We could support this by buffering them but we shouldn't need to for + // this use case. + throw new Error( + 'String chunks need to be passed in their original shape. ' + + 'Not split into smaller string chunks. ' + + 'This is a bug in the wiring of the React streams.', + ); + } + } + response._rowState = rowState; + response._rowID = rowID; + response._rowTag = rowTag; + response._rowLength = rowLength; +} + function parseModel(response: Response, json: UninitializedModel): T { return JSON.parse(json, response._fromJSON); } diff --git a/packages/react-client/src/ReactFlightReplyClient.js b/packages/react-client/src/ReactFlightReplyClient.js index ff80f4b310..c4033a999d 100644 --- a/packages/react-client/src/ReactFlightReplyClient.js +++ b/packages/react-client/src/ReactFlightReplyClient.js @@ -692,7 +692,8 @@ export function processReply( if (temporaryReferences === undefined) { throw new Error( 'Only plain objects, and a few built-ins, can be passed to Server Actions. ' + - 'Classes or null prototypes are not supported.', + 'Classes or null prototypes are not supported.' + + (__DEV__ ? describeObjectForErrorMessage(parent, key) : ''), ); } // We will have written this object to the temporary reference set above diff --git a/packages/react-client/src/__tests__/ReactFlight-test.js b/packages/react-client/src/__tests__/ReactFlight-test.js index 2e3857a90d..e48c926a91 100644 --- a/packages/react-client/src/__tests__/ReactFlight-test.js +++ b/packages/react-client/src/__tests__/ReactFlight-test.js @@ -81,6 +81,7 @@ let ErrorBoundary; let NoErrorExpected; let Scheduler; let assertLog; +let assertConsoleErrorDev; describe('ReactFlight', () => { beforeEach(() => { @@ -102,6 +103,7 @@ describe('ReactFlight', () => { Scheduler = require('scheduler'); const InternalTestUtils = require('internal-test-utils'); assertLog = InternalTestUtils.assertLog; + assertConsoleErrorDev = InternalTestUtils.assertConsoleErrorDev; ErrorBoundary = class extends React.Component { state = {hasError: false, error: null}; @@ -1434,16 +1436,25 @@ describe('ReactFlight', () => { it('should warn in DEV a child is missing keys on server component', () => { function NoKey({children}) { - return
; + return ReactServer.createElement('div', { + key: "this has a key but parent doesn't", + }); } expect(() => { + // While we're on the server we need to have the Server version active to track component stacks. + jest.resetModules(); + jest.mock('react', () => ReactServer); const transport = ReactNoopFlightServer.render( -
{Array(6).fill()}
, + ReactServer.createElement( + 'div', + null, + Array(6).fill(ReactServer.createElement(NoKey)), + ), ); + jest.resetModules(); + jest.mock('react', () => React); ReactNoopFlightClient.read(transport); - }).toErrorDev('Each child in a list should have a unique "key" prop.', { - withoutStack: gate(flags => flags.enableOwnerStacks), - }); + }).toErrorDev('Each child in a list should have a unique "key" prop.'); }); it('should warn in DEV a child is missing keys in client component', async () => { @@ -1482,7 +1493,10 @@ describe('ReactFlight', () => { expect(errors).toEqual([ 'Only plain objects, and a few built-ins, can be passed to Client Components ' + - 'from Server Components. Classes or null prototypes are not supported.', + 'from Server Components. Classes or null prototypes are not supported.' + + (__DEV__ + ? '\n' + ' \n' + ' ^^^^' + : '\n' + ' {value: {}}\n' + ' ^^'), ]); }); @@ -2625,7 +2639,7 @@ describe('ReactFlight', () => { return 'hello'; } function ServerComponent() { - console.log('hi', {prop: 123, fn: foo}); + console.log('hi', {prop: 123, fn: foo, map: new Map([['foo', foo]])}); throw new Error('err'); } @@ -2667,6 +2681,13 @@ describe('ReactFlight', () => { expect(typeof loggedFn).toBe('function'); expect(loggedFn).not.toBe(foo); expect(loggedFn.toString()).toBe(foo.toString()); + + const loggedMap = mockConsoleLog.mock.calls[0][1].map; + expect(loggedMap instanceof Map).toBe(true); + const loggedFn2 = loggedMap.get('foo'); + expect(typeof loggedFn2).toBe('function'); + expect(loggedFn2).not.toBe(foo); + expect(loggedFn2.toString()).toBe(foo.toString()); }); it('uses the server component debug info as the element owner in DEV', async () => { @@ -2728,4 +2749,142 @@ describe('ReactFlight', () => { expect(ReactNoop).toMatchRenderedOutput(Hello, Seb); }); + + // @gate __DEV__ && enableOwnerStacks + it('can get the component owner stacks during rendering in dev', () => { + let stack; + + function Foo() { + return ReactServer.createElement(Bar, null); + } + function Bar() { + return ReactServer.createElement( + 'div', + null, + ReactServer.createElement(Baz, null), + ); + } + + function Baz() { + stack = ReactServer.captureOwnerStack(); + return ReactServer.createElement('span', null, 'hi'); + } + ReactNoopFlightServer.render( + ReactServer.createElement( + 'div', + null, + ReactServer.createElement(Foo, null), + ), + ); + + expect(normalizeCodeLocInfo(stack)).toBe( + '\n in Bar (at **)' + '\n in Foo (at **)', + ); + }); + + // @gate __DEV__ && enableOwnerStacks + it('can get the component owner stacks for onError in dev', async () => { + const thrownError = new Error('hi'); + let caughtError; + let ownerStack; + + function Foo() { + return ReactServer.createElement(Bar, null); + } + function Bar() { + return ReactServer.createElement( + 'div', + null, + ReactServer.createElement(Baz, null), + ); + } + function Baz() { + throw thrownError; + } + + ReactNoopFlightServer.render( + ReactServer.createElement( + 'div', + null, + ReactServer.createElement(Foo, null), + ), + { + onError(error, errorInfo) { + caughtError = error; + ownerStack = ReactServer.captureOwnerStack + ? ReactServer.captureOwnerStack() + : null; + }, + }, + ); + + expect(caughtError).toBe(thrownError); + expect(normalizeCodeLocInfo(ownerStack)).toBe( + '\n in Bar (at **)' + '\n in Foo (at **)', + ); + }); + + // @gate (enableOwnerStacks && enableServerComponentLogs) || !__DEV__ + it('should include only one component stack in replayed logs (if DevTools or polyfill adds them)', () => { + class MyError extends Error { + toJSON() { + return 123; + } + } + + function Foo() { + return ReactServer.createElement('div', null, [ + 'Womp womp: ', + new MyError('spaghetti'), + ]); + } + + function Bar() { + const array = []; + // Trigger key warning + array.push(ReactServer.createElement(Foo)); + return ReactServer.createElement('div', null, array); + } + + function App() { + return ReactServer.createElement(Bar); + } + + // While we're on the server we need to have the Server version active to track component stacks. + jest.resetModules(); + jest.mock('react', () => ReactServer); + const transport = ReactNoopFlightServer.render( + ReactServer.createElement(App), + ); + + assertConsoleErrorDev([ + 'Each child in a list should have a unique "key" prop.' + + ' See https://react.dev/link/warning-keys for more information.\n' + + ' in Bar (at **)\n' + + ' in App (at **)', + 'Error objects cannot be rendered as text children. Try formatting it using toString().\n' + + '
Womp womp: {Error}
\n' + + ' ^^^^^^^\n' + + ' in Foo (at **)\n' + + ' in Bar (at **)\n' + + ' in App (at **)', + ]); + + // Replay logs on the client + jest.resetModules(); + jest.mock('react', () => React); + ReactNoopFlightClient.read(transport); + assertConsoleErrorDev( + [ + 'Each child in a list should have a unique "key" prop.' + + ' See https://react.dev/link/warning-keys for more information.', + 'Error objects cannot be rendered as text children. Try formatting it using toString().\n' + + '
Womp womp: {Error}
\n' + + ' ^^^^^^^', + ], + // We should have a stack in the replay but we don't yet set the owner from the Flight replaying + // so our simulated polyfill doesn't end up getting any component stacks yet. + {withoutStack: true}, + ); + }); }); diff --git a/packages/react-debug-tools/src/ReactDebugHooks.js b/packages/react-debug-tools/src/ReactDebugHooks.js index 09ba351235..d7ffc6626c 100644 --- a/packages/react-debug-tools/src/ReactDebugHooks.js +++ b/packages/react-debug-tools/src/ReactDebugHooks.js @@ -774,6 +774,7 @@ const Dispatcher: DispatcherType = { const DispatcherProxyHandler = { get(target: DispatcherType, prop: string) { if (target.hasOwnProperty(prop)) { + // $FlowFixMe[invalid-computed-prop] return target[prop]; } const error = new Error('Missing method in Dispatcher: ' + prop); diff --git a/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js b/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js index 0c54464b72..47d47926cf 100644 --- a/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js +++ b/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js @@ -11,7 +11,6 @@ 'use strict'; let React; -let ReactDOM; let ReactTestRenderer; let ReactDebugTools; let act; @@ -34,7 +33,6 @@ describe('ReactHooksInspectionIntegration', () => { jest.resetModules(); React = require('react'); ReactTestRenderer = require('react-test-renderer'); - ReactDOM = require('react-dom'); act = require('internal-test-utils').act; ReactDebugTools = require('react-debug-tools'); useMemoCache = require('react/compiler-runtime').c; @@ -2658,9 +2656,9 @@ describe('ReactHooksInspectionIntegration', () => { }); // @gate enableAsyncActions - it('should support useFormState hook', async () => { + it('should support useActionState hook', async () => { function Foo() { - const [value] = ReactDOM.useFormState(function increment(n) { + const [value] = React.useActionState(function increment(n) { return n; }, 0); React.useMemo(() => 'memo', []); @@ -2689,7 +2687,7 @@ describe('ReactHooksInspectionIntegration', () => { }, "id": 0, "isStateEditable": false, - "name": "FormState", + "name": "ActionState", "subHooks": [], "value": 0, }, diff --git a/packages/react-devtools-core/package.json b/packages/react-devtools-core/package.json index 3670512f04..4c7f54775f 100644 --- a/packages/react-devtools-core/package.json +++ b/packages/react-devtools-core/package.json @@ -1,6 +1,6 @@ { "name": "react-devtools-core", - "version": "5.3.0", + "version": "5.3.1", "description": "Use react-devtools outside of the browser", "license": "MIT", "main": "./dist/backend.js", diff --git a/packages/react-devtools-core/webpack.backend.js b/packages/react-devtools-core/webpack.backend.js index 24e3ced0d7..7efd5b0b5b 100644 --- a/packages/react-devtools-core/webpack.backend.js +++ b/packages/react-devtools-core/webpack.backend.js @@ -69,6 +69,8 @@ module.exports = { __PROFILE__: false, __TEST__: NODE_ENV === 'test', __IS_FIREFOX__: false, + __IS_CHROME__: false, + __IS_EDGE__: false, 'process.env.DEVTOOLS_PACKAGE': `"react-devtools-core"`, 'process.env.DEVTOOLS_VERSION': `"${DEVTOOLS_VERSION}"`, 'process.env.GITHUB_URL': `"${GITHUB_URL}"`, diff --git a/packages/react-devtools-extensions/chrome/manifest.json b/packages/react-devtools-extensions/chrome/manifest.json index 99c9d47fd0..4dcd951a48 100644 --- a/packages/react-devtools-extensions/chrome/manifest.json +++ b/packages/react-devtools-extensions/chrome/manifest.json @@ -2,8 +2,8 @@ "manifest_version": 3, "name": "React Developer Tools", "description": "Adds React debugging tools to the Chrome Developer Tools.", - "version": "5.3.0", - "version_name": "5.3.0", + "version": "5.3.1", + "version_name": "5.3.1", "minimum_chrome_version": "102", "icons": { "16": "icons/16-production.png", diff --git a/packages/react-devtools-extensions/edge/manifest.json b/packages/react-devtools-extensions/edge/manifest.json index 77c4f059eb..fd19f1c5df 100644 --- a/packages/react-devtools-extensions/edge/manifest.json +++ b/packages/react-devtools-extensions/edge/manifest.json @@ -2,8 +2,8 @@ "manifest_version": 3, "name": "React Developer Tools", "description": "Adds React debugging tools to the Microsoft Edge Developer Tools.", - "version": "5.3.0", - "version_name": "5.3.0", + "version": "5.3.1", + "version_name": "5.3.1", "minimum_chrome_version": "102", "icons": { "16": "icons/16-production.png", diff --git a/packages/react-devtools-extensions/firefox/manifest.json b/packages/react-devtools-extensions/firefox/manifest.json index 70639fae88..ffa48634e0 100644 --- a/packages/react-devtools-extensions/firefox/manifest.json +++ b/packages/react-devtools-extensions/firefox/manifest.json @@ -2,7 +2,7 @@ "manifest_version": 2, "name": "React Developer Tools", "description": "Adds React debugging tools to the Firefox Developer Tools.", - "version": "5.3.0", + "version": "5.3.1", "applications": { "gecko": { "id": "@react-devtools", diff --git a/packages/react-devtools-inline/package.json b/packages/react-devtools-inline/package.json index b6c0bd0c9b..ead4c18380 100644 --- a/packages/react-devtools-inline/package.json +++ b/packages/react-devtools-inline/package.json @@ -1,6 +1,6 @@ { "name": "react-devtools-inline", - "version": "5.3.0", + "version": "5.3.1", "description": "Embed react-devtools within a website", "license": "MIT", "main": "./dist/backend.js", diff --git a/packages/react-devtools-inline/webpack.config.js b/packages/react-devtools-inline/webpack.config.js index 2ab8db739a..7b153bbc13 100644 --- a/packages/react-devtools-inline/webpack.config.js +++ b/packages/react-devtools-inline/webpack.config.js @@ -73,6 +73,10 @@ module.exports = { __EXTENSION__: false, __PROFILE__: false, __TEST__: NODE_ENV === 'test', + // TODO: Should this be feature tested somehow? + __IS_CHROME__: false, + __IS_FIREFOX__: false, + __IS_EDGE__: false, 'process.env.DEVTOOLS_PACKAGE': `"react-devtools-inline"`, 'process.env.DEVTOOLS_VERSION': `"${DEVTOOLS_VERSION}"`, 'process.env.EDITOR_URL': EDITOR_URL != null ? `"${EDITOR_URL}"` : null, diff --git a/packages/react-devtools-shared/src/__tests__/console-test.js b/packages/react-devtools-shared/src/__tests__/console-test.js index c79850901a..75e7dc1c87 100644 --- a/packages/react-devtools-shared/src/__tests__/console-test.js +++ b/packages/react-devtools-shared/src/__tests__/console-test.js @@ -1000,7 +1000,7 @@ describe('console', () => { ); expect(mockWarn.mock.calls[1]).toHaveLength(3); expect(mockWarn.mock.calls[1][0]).toEqual( - '\x1b[2;38;2;124;124;124m%s %s\x1b[0m', + '\x1b[2;38;2;124;124;124m%s %o\x1b[0m', ); expect(mockWarn.mock.calls[1][1]).toMatch('warn'); expect(normalizeCodeLocInfo(mockWarn.mock.calls[1][2]).trim()).toEqual( @@ -1014,7 +1014,7 @@ describe('console', () => { ); expect(mockError.mock.calls[1]).toHaveLength(3); expect(mockError.mock.calls[1][0]).toEqual( - '\x1b[2;38;2;124;124;124m%s %s\x1b[0m', + '\x1b[2;38;2;124;124;124m%s %o\x1b[0m', ); expect(mockError.mock.calls[1][1]).toEqual('error'); expect(normalizeCodeLocInfo(mockError.mock.calls[1][2]).trim()).toEqual( diff --git a/packages/react-devtools-shared/src/__tests__/storeStressTestConcurrent-test.js b/packages/react-devtools-shared/src/__tests__/storeStressTestConcurrent-test.js index 472ad31671..8dd4ce4284 100644 --- a/packages/react-devtools-shared/src/__tests__/storeStressTestConcurrent-test.js +++ b/packages/react-devtools-shared/src/__tests__/storeStressTestConcurrent-test.js @@ -16,6 +16,8 @@ describe('StoreStressConcurrent', () => { let store; let print; + jest.setTimeout(15000); + beforeEach(() => { global.IS_REACT_ACT_ENVIRONMENT = true; diff --git a/packages/react-devtools-shared/src/__tests__/utils-test.js b/packages/react-devtools-shared/src/__tests__/utils-test.js index 9e781e072b..f35cacc733 100644 --- a/packages/react-devtools-shared/src/__tests__/utils-test.js +++ b/packages/react-devtools-shared/src/__tests__/utils-test.js @@ -26,6 +26,7 @@ import { REACT_STRICT_MODE_TYPE as StrictMode, } from 'shared/ReactSymbols'; import {createElement} from 'react'; +import {symbolicateSource} from '../symbolicateSource'; describe('utils', () => { describe('getDisplayName', () => { @@ -385,6 +386,35 @@ describe('utils', () => { }); }); + describe('symbolicateSource', () => { + const source = `"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.f = f; +function f() { } +//# sourceMappingURL=`; + const result = { + column: 16, + line: 1, + sourceURL: 'http://test/a.mts', + }; + const fs = { + 'http://test/a.mts': `export function f() {}`, + 'http://test/a.mjs.map': `{"version":3,"file":"a.mjs","sourceRoot":"","sources":["a.mts"],"names":[],"mappings":";;AAAA,cAAsB;AAAtB,SAAgB,CAAC,KAAI,CAAC"}`, + 'http://test/a.mjs': `${source}a.mjs.map`, + 'http://test/b.mjs': `${source}./a.mjs.map`, + 'http://test/c.mjs': `${source}http://test/a.mjs.map`, + 'http://test/d.mjs': `${source}/a.mjs.map`, + }; + const fetchFileWithCaching = async (url: string) => fs[url] || null; + it('should parse source map urls', async () => { + const run = url => symbolicateSource(fetchFileWithCaching, url, 4, 10); + await expect(run('http://test/a.mjs')).resolves.toStrictEqual(result); + await expect(run('http://test/b.mjs')).resolves.toStrictEqual(result); + await expect(run('http://test/c.mjs')).resolves.toStrictEqual(result); + await expect(run('http://test/d.mjs')).resolves.toStrictEqual(result); + }); + }); + describe('formatConsoleArguments', () => { it('works with empty arguments list', () => { expect(formatConsoleArguments(...[])).toEqual([]); diff --git a/packages/react-devtools-shared/src/__tests__/utils.js b/packages/react-devtools-shared/src/__tests__/utils.js index c0c472e681..4a42cf2703 100644 --- a/packages/react-devtools-shared/src/__tests__/utils.js +++ b/packages/react-devtools-shared/src/__tests__/utils.js @@ -463,6 +463,9 @@ export function overrideFeatureFlags(overrideFlags) { } export function normalizeCodeLocInfo(str) { + if (typeof str === 'object' && str !== null) { + str = str.stack; + } if (typeof str !== 'string') { return str; } diff --git a/packages/react-devtools-shared/src/backend/DevToolsComponentStackFrame.js b/packages/react-devtools-shared/src/backend/DevToolsComponentStackFrame.js index e20fb85e3c..e42073cc02 100644 --- a/packages/react-devtools-shared/src/backend/DevToolsComponentStackFrame.js +++ b/packages/react-devtools-shared/src/backend/DevToolsComponentStackFrame.js @@ -29,12 +29,19 @@ export function describeBuiltInComponentFrame(name: string): string { prefix = (match && match[1]) || ''; } } + let suffix = ''; + if (__IS_CHROME__ || __IS_EDGE__) { + suffix = ' ()'; + } else if (__IS_FIREFOX__) { + suffix = '@unknown:0:0'; + } // We use the prefix to ensure our stacks line up with native stack frames. - return '\n' + prefix + name; + // We use a suffix to ensure it gets parsed natively. + return '\n' + prefix + name + suffix; } export function describeDebugInfoFrame(name: string, env: ?string): string { - return describeBuiltInComponentFrame(name + (env ? ' (' + env + ')' : '')); + return describeBuiltInComponentFrame(name + (env ? ' [' + env + ']' : '')); } let reentry = false; diff --git a/packages/react-devtools-shared/src/backend/DevToolsFiberComponentStack.js b/packages/react-devtools-shared/src/backend/DevToolsFiberComponentStack.js index a4311797de..1e64b3e15f 100644 --- a/packages/react-devtools-shared/src/backend/DevToolsFiberComponentStack.js +++ b/packages/react-devtools-shared/src/backend/DevToolsFiberComponentStack.js @@ -28,6 +28,8 @@ export function describeFiber( currentDispatcherRef: CurrentDispatcherRef, ): string { const { + HostHoistable, + HostSingleton, HostComponent, LazyComponent, SuspenseComponent, @@ -40,9 +42,12 @@ export function describeFiber( } = workTagMap; switch (workInProgress.tag) { + case HostHoistable: + case HostSingleton: case HostComponent: return describeBuiltInComponentFrame(workInProgress.type); case LazyComponent: + // TODO: When we support Thenables as component types we should rename this. return describeBuiltInComponentFrame('Lazy'); case SuspenseComponent: return describeBuiltInComponentFrame('Suspense'); diff --git a/packages/react-devtools-shared/src/backend/console.js b/packages/react-devtools-shared/src/backend/console.js index e1b98b9190..227298da2b 100644 --- a/packages/react-devtools-shared/src/backend/console.js +++ b/packages/react-devtools-shared/src/backend/console.js @@ -63,6 +63,15 @@ function isStrictModeOverride(args: Array): boolean { } } +// We add a suffix to some frames that older versions of React didn't do. +// To compare if it's equivalent we strip out the suffix to see if they're +// still equivalent. Similarly, we sometimes use [] and sometimes () so we +// strip them to for the comparison. +const frameDiffs = / \(\\)$|\@unknown\:0\:0$|\(|\)|\[|\]/gm; +function areStackTracesEqual(a: string, b: string): boolean { + return a.replace(frameDiffs, '') === b.replace(frameDiffs, ''); +} + function restorePotentiallyModifiedArgs(args: Array): Array { // If the arguments don't have any styles applied, then just copy if (!isStrictModeOverride(args)) { @@ -97,6 +106,7 @@ const injectedRenderers: Map< let targetConsole: Object = console; let targetConsoleMethods: {[string]: $FlowFixMe} = {}; for (const method in console) { + // $FlowFixMe[invalid-computed-prop] targetConsoleMethods[method] = console[method]; } @@ -110,6 +120,7 @@ export function dangerous_setTargetConsoleForTesting( targetConsoleMethods = ({}: {[string]: $FlowFixMe}); for (const method in targetConsole) { + // $FlowFixMe[invalid-computed-prop] targetConsoleMethods[method] = console[method]; } } @@ -202,17 +213,11 @@ export function patch({ // $FlowFixMe[missing-local-annot] const overrideMethod = (...args) => { - let shouldAppendWarningStack = false; - if (method !== 'log') { - if (consoleSettingsRef.appendComponentStack) { - const lastArg = args.length > 0 ? args[args.length - 1] : null; - const alreadyHasComponentStack = - typeof lastArg === 'string' && isStringComponentStack(lastArg); - - // If we are ever called with a string that already has a component stack, - // e.g. a React error/warning, don't append a second stack. - shouldAppendWarningStack = !alreadyHasComponentStack; - } + let alreadyHasComponentStack = false; + if (method !== 'log' && consoleSettingsRef.appendComponentStack) { + const lastArg = args.length > 0 ? args[args.length - 1] : null; + alreadyHasComponentStack = + typeof lastArg === 'string' && isStringComponentStack(lastArg); // The last argument should be a component stack. } const shouldShowInlineWarningsAndErrors = @@ -242,7 +247,7 @@ export function patch({ } if ( - shouldAppendWarningStack && + consoleSettingsRef.appendComponentStack && !supportsNativeConsoleTasks(current) ) { const componentStack = getStackByFiberInDevAndProd( @@ -251,17 +256,55 @@ export function patch({ (currentDispatcherRef: any), ); if (componentStack !== '') { - if (isStrictModeOverride(args)) { - if (__IS_FIREFOX__) { - args[0] = `${args[0]} %s`; - args.push(componentStack); - } else { - args[0] = - ANSI_STYLE_DIMMING_TEMPLATE_WITH_COMPONENT_STACK; - args.push(componentStack); + // Create a fake Error so that when we print it we get native source maps. Every + // browser will print the .stack property of the error and then parse it back for source + // mapping. Rather than print the internal slot. So it doesn't matter that the internal + // slot doesn't line up. + const fakeError = new Error(''); + // In Chromium, only the stack property is printed but in Firefox the : + // gets printed so to make the colon make sense, we name it so we print Component Stack: + // and similarly Safari leave an expandable slot. + fakeError.name = 'Component Stack'; // This gets printed + // In Chromium, the stack property needs to start with ^[\w.]*Error\b to trigger stack + // formatting. Otherwise it is left alone. So we prefix it. Otherwise we just override it + // to our own stack. + fakeError.stack = + __IS_CHROME__ || __IS_EDGE__ + ? 'Error Component Stack:' + componentStack + : componentStack; + if (alreadyHasComponentStack) { + // Only modify the component stack if it matches what we would've added anyway. + // Otherwise we assume it was a non-React stack. + if (isStrictModeOverride(args)) { + // We do nothing to Strict Mode overrides that already has a stack + // because we have already lost some context for how to format it + // since we've already merged the stack into the log at this point. + } else if ( + areStackTracesEqual( + args[args.length - 1], + componentStack, + ) + ) { + const firstArg = args[0]; + if ( + args.length > 1 && + typeof firstArg === 'string' && + firstArg.endsWith('%s') + ) { + args[0] = firstArg.slice(0, firstArg.length - 2); // Strip the %s param + } + args[args.length - 1] = fakeError; } } else { - args.push(componentStack); + args.push(fakeError); + if (isStrictModeOverride(args)) { + if (__IS_FIREFOX__) { + args[0] = `${args[0]} %o`; + } else { + args[0] = + ANSI_STYLE_DIMMING_TEMPLATE_WITH_COMPONENT_STACK; + } + } } } } diff --git a/packages/react-devtools-shared/src/backend/renderer.js b/packages/react-devtools-shared/src/backend/renderer.js index 59af004fb3..62bf7fe4a7 100644 --- a/packages/react-devtools-shared/src/backend/renderer.js +++ b/packages/react-devtools-shared/src/backend/renderer.js @@ -3391,6 +3391,7 @@ export function attach( // Temporarily disable all console logging before re-running the hook. for (const method in console) { try { + // $FlowFixMe[invalid-computed-prop] originalConsoleMethods[method] = console[method]; // $FlowFixMe[prop-missing] console[method] = () => {}; diff --git a/packages/react-devtools-shared/src/constants.js b/packages/react-devtools-shared/src/constants.js index 303ae50288..52d39f2d90 100644 --- a/packages/react-devtools-shared/src/constants.js +++ b/packages/react-devtools-shared/src/constants.js @@ -62,4 +62,4 @@ export const PROFILER_EXPORT_VERSION = 5; export const FIREFOX_CONSOLE_DIMMING_COLOR = 'color: rgba(124, 124, 124, 0.75)'; export const ANSI_STYLE_DIMMING_TEMPLATE = '\x1b[2;38;2;124;124;124m%s\x1b[0m'; export const ANSI_STYLE_DIMMING_TEMPLATE_WITH_COMPONENT_STACK = - '\x1b[2;38;2;124;124;124m%s %s\x1b[0m'; + '\x1b[2;38;2;124;124;124m%s %o\x1b[0m'; diff --git a/packages/react-devtools-shared/src/hook.js b/packages/react-devtools-shared/src/hook.js index 0dd96dc471..c16409e3d9 100644 --- a/packages/react-devtools-shared/src/hook.js +++ b/packages/react-devtools-shared/src/hook.js @@ -32,6 +32,7 @@ export function installHook(target: any): DevToolsHook | null { let targetConsole: Object = console; let targetConsoleMethods: {[string]: $FlowFixMe} = {}; for (const method in console) { + // $FlowFixMe[invalid-computed-prop] targetConsoleMethods[method] = console[method]; } @@ -42,6 +43,7 @@ export function installHook(target: any): DevToolsHook | null { targetConsoleMethods = ({}: {[string]: $FlowFixMe}); for (const method in targetConsole) { + // $FlowFixMe[invalid-computed-prop] targetConsoleMethods[method] = console[method]; } } diff --git a/packages/react-devtools-shared/src/hooks/SourceMapConsumer.js b/packages/react-devtools-shared/src/hooks/SourceMapConsumer.js index 779cbb3b2f..8e6503685b 100644 --- a/packages/react-devtools-shared/src/hooks/SourceMapConsumer.js +++ b/packages/react-devtools-shared/src/hooks/SourceMapConsumer.js @@ -24,8 +24,8 @@ type SearchPosition = { type ResultPosition = { column: number, line: number, - sourceContent: string, - sourceURL: string, + sourceContent: string | null, + sourceURL: string | null, }; export type SourceMapConsumerType = { @@ -118,18 +118,11 @@ function BasicSourceMapConsumer(sourceMapJSON: BasicSourceMap) { const line = nearestEntry[2] + 1; const column = nearestEntry[3]; - if (sourceContent === null || sourceURL === null) { - // TODO maybe fall back to the runtime source instead of throwing? - throw Error( - `Could not find original source for line:${lineNumber} and column:${columnNumber}`, - ); - } - return { column, line, - sourceContent: ((sourceContent: any): string), - sourceURL: ((sourceURL: any): string), + sourceContent: ((sourceContent: any): string | null), + sourceURL: ((sourceURL: any): string | null), }; } diff --git a/packages/react-devtools-shared/src/hooks/parseHookNames/parseSourceAndMetadata.js b/packages/react-devtools-shared/src/hooks/parseHookNames/parseSourceAndMetadata.js index 40bfed48ba..15423c1241 100644 --- a/packages/react-devtools-shared/src/hooks/parseHookNames/parseSourceAndMetadata.js +++ b/packages/react-devtools-shared/src/hooks/parseHookNames/parseSourceAndMetadata.js @@ -276,6 +276,11 @@ function parseSourceAST( columnNumber, lineNumber, }); + if (sourceContent === null || sourceURL === null) { + throw Error( + `Could not find original source for line:${lineNumber} and column:${columnNumber}`, + ); + } originalSourceColumnNumber = column; originalSourceLineNumber = line; diff --git a/packages/react-devtools-shared/src/symbolicateSource.js b/packages/react-devtools-shared/src/symbolicateSource.js index d28ed42e59..9430e88b3f 100644 --- a/packages/react-devtools-shared/src/symbolicateSource.js +++ b/packages/react-devtools-shared/src/symbolicateSource.js @@ -39,7 +39,7 @@ export async function symbolicateSourceWithCache( } const SOURCE_MAP_ANNOTATION_PREFIX = 'sourceMappingURL='; -async function symbolicateSource( +export async function symbolicateSource( fetchFileWithCaching: FetchFileWithCaching, sourceURL: string, lineNumber: number, // 1-based @@ -63,11 +63,12 @@ async function symbolicateSource( const sourceMapAnnotationStartIndex = resourceLine.indexOf( SOURCE_MAP_ANNOTATION_PREFIX, ); - const sourceMapURL = resourceLine.slice( + const sourceMapAt = resourceLine.slice( sourceMapAnnotationStartIndex + SOURCE_MAP_ANNOTATION_PREFIX.length, resourceLine.length, ); + const sourceMapURL = new URL(sourceMapAt, sourceURL).toString(); const sourceMap = await fetchFileWithCaching(sourceMapURL).catch( () => null, ); @@ -84,29 +85,33 @@ async function symbolicateSource( columnNumber, // 1-based }); + if (possiblyURL === null) { + return null; + } try { - void new URL(possiblyURL); // This is a valid URL + // sourceMapURL = https://react.dev/script.js.map + void new URL(possiblyURL); // test if it is a valid URL const normalizedURL = normalizeUrl(possiblyURL); return {sourceURL: normalizedURL, line, column}; } catch (e) { // This is not valid URL - if (possiblyURL.startsWith('/')) { + if ( + // sourceMapURL = /file + possiblyURL.startsWith('/') || + // sourceMapURL = C:\\... + possiblyURL.slice(1).startsWith(':\\\\') + ) { // This is an absolute path return {sourceURL: possiblyURL, line, column}; } // This is a relative path - const [sourceMapAbsolutePathWithoutQueryParameters] = - sourceMapURL.split(/[?#&]/); - - const absoluteSourcePath = - sourceMapAbsolutePathWithoutQueryParameters + - (sourceMapAbsolutePathWithoutQueryParameters.endsWith('/') - ? '' - : '/') + - possiblyURL; - + // possiblyURL = x.js.map, sourceMapURL = https://react.dev/script.js.map + const absoluteSourcePath = new URL( + possiblyURL, + sourceMapURL, + ).toString(); return {sourceURL: absoluteSourcePath, line, column}; } } catch (e) { diff --git a/packages/react-devtools-shared/src/utils.js b/packages/react-devtools-shared/src/utils.js index 5b0903883c..ffbc9e390d 100644 --- a/packages/react-devtools-shared/src/utils.js +++ b/packages/react-devtools-shared/src/utils.js @@ -1017,7 +1017,7 @@ export function backendToFrontendSerializedElementMapper( }; } -// This is a hacky one to just support this exact case. +// Chrome normalizes urls like webpack-internals:// but new URL don't, so cannot use new URL here. export function normalizeUrl(url: string): string { return url.replace('/./', '/'); } diff --git a/packages/react-devtools-timeline/package.json b/packages/react-devtools-timeline/package.json index da673cd03b..b9a8aab901 100644 --- a/packages/react-devtools-timeline/package.json +++ b/packages/react-devtools-timeline/package.json @@ -1,7 +1,7 @@ { "private": true, "name": "react-devtools-timeline", - "version": "5.3.0", + "version": "5.3.1", "license": "MIT", "dependencies": { "@elg/speedscope": "1.9.0-a6f84db", diff --git a/packages/react-devtools/CHANGELOG.md b/packages/react-devtools/CHANGELOG.md index e6d5525fd6..bfebd3ea0a 100644 --- a/packages/react-devtools/CHANGELOG.md +++ b/packages/react-devtools/CHANGELOG.md @@ -4,6 +4,17 @@ --- +### 5.3.1 +July 3, 2024 + +* chore[react-devtools/renderer]: dont show strict mode warning for prod renderer builds ([hoxyq](https://github.com/hoxyq) in [#30158](https://github.com/facebook/react/pull/30158)) +* chore[react-devtools/ui]: fix strict mode badge styles ([hoxyq](https://github.com/hoxyq) in [#30159](https://github.com/facebook/react/pull/30159)) +* fix[react-devtools]: restore original args when recording errors ([hoxyq](https://github.com/hoxyq) in [#30091](https://github.com/facebook/react/pull/30091)) +* Read constructor name more carefully ([LoganDark](https://github.com/LoganDark) in [#29954](https://github.com/facebook/react/pull/29954)) +* refactor[react-devtools/extensions]: dont debounce cleanup logic on navigation ([hoxyq](https://github.com/hoxyq) in [#30027](https://github.com/facebook/react/pull/30027)) + +--- + ### 5.3.0 June 17, 2024 diff --git a/packages/react-devtools/package.json b/packages/react-devtools/package.json index 37ed26fe64..d2a9ec6614 100644 --- a/packages/react-devtools/package.json +++ b/packages/react-devtools/package.json @@ -1,6 +1,6 @@ { "name": "react-devtools", - "version": "5.3.0", + "version": "5.3.1", "description": "Use react-devtools outside of the browser", "license": "MIT", "repository": { @@ -27,7 +27,7 @@ "electron": "^23.1.2", "internal-ip": "^6.2.0", "minimist": "^1.2.3", - "react-devtools-core": "5.3.0", + "react-devtools-core": "5.3.1", "update-notifier": "^2.1.0" } } diff --git a/packages/react-dom-bindings/src/client/DOMAccessibilityRoles.js b/packages/react-dom-bindings/src/client/DOMAccessibilityRoles.js index 0a6bbb4c5d..4c1091f245 100644 --- a/packages/react-dom-bindings/src/client/DOMAccessibilityRoles.js +++ b/packages/react-dom-bindings/src/client/DOMAccessibilityRoles.js @@ -60,6 +60,7 @@ const tagToRoleMappings = { }; function getImplicitRole(element: Element): string | null { + // $FlowFixMe[invalid-computed-prop] const mappedByTag = tagToRoleMappings[element.tagName]; if (mappedByTag !== undefined) { return mappedByTag; diff --git a/packages/react-dom-bindings/src/client/DOMPropertyOperations.js b/packages/react-dom-bindings/src/client/DOMPropertyOperations.js index 1d5211e451..da928058bb 100644 --- a/packages/react-dom-bindings/src/client/DOMPropertyOperations.js +++ b/packages/react-dom-bindings/src/client/DOMPropertyOperations.js @@ -196,6 +196,7 @@ export function setValueForPropertyOnCustomComponent( const eventName = name.slice(2, useCapture ? name.length - 7 : undefined); const prevProps = getFiberCurrentPropsFromNode(node); + // $FlowFixMe[invalid-computed-prop] const prevValue = prevProps != null ? prevProps[name] : null; if (typeof prevValue === 'function') { node.removeEventListener(eventName, prevValue, useCapture); diff --git a/packages/react-dom-bindings/src/client/ReactDOMComponent.js b/packages/react-dom-bindings/src/client/ReactDOMComponent.js index 160546d9b7..9f7b23026f 100644 --- a/packages/react-dom-bindings/src/client/ReactDOMComponent.js +++ b/packages/react-dom-bindings/src/client/ReactDOMComponent.js @@ -1031,6 +1031,53 @@ export function setInitialProperties( // Fast track the most common tag types break; } + // img tags previously were implemented as void elements with non delegated events however Safari (and possibly Firefox) + // begin fetching the image as soon as the `src` or `srcSet` property is set and if we set these before other properties + // that can modify the request (such as crossorigin) or the resource fetch (such as sizes) then the browser will load + // the wrong thing or load more than one thing. This implementation ensures src and srcSet are set on the instance last + case 'img': { + listenToNonDelegatedEvent('error', domElement); + listenToNonDelegatedEvent('load', domElement); + // Mostly a port of Void Element logic with special casing to ensure srcset and src are set last + let hasSrc = false; + let hasSrcSet = false; + for (const propKey in props) { + if (!props.hasOwnProperty(propKey)) { + continue; + } + const propValue = props[propKey]; + if (propValue == null) { + continue; + } + switch (propKey) { + case 'src': + hasSrc = true; + break; + case 'srcSet': + hasSrcSet = true; + break; + case 'children': + case 'dangerouslySetInnerHTML': { + // TODO: Can we make this a DEV warning to avoid this deny list? + throw new Error( + `${tag} is a void element tag and must neither have \`children\` nor ` + + 'use `dangerouslySetInnerHTML`.', + ); + } + // defaultChecked and defaultValue are ignored by setProp + default: { + setProp(domElement, tag, propKey, propValue, props, null); + } + } + } + if (hasSrcSet) { + setProp(domElement, tag, 'srcSet', props.srcSet, props, null); + } + if (hasSrc) { + setProp(domElement, tag, 'src', props.src, props, null); + } + return; + } case 'input': { if (__DEV__) { checkControlledValueProps('input', props); @@ -1269,7 +1316,6 @@ export function setInitialProperties( } case 'embed': case 'source': - case 'img': case 'link': { // These are void elements that also need delegated events. listenToNonDelegatedEvent('error', domElement); diff --git a/packages/react-dom-bindings/src/client/validateDOMNesting.js b/packages/react-dom-bindings/src/client/validateDOMNesting.js index 2f192cd18b..49d8b5c158 100644 --- a/packages/react-dom-bindings/src/client/validateDOMNesting.js +++ b/packages/react-dom-bindings/src/client/validateDOMNesting.js @@ -7,7 +7,54 @@ * @flow */ -import {getCurrentParentStackInDev} from 'react-reconciler/src/ReactCurrentFiber'; +import type {Fiber} from 'react-reconciler/src/ReactInternalTypes'; +import type {HydrationDiffNode} from 'react-reconciler/src/ReactFiberHydrationDiffs'; + +import {enableOwnerStacks} from 'shared/ReactFeatureFlags'; + +import { + current, + runWithFiberInDEV, +} from 'react-reconciler/src/ReactCurrentFiber'; +import { + HostComponent, + HostHoistable, + HostSingleton, + HostText, +} from 'react-reconciler/src/ReactWorkTags'; + +import {describeDiff} from 'react-reconciler/src/ReactFiberHydrationDiffs'; + +function describeAncestors( + ancestor: Fiber, + child: Fiber, + props: null | {children: null}, +): string { + let fiber: null | Fiber = child; + let node: null | HydrationDiffNode = null; + let distanceFromLeaf = 0; + while (fiber) { + if (fiber === ancestor) { + distanceFromLeaf = 0; + } + node = { + fiber: fiber, + children: node !== null ? [node] : [], + serverProps: + fiber === child ? props : fiber === ancestor ? null : undefined, + serverTail: [], + distanceFromLeaf: distanceFromLeaf, + }; + distanceFromLeaf++; + fiber = fiber.return; + } + if (node !== null) { + // Describe the node using the hydration diff logic. + // Replace + with - to mark ancestor and child. It's kind of arbitrary. + return describeDiff(node).replaceAll(/^[+-]/gm, '>'); + } + return ''; +} type Info = {tag: string}; export type AncestorInfoDev = { @@ -440,6 +487,21 @@ function findInvalidAncestorForTag( const didWarn: {[string]: boolean} = {}; +function findAncestor(parent: null | Fiber, tagName: string): null | Fiber { + while (parent) { + switch (parent.tag) { + case HostComponent: + case HostHoistable: + case HostSingleton: + if (parent.type === tagName) { + return parent; + } + } + parent = parent.return; + } + return null; +} + function validateDOMNesting( childTag: string, ancestorInfo: AncestorInfoDev, @@ -470,6 +532,14 @@ function validateDOMNesting( } didWarn[warnKey] = true; + const child = current; + const ancestor = child ? findAncestor(child.return, ancestorTag) : null; + + const ancestorDescription = + child !== null && ancestor !== null + ? describeAncestors(ancestor, child, null) + : ''; + const tagDisplayName = '<' + childTag + '>'; if (invalidParent) { let info = ''; @@ -478,33 +548,45 @@ function validateDOMNesting( ' Add a , or to your code to match the DOM tree generated by ' + 'the browser.'; } - // Don't transform into consoleWithStackDev here because we add a manual stack. - // We use the parent stack here instead of the owner stack because the parent - // stack has more useful context for nesting. - // TODO: Format this as a linkified "diff view" with props instead of - // a stack trace since the stack trace format is now for owner stacks. - console['error']( + console.error( 'In HTML, %s cannot be a child of <%s>.%s\n' + 'This will cause a hydration error.%s', tagDisplayName, ancestorTag, info, - getCurrentParentStackInDev(), + ancestorDescription, ); } else { - // Don't transform into consoleWithStackDev here because we add a manual stack. - // We use the parent stack here instead of the owner stack because the parent - // stack has more useful context for nesting. - // TODO: Format this as a linkified "diff view" with props instead of - // a stack trace since the stack trace format is now for owner stacks. - console['error']( + console.error( 'In HTML, %s cannot be a descendant of <%s>.\n' + 'This will cause a hydration error.%s', tagDisplayName, ancestorTag, - getCurrentParentStackInDev(), + ancestorDescription, ); } + if (enableOwnerStacks && child) { + // For debugging purposes find the nearest ancestor that caused the issue. + // The stack trace of this ancestor can be useful to find the cause. + // If the parent is a direct parent in the same owner, we don't bother. + const parent = child.return; + if ( + ancestor !== null && + parent !== null && + (ancestor !== parent || parent._debugOwner !== child._debugOwner) + ) { + runWithFiberInDEV(ancestor, () => { + console.error( + // We repeat some context because this log might be taken out of context + // such as in React DevTools or grouped server logs. + '<%s> cannot contain a nested %s.\n' + + 'See this log for the ancestor stack trace.', + ancestorTag, + tagDisplayName, + ); + }); + } + } return false; } return true; @@ -522,31 +604,33 @@ function validateTextNesting(childText: string, parentTag: string): boolean { } didWarn[warnKey] = true; + const child = current; + const ancestor = child ? findAncestor(child, parentTag) : null; + + const ancestorDescription = + child !== null && ancestor !== null + ? describeAncestors( + ancestor, + child, + child.tag !== HostText ? {children: null} : null, + ) + : ''; + if (/\S/.test(childText)) { - // Don't transform into consoleWithStackDev here because we add a manual stack. - // We use the parent stack here instead of the owner stack because the parent - // stack has more useful context for nesting. - // TODO: Format this as a linkified "diff view" with props instead of - // a stack trace since the stack trace format is now for owner stacks. - console['error']( + console.error( 'In HTML, text nodes cannot be a child of <%s>.\n' + 'This will cause a hydration error.%s', parentTag, - getCurrentParentStackInDev(), + ancestorDescription, ); } else { - // Don't transform into consoleWithStackDev here because we add a manual stack. - // We use the parent stack here instead of the owner stack because the parent - // stack has more useful context for nesting. - // TODO: Format this as a linkified "diff view" with props instead of - // a stack trace since the stack trace format is now for owner stacks. - console['error']( + console.error( 'In HTML, whitespace text nodes cannot be a child of <%s>. ' + "Make sure you don't have any extra whitespace between tags on " + 'each line of your source code.\n' + 'This will cause a hydration error.%s', parentTag, - getCurrentParentStackInDev(), + ancestorDescription, ); } return false; diff --git a/packages/react-dom-bindings/src/events/getListener.js b/packages/react-dom-bindings/src/events/getListener.js index 68ab23e219..d6c7bab6d0 100644 --- a/packages/react-dom-bindings/src/events/getListener.js +++ b/packages/react-dom-bindings/src/events/getListener.js @@ -62,6 +62,7 @@ export default function getListener( // Work in progress. return null; } + // $FlowFixMe[invalid-computed-prop] const listener = props[registrationName]; if (shouldPreventMouseEvent(registrationName, inst.type, props)) { return null; diff --git a/packages/react-dom-bindings/src/server/ReactDOMFlightServerHostDispatcher.js b/packages/react-dom-bindings/src/server/ReactDOMFlightServerHostDispatcher.js index 3303c07cfb..c2a9227737 100644 --- a/packages/react-dom-bindings/src/server/ReactDOMFlightServerHostDispatcher.js +++ b/packages/react-dom-bindings/src/server/ReactDOMFlightServerHostDispatcher.js @@ -238,6 +238,7 @@ function trimOptions< let hasProperties = false; const trimmed: T = ({}: any); for (const key in options) { + // $FlowFixMe[invalid-computed-prop] if (options[key] != null) { hasProperties = true; (trimmed: any)[key] = options[key]; diff --git a/packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js b/packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js index 594051dc35..8a670f8459 100644 --- a/packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js +++ b/packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js @@ -431,9 +431,13 @@ export function createRenderState( fontPreloads: '', highImagePreloads: '', remainingCapacity: - typeof maxHeadersLength === 'number' + // We seed the remainingCapacity with 2 extra bytes because when we decrement the capacity + // we always assume we are inserting an interstitial ", " however the first header does not actually + // consume these two extra bytes. + 2 + + (typeof maxHeadersLength === 'number' ? maxHeadersLength - : DEFAULT_HEADERS_CAPACITY_IN_UTF16_CODE_UNITS, + : DEFAULT_HEADERS_CAPACITY_IN_UTF16_CODE_UNITS), } : null; const renderState: RenderState = { @@ -2953,7 +2957,7 @@ function pushImg( // make this behavior different between render and prerender since in the latter case // we are less sensitive to the current requests runtime per and more sensitive to maximizing // headers. - (headers.remainingCapacity -= header.length) >= 2) + (headers.remainingCapacity -= header.length + 2) >= 0) ) { // If we postpone in the shell we will still emit this preload so we track // it to make sure we don't reset it. @@ -5393,7 +5397,7 @@ function prefetchDNS(href: string) { // make this behavior different between render and prerender since in the latter case // we are less sensitive to the current requests runtime per and more sensitive to maximizing // headers. - (headers.remainingCapacity -= header.length) >= 2) + (headers.remainingCapacity -= header.length + 2) >= 0) ) { // Store this as resettable in case we are prerendering and postpone in the Shell renderState.resets.dns[key] = EXISTS; @@ -5452,7 +5456,7 @@ function preconnect(href: string, crossOrigin: ?CrossOriginEnum) { // make this behavior different between render and prerender since in the latter case // we are less sensitive to the current requests runtime per and more sensitive to maximizing // headers. - (headers.remainingCapacity -= header.length) >= 2) + (headers.remainingCapacity -= header.length + 2) >= 0) ) { // Store this in resettableState in case we are prerending and postpone in the Shell renderState.resets.connect[bucket][key] = EXISTS; @@ -5518,7 +5522,7 @@ function preload(href: string, as: string, options?: ?PreloadImplOptions) { // make this behavior different between render and prerender since in the latter case // we are less sensitive to the current requests runtime per and more sensitive to maximizing // headers. - (headers.remainingCapacity -= header.length) >= 2) + (headers.remainingCapacity -= header.length + 2) >= 0) ) { // If we postpone in the shell we will still emit a preload as a header so we // track this to make sure we don't reset it. @@ -5633,7 +5637,7 @@ function preload(href: string, as: string, options?: ?PreloadImplOptions) { // make this behavior different between render and prerender since in the latter case // we are less sensitive to the current requests runtime per and more sensitive to maximizing // headers. - (headers.remainingCapacity -= header.length) >= 2) + (headers.remainingCapacity -= header.length + 2) >= 0) ) { // If we postpone in the shell we will still emit this preload so we // track it here to prevent it from being reset. @@ -6073,6 +6077,7 @@ function getPreloadAsHeader( let value = `<${escapedHref}>; rel=preload; as="${escapedAs}"`; for (const paramName in params) { if (hasOwnProperty.call(params, paramName)) { + // $FlowFixMe[invalid-computed-prop] const paramValue = params[paramName]; if (typeof paramValue === 'string') { value += `; ${paramName.toLowerCase()}="${escapeStringForLinkHeaderQuotedParamValueContext( @@ -6259,7 +6264,7 @@ export function emitEarlyPreloads( // This means that a particularly long header might close out the header queue where later // headers could still fit. We could in the future alter the behavior here based on prerender vs render // since during prerender we aren't as concerned with pure runtime performance. - if ((headers.remainingCapacity -= header.length) >= 2) { + if ((headers.remainingCapacity -= header.length + 2) >= 0) { renderState.resets.style[key] = PRELOAD_NO_CREDS; if (linkHeader) { linkHeader += ', '; diff --git a/packages/react-dom/src/__tests__/ReactDOMComponent-test.js b/packages/react-dom/src/__tests__/ReactDOMComponent-test.js index 6e02e17ece..d37a4ecba6 100644 --- a/packages/react-dom/src/__tests__/ReactDOMComponent-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMComponent-test.js @@ -2193,13 +2193,18 @@ describe('ReactDOMComponent', () => {
, ); }); - }).toErrorDev([ - 'In HTML, cannot be a child of ' + - '
.\n' + - 'This will cause a hydration error.' + + }).toErrorDev( + 'In HTML, cannot be a child of
.\n' + + 'This will cause a hydration error.\n' + + '\n' + + '>
\n' + + '> \n' + + ' ...\n' + '\n in tr (at **)' + - '\n in div (at **)', - ]); + (gate(flags => flags.enableOwnerStacks) + ? '' + : '\n in div (at **)'), + ); }); it('warns on invalid nesting at root', async () => { @@ -2215,12 +2220,13 @@ describe('ReactDOMComponent', () => { ); }); }).toErrorDev( - 'In HTML,

cannot be a descendant ' + - 'of

.\n' + + 'In HTML,

cannot be a descendant of

.\n' + 'This will cause a hydration error.' + // There is no outer `p` here because root container is not part of the stack. '\n in p (at **)' + - '\n in span (at **)', + (gate(flags => flags.enableOwnerStacks) + ? '' + : '\n in span (at **)'), ); }); @@ -2248,29 +2254,90 @@ describe('ReactDOMComponent', () => { await act(() => { root.render(); }); - }).toErrorDev([ - 'In HTML, cannot be a child of ' + - '. Add a , or to your code to match the DOM tree generated ' + - 'by the browser.\n' + - 'This will cause a hydration error.' + - '\n in tr (at **)' + - '\n in Row (at **)' + - '\n in table (at **)' + - '\n in Foo (at **)', - 'In HTML, text nodes cannot be a ' + - 'child of .\n' + - 'This will cause a hydration error.' + - '\n in tr (at **)' + - '\n in Row (at **)' + - '\n in table (at **)' + - '\n in Foo (at **)', - 'In HTML, whitespace text nodes cannot ' + - "be a child of
. Make sure you don't have any extra " + - 'whitespace between tags on each line of your source code.\n' + - 'This will cause a hydration error.' + - '\n in table (at **)' + - '\n in Foo (at **)', - ]); + }).toErrorDev( + gate(flags => flags.enableOwnerStacks) + ? [ + 'In HTML, cannot be a child of ' + + '
. Add a , or to your code to match the DOM tree generated ' + + 'by the browser.\n' + + 'This will cause a hydration error.\n' + + '\n' + + ' \n' + + '>
\n' + + ' \n' + + '> \n' + + ' ...\n' + + '\n in tr (at **)' + + '\n in Row (at **)', + '
cannot contain a nested .\nSee this log for the ancestor stack trace.' + + '\n in table (at **)' + + '\n in Foo (at **)', + 'In HTML, text nodes cannot be a ' + + 'child of .\n' + + 'This will cause a hydration error.\n' + + '\n' + + ' \n' + + '
\n' + + ' \n' + + ' \n' + + '> x\n' + + ' ...\n' + + '\n in tr (at **)' + + '\n in Row (at **)', + 'In HTML, whitespace text nodes cannot ' + + "be a child of
. Make sure you don't have any extra " + + 'whitespace between tags on each line of your source code.\n' + + 'This will cause a hydration error.\n' + + '\n' + + ' \n' + + '>
\n' + + ' \n' + + '> {" "}\n' + + '\n in table (at **)' + + '\n in Foo (at **)', + ] + : [ + 'In HTML, cannot be a child of ' + + '
. Add a , or to your code to match the DOM tree generated ' + + 'by the browser.\n' + + 'This will cause a hydration error.\n' + + '\n' + + ' \n' + + '>
\n' + + ' \n' + + '> \n' + + ' ...\n' + + '\n in tr (at **)' + + '\n in Row (at **)' + + '\n in table (at **)' + + '\n in Foo (at **)', + 'In HTML, text nodes cannot be a ' + + 'child of .\n' + + 'This will cause a hydration error.\n' + + '\n' + + ' \n' + + '
\n' + + ' \n' + + ' \n' + + '> x\n' + + ' ...\n' + + '\n in tr (at **)' + + '\n in Row (at **)' + + '\n in table (at **)' + + '\n in Foo (at **)', + 'In HTML, whitespace text nodes cannot ' + + "be a child of
. Make sure you don't have any extra " + + 'whitespace between tags on each line of your source code.\n' + + 'This will cause a hydration error.\n' + + '\n' + + ' \n' + + '>
\n' + + ' \n' + + '> {" "}\n' + + '\n in table (at **)' + + '\n in Foo (at **)', + ], + ); }); it('warns nicely for updating table rows to use text', async () => { @@ -2297,7 +2364,11 @@ describe('ReactDOMComponent', () => { 'In HTML, whitespace text nodes cannot ' + "be a child of
. Make sure you don't have any extra " + 'whitespace between tags on each line of your source code.\n' + - 'This will cause a hydration error.' + + 'This will cause a hydration error.\n' + + '\n' + + ' \n' + + '
\n' + + '> {" "}\n' + '\n in table (at **)' + '\n in Foo (at **)', ]); @@ -2325,12 +2396,21 @@ describe('ReactDOMComponent', () => { }).toErrorDev([ 'In HTML, text nodes cannot be a ' + 'child of .\n' + - 'This will cause a hydration error.' + + 'This will cause a hydration error.\n' + + '\n' + + ' \n' + + '
\n' + + ' \n' + + ' \n' + + ' \n' + + '> text\n' + '\n in tr (at **)' + '\n in Row (at **)' + - '\n in tbody (at **)' + - '\n in table (at **)' + - '\n in Foo (at **)', + (gate(flags => flags.enableOwnerStacks) + ? '' + : '\n in tbody (at **)' + + '\n in table (at **)' + + '\n in Foo (at **)'), ]); }); @@ -2359,11 +2439,21 @@ describe('ReactDOMComponent', () => { root.render(); }); }).toErrorDev( - '\n in tr (at **)' + - '\n in Row (at **)' + - '\n in FancyRow (at **)' + - '\n in table (at **)' + - '\n in Viz1 (at **)', + gate(flags => flags.enableOwnerStacks) + ? [ + '\n in tr (at **)' + + '\n in Row (at **)' + + '\n in FancyRow (at **)' + + '\n in Viz1 (at **)', + '\n in table (at **)' + '\n in Viz1 (at **)', + ] + : [ + '\n in tr (at **)' + + '\n in Row (at **)' + + '\n in FancyRow (at **)' + + '\n in table (at **)' + + '\n in Viz1 (at **)', + ], ); }); @@ -2405,13 +2495,26 @@ describe('ReactDOMComponent', () => { root.render(); }); }).toErrorDev( - '\n in tr (at **)' + - '\n in Row (at **)' + - '\n in FancyRow (at **)' + - '\n in table (at **)' + - '\n in Table (at **)' + - '\n in FancyTable (at **)' + - '\n in Viz2 (at **)', + gate(flags => flags.enableOwnerStacks) + ? [ + '\n in tr (at **)' + + '\n in Row (at **)' + + '\n in FancyRow (at **)' + + '\n in Viz2 (at **)', + '\n in table (at **)' + + '\n in Table (at **)' + + '\n in FancyTable (at **)' + + '\n in Viz2 (at **)', + ] + : [ + '\n in tr (at **)' + + '\n in Row (at **)' + + '\n in FancyRow (at **)' + + '\n in table (at **)' + + '\n in Table (at **)' + + '\n in FancyTable (at **)' + + '\n in Viz2 (at **)', + ], ); }); @@ -2446,12 +2549,23 @@ describe('ReactDOMComponent', () => { ); }); }).toErrorDev( - '\n in tr (at **)' + - '\n in Row (at **)' + - '\n in FancyRow (at **)' + - '\n in table (at **)' + - '\n in Table (at **)' + - '\n in FancyTable (at **)', + gate(flags => flags.enableOwnerStacks) + ? [ + '\n in tr (at **)' + + '\n in Row (at **)' + + '\n in FancyRow (at **)', + '\n in table (at **)' + + '\n in Table (at **)' + + '\n in FancyTable (at **)', + ] + : [ + '\n in tr (at **)' + + '\n in Row (at **)' + + '\n in FancyRow (at **)' + + '\n in table (at **)' + + '\n in Table (at **)' + + '\n in FancyTable (at **)', + ], ); }); @@ -2475,10 +2589,19 @@ describe('ReactDOMComponent', () => { ); }); }).toErrorDev( - '\n in tr (at **)' + - '\n in Row (at **)' + - '\n in FancyRow (at **)' + - '\n in table (at **)', + gate(flags => flags.enableOwnerStacks) + ? [ + '\n in tr (at **)' + + '\n in Row (at **)' + + '\n in FancyRow (at **)', + '\n in table (at **)', + ] + : [ + '\n in tr (at **)' + + '\n in Row (at **)' + + '\n in FancyRow (at **)' + + '\n in table (at **)', + ], ); }); @@ -2506,10 +2629,19 @@ describe('ReactDOMComponent', () => { ); }); }).toErrorDev( - '\n in tr (at **)' + - '\n in table (at **)' + - '\n in Table (at **)' + - '\n in FancyTable (at **)', + gate(flags => flags.enableOwnerStacks) + ? [ + '\n in tr (at **)', + '\n in table (at **)' + + '\n in Table (at **)' + + '\n in FancyTable (at **)', + ] + : [ + '\n in tr (at **)' + + '\n in table (at **)' + + '\n in Table (at **)' + + '\n in FancyTable (at **)', + ], ); class Link extends React.Component { @@ -2531,11 +2663,18 @@ describe('ReactDOMComponent', () => { ); }); }).toErrorDev( - '\n in a (at **)' + - '\n in Link (at **)' + - '\n in div (at **)' + - '\n in a (at **)' + - '\n in Link (at **)', + gate(flags => flags.enableOwnerStacks) + ? [ + '\n in a (at **)' + '\n in Link (at **)', + '\n in a (at **)' + '\n in Link (at **)', + ] + : [ + '\n in a (at **)' + + '\n in Link (at **)' + + '\n in div (at **)' + + '\n in a (at **)' + + '\n in Link (at **)', + ], ); }); diff --git a/packages/react-dom/src/__tests__/ReactDOMConsoleErrorReporting-test.js b/packages/react-dom/src/__tests__/ReactDOMConsoleErrorReporting-test.js index 9eeb4e7c07..2aca1e0588 100644 --- a/packages/react-dom/src/__tests__/ReactDOMConsoleErrorReporting-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMConsoleErrorReporting-test.js @@ -143,7 +143,8 @@ describe('ReactDOMConsoleErrorReporting', () => { expect.stringContaining('%s'), expect.stringContaining('An error occurred in the component'), expect.stringContaining('Consider adding an error boundary'), - expect.stringContaining('Foo'), + // The component stack is not added without the polyfill/devtools. + // expect.stringContaining('Foo'), ], ]); } else { @@ -208,7 +209,8 @@ describe('ReactDOMConsoleErrorReporting', () => { 'The above error occurred in the component', ), expect.stringContaining('ErrorBoundary'), - expect.stringContaining('Foo'), + // The component stack is not added without the polyfill/devtools. + // expect.stringContaining('Foo'), ], ]); } else { @@ -274,7 +276,8 @@ describe('ReactDOMConsoleErrorReporting', () => { expect.stringContaining('%s'), expect.stringContaining('An error occurred in the component'), expect.stringContaining('Consider adding an error boundary'), - expect.stringContaining('Foo'), + // The component stack is not added without the polyfill/devtools. + // expect.stringContaining('Foo'), ], ]); } else { @@ -344,7 +347,8 @@ describe('ReactDOMConsoleErrorReporting', () => { 'The above error occurred in the component', ), expect.stringContaining('ErrorBoundary'), - expect.stringContaining('Foo'), + // The component stack is not added without the polyfill/devtools. + // expect.stringContaining('Foo'), ], ]); } else { @@ -410,7 +414,8 @@ describe('ReactDOMConsoleErrorReporting', () => { expect.stringContaining('%s'), expect.stringContaining('An error occurred in the component'), expect.stringContaining('Consider adding an error boundary'), - expect.stringContaining('Foo'), + // The component stack is not added without the polyfill/devtools. + // expect.stringContaining('Foo'), ], ]); } else { @@ -478,7 +483,8 @@ describe('ReactDOMConsoleErrorReporting', () => { 'The above error occurred in the component', ), expect.stringContaining('ErrorBoundary'), - expect.stringContaining('Foo'), + // The component stack is not added without the polyfill/devtools. + // expect.stringContaining('Foo'), ], ]); } else { diff --git a/packages/react-dom/src/__tests__/ReactDOMConsoleErrorReportingLegacy-test.js b/packages/react-dom/src/__tests__/ReactDOMConsoleErrorReportingLegacy-test.js index 65301c789d..99bfeac56b 100644 --- a/packages/react-dom/src/__tests__/ReactDOMConsoleErrorReportingLegacy-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMConsoleErrorReportingLegacy-test.js @@ -162,7 +162,8 @@ describe('ReactDOMConsoleErrorReporting', () => { // Addendum by React: expect.stringContaining('An error occurred in the component'), expect.stringContaining('Consider adding an error boundary'), - expect.stringContaining('Foo'), + // The component stack is not added without the polyfill/devtools. + // expect.stringContaining('Foo'), ], ]); @@ -239,7 +240,8 @@ describe('ReactDOMConsoleErrorReporting', () => { 'The above error occurred in the component', ), expect.stringContaining('ErrorBoundary'), - expect.stringContaining('Foo'), + // The component stack is not added without the polyfill/devtools. + // expect.stringContaining('Foo'), ], ]); } else { @@ -309,7 +311,8 @@ describe('ReactDOMConsoleErrorReporting', () => { // Addendum by React: expect.stringContaining('An error occurred in the component'), expect.stringContaining('Consider adding an error boundary'), - expect.stringContaining('Foo'), + // The component stack is not added without the polyfill/devtools. + // expect.stringContaining('Foo'), ], ]); @@ -390,7 +393,8 @@ describe('ReactDOMConsoleErrorReporting', () => { 'The above error occurred in the component', ), expect.stringContaining('ErrorBoundary'), - expect.stringContaining('Foo'), + // The component stack is not added without the polyfill/devtools. + // expect.stringContaining('Foo'), ], ]); } else { @@ -460,7 +464,8 @@ describe('ReactDOMConsoleErrorReporting', () => { // Addendum by React: expect.stringContaining('An error occurred in the component'), expect.stringContaining('Consider adding an error boundary'), - expect.stringContaining('Foo'), + // The component stack is not added without the polyfill/devtools. + // expect.stringContaining('Foo'), ], ]); @@ -540,7 +545,8 @@ describe('ReactDOMConsoleErrorReporting', () => { 'The above error occurred in the component', ), expect.stringContaining('ErrorBoundary'), - expect.stringContaining('Foo'), + // The component stack is not added without the polyfill/devtools. + // expect.stringContaining('Foo'), ], ]); } else { diff --git a/packages/react-dom/src/__tests__/ReactDOMFiber-test.js b/packages/react-dom/src/__tests__/ReactDOMFiber-test.js index a1633fa1e6..7c701219ec 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFiber-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFiber-test.js @@ -13,10 +13,12 @@ let React; let ReactDOM; let PropTypes; let ReactDOMClient; -let root; let Scheduler; + let act; +let assertConsoleErrorDev; let assertLog; +let root; describe('ReactDOMFiber', () => { let container; @@ -29,7 +31,7 @@ describe('ReactDOMFiber', () => { ReactDOMClient = require('react-dom/client'); Scheduler = require('scheduler'); act = require('internal-test-utils').act; - assertLog = require('internal-test-utils').assertLog; + ({assertConsoleErrorDev, assertLog} = require('internal-test-utils')); container = document.createElement('div'); document.body.appendChild(container); @@ -732,6 +734,10 @@ describe('ReactDOMFiber', () => { await act(async () => { root.render(); }); + assertConsoleErrorDev([ + 'Parent uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.', + 'Component uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.', + ]); expect(container.innerHTML).toBe(''); expect(portalContainer.innerHTML).toBe('
bar
'); }); diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzForm-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzForm-test.js index b83abb5693..0ecfe3eb82 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFizzForm-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFizzForm-test.js @@ -40,12 +40,12 @@ describe('ReactDOMFizzForm', () => { act = require('internal-test-utils').act; container = document.createElement('div'); document.body.appendChild(container); - if (__VARIANT__) { - // Remove after API is deleted. - useActionState = require('react-dom').useFormState; - } else { - useActionState = require('react').useActionState; - } + // TODO: Test the old api but it warns so needs warnings to be asserted. + // if (__VARIANT__) { + // Remove after API is deleted. + // useActionState = require('react-dom').useFormState; + // } + useActionState = require('react').useActionState; }); afterEach(() => { diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js index 62e0a17af0..f51101b4d4 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js @@ -27,6 +27,8 @@ let ReactDOMFizzServer; let ReactDOMFizzStatic; let Suspense; let SuspenseList; + +let assertConsoleErrorDev; let useSyncExternalStore; let useSyncExternalStoreWithSelector; let use; @@ -116,12 +118,14 @@ describe('ReactDOMFizzServer', () => { useActionState = React.useActionState; } - const InternalTestUtils = require('internal-test-utils'); - waitForAll = InternalTestUtils.waitForAll; - waitFor = InternalTestUtils.waitFor; - waitForPaint = InternalTestUtils.waitForPaint; - assertLog = InternalTestUtils.assertLog; - clientAct = InternalTestUtils.act; + ({ + assertConsoleErrorDev, + assertLog, + act: clientAct, + waitFor, + waitForAll, + waitForPaint, + } = require('internal-test-utils')); if (gate(flags => flags.source)) { // The `with-selector` module composes the main `use-sync-external-store` @@ -700,17 +704,39 @@ describe('ReactDOMFizzServer', () => { it('should client render a boundary if a lazy component rejects', async () => { let rejectComponent; + const promise = new Promise((resolve, reject) => { + rejectComponent = reject; + }); const LazyComponent = React.lazy(() => { - return new Promise((resolve, reject) => { - rejectComponent = reject; - }); + return promise; + }); + + const LazyLazy = React.lazy(async () => { + return { + default: LazyComponent, + }; + }); + + function Wrapper({children}) { + return children; + } + const LazyWrapper = React.lazy(() => { + return { + then(callback) { + callback({ + default: Wrapper, + }); + }, + }; }); function App({isClient}) { return (
}> - {isClient ? : } + + {isClient ? : } +
); @@ -744,6 +770,7 @@ describe('ReactDOMFizzServer', () => { }); pipe(writable); }); + expect(loggedErrors).toEqual([]); expect(bootstrapped).toBe(true); @@ -772,7 +799,7 @@ describe('ReactDOMFizzServer', () => { 'Switched to client rendering because the server rendering errored:\n\n' + theError.message, expectedDigest, - componentStack(['Lazy', 'Suspense', 'div', 'App']), + componentStack(['Lazy', 'Wrapper', 'Suspense', 'div', 'App']), ], ], [ @@ -852,13 +879,9 @@ describe('ReactDOMFizzServer', () => { } await act(() => { - const {pipe} = renderToPipeableStream( - , - - { - onError, - }, - ); + const {pipe} = renderToPipeableStream(, { + onError, + }); pipe(writable); }); expect(loggedErrors).toEqual([]); @@ -896,7 +919,7 @@ describe('ReactDOMFizzServer', () => { 'Switched to client rendering because the server rendering errored:\n\n' + theError.message, expectedDigest, - componentStack(['Lazy', 'Suspense', 'div', 'App']), + componentStack(['Suspense', 'div', 'App']), ], ], [ @@ -1395,13 +1418,13 @@ describe('ReactDOMFizzServer', () => { 'The render was aborted by the server without a reason.', expectedDigest, // We get the stack of the task when it was aborted which is why we see `h1` - componentStack(['h1', 'Suspense', 'div', 'App']), + componentStack(['AsyncText', 'h1', 'Suspense', 'div', 'App']), ], [ 'Switched to client rendering because the server rendering aborted due to:\n\n' + 'The render was aborted by the server without a reason.', expectedDigest, - componentStack(['Suspense', 'main', 'div', 'App']), + componentStack(['AsyncText', 'Suspense', 'main', 'div', 'App']), ], ], [ @@ -1789,83 +1812,59 @@ describe('ReactDOMFizzServer', () => { ); } - // We can't use the toErrorDev helper here because this is an async act. - const originalConsoleError = console.error; - const mockError = jest.fn(); - console.error = (...args) => { - mockError(...args.map(normalizeCodeLocInfo)); - }; + await act(() => { + const {pipe} = renderToPipeableStream(); + pipe(writable); + }); - try { - await act(() => { - const {pipe} = renderToPipeableStream(); - pipe(writable); - }); + expect(getVisibleChildren(container)).toEqual( +
+ Loading +
, + ); - expect(getVisibleChildren(container)).toEqual( + assertConsoleErrorDev([ + ' is using incorrect casing. Use PascalCase for React components, or lowercase for HTML elements.' + + '\n' + + (gate(flags => flags.enableOwnerStacks) + ? ' in inCorrectTag (at **)\n' + + ' in C (at **)\n' + + ' in A (at **)' + : ' in inCorrectTag (at **)\n' + + ' in C (at **)\n' + + ' in Suspense (at **)\n' + + ' in div (at **)\n' + + ' in A (at **)'), + ]); + + await act(() => { + resolveText('Hello'); + resolveText('World'); + }); + + assertConsoleErrorDev([ + 'Each child in a list should have a unique "key" prop.\n\nCheck the render method of `B`.' + + ' See https://react.dev/link/warning-keys for more information.\n' + + (gate(flags => flags.enableOwnerStacks) + ? ' in span (at **)\n' + + ' in mapper (at **)\n' + + ' in B (at **)\n' + + ' in A (at **)' + : ' in span (at **)\n' + + ' in B (at **)\n' + + ' in Suspense (at **)\n' + + ' in div (at **)\n' + + ' in A (at **)'), + ]); + + expect(getVisibleChildren(container)).toEqual( +
- Loading -
, - ); - - if (__DEV__) { - expect(mockError).toHaveBeenCalledWith( - '<%s /> is using incorrect casing. Use PascalCase for React components, or lowercase for HTML elements.%s', - 'inCorrectTag', - '\n' + - (gate(flags => flags.enableOwnerStacks) - ? ' in inCorrectTag (at **)\n' + - ' in C (at **)\n' + - ' in A (at **)' - : ' in inCorrectTag (at **)\n' + - ' in C (at **)\n' + - ' in Suspense (at **)\n' + - ' in div (at **)\n' + - ' in A (at **)'), - ); - mockError.mockClear(); - } else { - expect(mockError).not.toHaveBeenCalled(); - } - - await act(() => { - resolveText('Hello'); - resolveText('World'); - }); - - if (__DEV__) { - expect(mockError).toHaveBeenCalledWith( - 'Each child in a list should have a unique "key" prop.%s%s' + - ' See https://react.dev/link/warning-keys for more information.%s', - '\n\nCheck the render method of `B`.', - '', - '\n' + - (gate(flags => flags.enableOwnerStacks) - ? ' in span (at **)\n' + - ' in mapper (at **)\n' + - ' in B (at **)\n' + - ' in A (at **)' - : ' in span (at **)\n' + - ' in B (at **)\n' + - ' in Suspense (at **)\n' + - ' in div (at **)\n' + - ' in A (at **)'), - ); - } else { - expect(mockError).not.toHaveBeenCalled(); - } - - expect(getVisibleChildren(container)).toEqual( -
-
- Hello - World -
-
, - ); - } finally { - console.error = originalConsoleError; - } + Hello + World +
+ , + ); }); // @gate !disableLegacyContext @@ -1931,6 +1930,10 @@ describe('ReactDOMFizzServer', () => { ); pipe(writable); }); + assertConsoleErrorDev([ + 'TestProvider uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.', + 'TestConsumer uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.', + ]); expect(getVisibleChildren(container)).toEqual(
Loading: A @@ -3523,13 +3526,13 @@ describe('ReactDOMFizzServer', () => { 'Switched to client rendering because the server rendering aborted due to:\n\n' + 'foobar', 'a digest', - componentStack(['Suspense', 'p', 'div', 'App']), + componentStack(['AsyncText', 'Suspense', 'p', 'div', 'App']), ], [ 'Switched to client rendering because the server rendering aborted due to:\n\n' + 'foobar', 'a digest', - componentStack(['Suspense', 'span', 'div', 'App']), + componentStack(['AsyncText', 'Suspense', 'span', 'div', 'App']), ], ], [ @@ -3606,13 +3609,13 @@ describe('ReactDOMFizzServer', () => { 'Switched to client rendering because the server rendering aborted due to:\n\n' + 'uh oh', 'a digest', - componentStack(['Suspense', 'p', 'div', 'App']), + componentStack(['AsyncText', 'Suspense', 'p', 'div', 'App']), ], [ 'Switched to client rendering because the server rendering aborted due to:\n\n' + 'uh oh', 'a digest', - componentStack(['Suspense', 'span', 'div', 'App']), + componentStack(['AsyncText', 'Suspense', 'span', 'div', 'App']), ], ], [ @@ -3937,6 +3940,59 @@ describe('ReactDOMFizzServer', () => { expect(getVisibleChildren(container)).toEqual(
hello
); }); + it('accounts for the length of the interstitial between links when computing the headers length', async () => { + let headers = null; + function onHeaders(x) { + headers = x; + } + + function App() { + // 20 bytes + ReactDOM.preconnect('01'); + // 42 bytes + ReactDOM.preconnect('02'); + // 64 bytes + ReactDOM.preconnect('03'); + // 86 bytes + ReactDOM.preconnect('04'); + // 108 bytes + ReactDOM.preconnect('05'); + // 130 bytes + ReactDOM.preconnect('06'); + // 152 bytes + ReactDOM.preconnect('07'); + // 174 bytes + ReactDOM.preconnect('08'); + // 196 bytes + ReactDOM.preconnect('09'); + // 218 bytes + ReactDOM.preconnect('10'); + // 240 bytes + ReactDOM.preconnect('11'); + // 262 bytes + ReactDOM.preconnect('12'); + // 284 bytes + ReactDOM.preconnect('13'); + // 306 bytes + ReactDOM.preconnect('14'); + return ( + + hello + + ); + } + + await act(() => { + renderToPipeableStream(, {onHeaders, maxHeadersLength: 305}); + }); + expect(headers.Link.length).toBe(284); + + await act(() => { + renderToPipeableStream(, {onHeaders, maxHeadersLength: 306}); + }); + expect(headers.Link.length).toBe(306); + }); + describe('error escaping', () => { it('escapes error hash, message, and component stack values in directly flushed errors (html escaping)', async () => { window.__outlet = {}; @@ -4656,12 +4712,6 @@ describe('ReactDOMFizzServer', () => { // @gate favorSafetyOverHydrationPerf it('only warns once on hydration mismatch while within a suspense boundary', async () => { - const originalConsoleError = console.error; - const mockError = jest.fn(); - console.error = (...args) => { - mockError(...args.map(normalizeCodeLocInfo)); - }; - const App = ({text}) => { return (
@@ -4674,45 +4724,40 @@ describe('ReactDOMFizzServer', () => { ); }; - try { - await act(() => { - const {pipe} = renderToPipeableStream(); - pipe(writable); - }); + await act(() => { + const {pipe} = renderToPipeableStream(); + pipe(writable); + }); - expect(getVisibleChildren(container)).toEqual( -
-

initial

-

initial

-

initial

-
, - ); + expect(getVisibleChildren(container)).toEqual( +
+

initial

+

initial

+

initial

+
, + ); - ReactDOMClient.hydrateRoot(container, , { - onRecoverableError(error) { - Scheduler.log('onRecoverableError: ' + normalizeError(error.message)); - if (error.cause) { - Scheduler.log('Cause: ' + normalizeError(error.cause.message)); - } - }, - }); - await waitForAll([ - "onRecoverableError: Hydration failed because the server rendered HTML didn't match the client.", - ]); + ReactDOMClient.hydrateRoot(container, , { + onRecoverableError(error) { + Scheduler.log('onRecoverableError: ' + normalizeError(error.message)); + if (error.cause) { + Scheduler.log('Cause: ' + normalizeError(error.cause.message)); + } + }, + }); + await waitForAll([ + "onRecoverableError: Hydration failed because the server rendered HTML didn't match the client.", + ]); - expect(getVisibleChildren(container)).toEqual( -
-

replaced

-

replaced

-

replaced

-
, - ); + expect(getVisibleChildren(container)).toEqual( +
+

replaced

+

replaced

+

replaced

+
, + ); - await waitForAll([]); - expect(mockError.mock.calls.length).toBe(0); - } finally { - console.error = originalConsoleError; - } + await waitForAll([]); }); it('supresses hydration warnings when an error occurs within a Suspense boundary', async () => { @@ -4788,18 +4833,6 @@ describe('ReactDOMFizzServer', () => { }); it('does not log for errors after the first hydration error', async () => { - // We can't use the toErrorDev helper here because this is async. - const originalConsoleError = console.error; - const mockError = jest.fn(); - console.error = (...args) => { - if (args.length > 1) { - if (typeof args[1] === 'object') { - mockError(args[0].split('\n')[0]); - return; - } - } - mockError(...args.map(normalizeCodeLocInfo)); - }; let isClient = false; function ThrowWhenHydrating({children, message}) { @@ -4837,69 +4870,50 @@ describe('ReactDOMFizzServer', () => { ); }; - try { - await act(() => { - const {pipe} = renderToPipeableStream(); - pipe(writable); - }); + await act(() => { + const {pipe} = renderToPipeableStream(); + pipe(writable); + }); - expect(getVisibleChildren(container)).toEqual( -
-

one

-

two

-

three

-
, - ); + expect(getVisibleChildren(container)).toEqual( +
+

one

+

two

+

three

+
, + ); - isClient = true; + isClient = true; - ReactDOMClient.hydrateRoot(container, , { - onRecoverableError(error) { - Scheduler.log('onRecoverableError: ' + normalizeError(error.message)); - if (error.cause) { - Scheduler.log('Cause: ' + normalizeError(error.cause.message)); - } - }, - }); - await waitForAll([ - 'throwing: first error', + ReactDOMClient.hydrateRoot(container, , { + onRecoverableError(error) { + Scheduler.log('onRecoverableError: ' + normalizeError(error.message)); + if (error.cause) { + Scheduler.log('Cause: ' + normalizeError(error.cause.message)); + } + }, + }); + await waitForAll([ + 'throwing: first error', - // onRecoverableError because the UI recovered without surfacing the - // error to the user. - 'onRecoverableError: There was an error while hydrating but React was able to recover by instead client rendering from the nearest Suspense boundary.', - 'Cause: first error', - ]); - expect(mockError.mock.calls).toEqual([]); - mockError.mockClear(); + // onRecoverableError because the UI recovered without surfacing the + // error to the user. + 'onRecoverableError: There was an error while hydrating but React was able to recover by instead client rendering from the nearest Suspense boundary.', + 'Cause: first error', + ]); - expect(getVisibleChildren(container)).toEqual( -
-

one

-

two

-

three

-
, - ); + expect(getVisibleChildren(container)).toEqual( +
+

one

+

two

+

three

+
, + ); - await waitForAll([]); - expect(mockError.mock.calls).toEqual([]); - } finally { - console.error = originalConsoleError; - } + await waitForAll([]); }); it('does not log for errors after a preceding fiber suspends', async () => { - // We can't use the toErrorDev helper here because this is async. - const originalConsoleError = console.error; - const mockError = jest.fn(); - console.error = (...args) => { - if (args.length > 1) { - if (typeof args[1] === 'object') { - mockError(args[0].split('\n')[0]); - return; - } - } - mockError(...args.map(normalizeCodeLocInfo)); - }; let isClient = false; let promise = null; let unsuspend = null; @@ -4957,56 +4971,51 @@ describe('ReactDOMFizzServer', () => { ); }; - try { - await act(() => { - const {pipe} = renderToPipeableStream(); - pipe(writable); - }); + await act(() => { + const {pipe} = renderToPipeableStream(); + pipe(writable); + }); - expect(getVisibleChildren(container)).toEqual( -
-

one

-

two

-

three

-
, - ); + expect(getVisibleChildren(container)).toEqual( +
+

one

+

two

+

three

+
, + ); - isClient = true; + isClient = true; - ReactDOMClient.hydrateRoot(container, , { - onRecoverableError(error) { - Scheduler.log('onRecoverableError: ' + normalizeError(error.message)); - if (error.cause) { - Scheduler.log('Cause: ' + normalizeError(error.cause.message)); - } - }, - }); - await waitForAll(['suspending']); - expect(mockError.mock.calls).toEqual([]); + ReactDOMClient.hydrateRoot(container, , { + onRecoverableError(error) { + Scheduler.log('onRecoverableError: ' + normalizeError(error.message)); + if (error.cause) { + Scheduler.log('Cause: ' + normalizeError(error.cause.message)); + } + }, + }); + await waitForAll(['suspending']); - expect(getVisibleChildren(container)).toEqual( -
-

one

-

two

-

three

-
, - ); - await unsuspend(); - await waitForAll([ - 'throwing: first error', - 'onRecoverableError: There was an error while hydrating but React was able to recover by instead client rendering from the nearest Suspense boundary.', - 'Cause: first error', - ]); - expect(getVisibleChildren(container)).toEqual( -
-

one

-

two

-

three

-
, - ); - } finally { - console.error = originalConsoleError; - } + expect(getVisibleChildren(container)).toEqual( +
+

one

+

two

+

three

+
, + ); + await unsuspend(); + await waitForAll([ + 'throwing: first error', + 'onRecoverableError: There was an error while hydrating but React was able to recover by instead client rendering from the nearest Suspense boundary.', + 'Cause: first error', + ]); + expect(getVisibleChildren(container)).toEqual( +
+

one

+

two

+

three

+
, + ); }); it('(outdated behavior) suspending after erroring will cause errors previously queued to be silenced until the boundary resolves', async () => { @@ -5015,18 +5024,6 @@ describe('ReactDOMFizzServer', () => { // stack and revert to client rendering. I've kept the test around just to // demonstrate what actually happens in this sequence of events. - // We can't use the toErrorDev helper here because this is async. - const originalConsoleError = console.error; - const mockError = jest.fn(); - console.error = (...args) => { - if (args.length > 1) { - if (typeof args[1] === 'object') { - mockError(args[0].split('\n')[0]); - return; - } - } - mockError(...args.map(normalizeCodeLocInfo)); - }; let isClient = false; let promise = null; let unsuspend = null; @@ -5084,60 +5081,53 @@ describe('ReactDOMFizzServer', () => { ); }; - try { - await act(() => { - const {pipe} = renderToPipeableStream(); - pipe(writable); - }); + await act(() => { + const {pipe} = renderToPipeableStream(); + pipe(writable); + }); - expect(getVisibleChildren(container)).toEqual( -
-

one

-

two

-

three

-
, - ); + expect(getVisibleChildren(container)).toEqual( +
+

one

+

two

+

three

+
, + ); - isClient = true; + isClient = true; - ReactDOMClient.hydrateRoot(container, , { - onRecoverableError(error) { - Scheduler.log('onRecoverableError: ' + normalizeError(error.message)); - if (error.cause) { - Scheduler.log('Cause: ' + normalizeError(error.cause.message)); - } - }, - }); - await waitForAll([ - 'throwing: first error', - 'suspending', - 'onRecoverableError: There was an error while hydrating but React was able to recover by instead client rendering from the nearest Suspense boundary.', - 'Cause: first error', - ]); - expect(mockError.mock.calls).toEqual([]); - mockError.mockClear(); + ReactDOMClient.hydrateRoot(container, , { + onRecoverableError(error) { + Scheduler.log('onRecoverableError: ' + normalizeError(error.message)); + if (error.cause) { + Scheduler.log('Cause: ' + normalizeError(error.cause.message)); + } + }, + }); + await waitForAll([ + 'throwing: first error', + 'suspending', + 'onRecoverableError: There was an error while hydrating but React was able to recover by instead client rendering from the nearest Suspense boundary.', + 'Cause: first error', + ]); - expect(getVisibleChildren(container)).toEqual( -
-

Loading...

-
, - ); - await clientAct(() => unsuspend()); - // Since our client components only throw on the very first render there are no - // new throws in this pass - assertLog([]); - expect(mockError.mock.calls).toEqual([]); + expect(getVisibleChildren(container)).toEqual( +
+

Loading...

+
, + ); + await clientAct(() => unsuspend()); + // Since our client components only throw on the very first render there are no + // new throws in this pass + assertLog([]); - expect(getVisibleChildren(container)).toEqual( -
-

one

-

two

-

three

-
, - ); - } finally { - console.error = originalConsoleError; - } + expect(getVisibleChildren(container)).toEqual( +
+

one

+

two

+

three

+
, + ); }); it('#24578 Hydration errors caused by a suspending component should not become recoverable when nested in an ancestor Suspense that is showing primary content', async () => { @@ -6514,11 +6504,6 @@ describe('ReactDOMFizzServer', () => { function MyScript() { return 'bar();'; } - const originalConsoleError = console.error; - const mockError = jest.fn(); - console.error = (...args) => { - mockError(...args.map(normalizeCodeLocInfo)); - }; function App() { return ( @@ -6536,47 +6521,31 @@ describe('ReactDOMFizzServer', () => { ); } - try { - await act(async () => { - const {pipe} = renderToPipeableStream(); - pipe(writable); - }); + await act(async () => { + const {pipe} = renderToPipeableStream(); + pipe(writable); + }); - if (__DEV__) { - expect(mockError.mock.calls.length).toBe(3); - expect(mockError.mock.calls[0]).toEqual([ - 'A script element was rendered with %s. If script element has children it must be a single string. Consider using dangerouslySetInnerHTML or passing a plain string as children.%s', - 'a number for children', - componentStack( - gate(flags => flags.enableOwnerStacks) - ? ['script', 'App'] - : ['script', 'body', 'html', 'App'], - ), - ]); - expect(mockError.mock.calls[1]).toEqual([ - 'A script element was rendered with %s. If script element has children it must be a single string. Consider using dangerouslySetInnerHTML or passing a plain string as children.%s', - 'an array for children', - componentStack( - gate(flags => flags.enableOwnerStacks) - ? ['script', 'App'] - : ['script', 'body', 'html', 'App'], - ), - ]); - expect(mockError.mock.calls[2]).toEqual([ - 'A script element was rendered with %s. If script element has children it must be a single string. Consider using dangerouslySetInnerHTML or passing a plain string as children.%s', - 'something unexpected for children', - componentStack( - gate(flags => flags.enableOwnerStacks) - ? ['script', 'App'] - : ['script', 'body', 'html', 'App'], - ), - ]); - } else { - expect(mockError.mock.calls.length).toBe(0); - } - } finally { - console.error = originalConsoleError; - } + assertConsoleErrorDev([ + 'A script element was rendered with a number for children. If script element has children it must be a single string. Consider using dangerouslySetInnerHTML or passing a plain string as children.' + + componentStack( + gate(flags => flags.enableOwnerStacks) + ? ['script', 'App'] + : ['script', 'body', 'html', 'App'], + ), + 'A script element was rendered with an array for children. If script element has children it must be a single string. Consider using dangerouslySetInnerHTML or passing a plain string as children.' + + componentStack( + gate(flags => flags.enableOwnerStacks) + ? ['script', 'App'] + : ['script', 'body', 'html', 'App'], + ), + 'A script element was rendered with something unexpected for children. If script element has children it must be a single string. Consider using dangerouslySetInnerHTML or passing a plain string as children.' + + componentStack( + gate(flags => flags.enableOwnerStacks) + ? ['script', 'App'] + : ['script', 'body', 'html', 'App'], + ), + ]); }); // @gate enablePostpone diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzServerNode-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzServerNode-test.js index eb41a627b7..1e93c2420b 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFizzServerNode-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFizzServerNode-test.js @@ -585,7 +585,7 @@ describe('ReactDOMFizzServerNode', () => { let isComplete = false; let rendered = false; const promise = new Promise(r => (resolve = r)); - function Wait() { + function Wait({prop}) { if (!hasLoaded) { throw promise; } diff --git a/packages/react-dom/src/__tests__/ReactDOMForm-test.js b/packages/react-dom/src/__tests__/ReactDOMForm-test.js index 4c3ebecccb..7ba4bea06b 100644 --- a/packages/react-dom/src/__tests__/ReactDOMForm-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMForm-test.js @@ -385,12 +385,16 @@ describe('ReactDOMForm', () => { , ); }); - }).toErrorDev([ + }).toErrorDev( 'In HTML,
cannot be a descendant of .\n' + - 'This will cause a hydration error.' + + 'This will cause a hydration error.\n' + + '\n' + + '> \n' + + ' \n' + + '> \n' + '\n in form (at **)' + - '\n in form (at **)', - ]); + (gate(flags => flags.enableOwnerStacks) ? '' : '\n in form (at **)'), + ); await submit(ref.current); diff --git a/packages/react-dom/src/__tests__/ReactDOMLegacyFiber-test.js b/packages/react-dom/src/__tests__/ReactDOMLegacyFiber-test.js index b81dea9f18..c799e3b904 100644 --- a/packages/react-dom/src/__tests__/ReactDOMLegacyFiber-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMLegacyFiber-test.js @@ -786,7 +786,12 @@ describe('ReactDOMLegacyFiber', () => { } } - ReactDOM.render(, container); + expect(() => { + ReactDOM.render(, container); + }).toErrorDev([ + 'Parent uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.', + 'Component uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.', + ]); expect(container.innerHTML).toBe(''); expect(portalContainer.innerHTML).toBe('
bar
'); }); @@ -829,7 +834,13 @@ describe('ReactDOMLegacyFiber', () => { } } - const instance = ReactDOM.render(, container); + let instance; + expect(() => { + instance = ReactDOM.render(, container); + }).toErrorDev([ + 'Parent uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.', + 'Component uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.', + ]); expect(portalContainer.innerHTML).toBe('
initial-initial
'); expect(container.innerHTML).toBe(''); instance.setState({bar: 'changed'}); @@ -871,7 +882,12 @@ describe('ReactDOMLegacyFiber', () => { } } - ReactDOM.render(, container); + expect(() => { + ReactDOM.render(, container); + }).toErrorDev([ + 'Parent uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.', + 'Component uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.', + ]); expect(portalContainer.innerHTML).toBe('
initial-initial
'); expect(container.innerHTML).toBe(''); ReactDOM.render(, container); diff --git a/packages/react-dom/src/__tests__/ReactDOMOption-test.js b/packages/react-dom/src/__tests__/ReactDOMOption-test.js index dab7f69b27..ce5e3c65bc 100644 --- a/packages/react-dom/src/__tests__/ReactDOMOption-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMOption-test.js @@ -53,8 +53,15 @@ describe('ReactDOMOption', () => { }).toErrorDev( 'In HTML,
cannot be a child of