diff --git a/.codesandbox/ci.json b/.codesandbox/ci.json index f644328ad927e..2164efec0c8fa 100644 --- a/.codesandbox/ci.json +++ b/.codesandbox/ci.json @@ -1,7 +1,7 @@ { "packages": ["packages/react", "packages/react-dom", "packages/react-server-dom-webpack", "packages/scheduler"], "buildCommand": "download-build-in-codesandbox-ci", - "node": "18", + "node": "20", "publishDirectory": { "react": "build/oss-experimental/react", "react-dom": "build/oss-experimental/react-dom", diff --git a/.eslintignore b/.eslintignore index c30542a3f7e2c..fd9cc6bdca2fe 100644 --- a/.eslintignore +++ b/.eslintignore @@ -28,3 +28,6 @@ packages/react-devtools-shared/src/hooks/__tests__/__source__/__untransformed__/ packages/react-devtools-shell/dist packages/react-devtools-timeline/dist packages/react-devtools-timeline/static + +# Imported third-party Flow types +flow-typed/ diff --git a/.eslintrc.js b/.eslintrc.js index 49846c1f5e9bc..4f902576ad82c 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -468,13 +468,14 @@ module.exports = { files: ['packages/react-server-dom-webpack/**/*.js'], globals: { __webpack_chunk_load__: 'readonly', + __webpack_get_script_filename__: 'readonly', __webpack_require__: 'readonly', }, }, { files: ['packages/react-server-dom-turbopack/**/*.js'], globals: { - __turbopack_load__: 'readonly', + __turbopack_load_by_url__: 'readonly', __turbopack_require__: 'readonly', }, }, @@ -496,6 +497,7 @@ module.exports = { 'packages/react-devtools-shared/src/devtools/views/**/*.js', 'packages/react-devtools-shared/src/hook.js', 'packages/react-devtools-shared/src/backend/console.js', + 'packages/react-devtools-shared/src/backend/fiber/renderer.js', 'packages/react-devtools-shared/src/backend/shared/DevToolsComponentStackFrame.js', 'packages/react-devtools-shared/src/frontend/utils/withPermissionsCheck.js', ], @@ -504,6 +506,7 @@ module.exports = { __IS_FIREFOX__: 'readonly', __IS_EDGE__: 'readonly', __IS_NATIVE__: 'readonly', + __IS_INTERNAL_MCP_BUILD__: 'readonly', __IS_INTERNAL_VERSION__: 'readonly', chrome: 'readonly', }, @@ -544,13 +547,10 @@ module.exports = { }, globals: { - $Call: 'readonly', - $ElementType: 'readonly', $Flow$ModuleRef: 'readonly', $FlowFixMe: 'readonly', $Keys: 'readonly', $NonMaybeType: 'readonly', - $PropertyType: 'readonly', $ReadOnly: 'readonly', $ReadOnlyArray: 'readonly', $ArrayBufferView: 'readonly', @@ -559,11 +559,13 @@ module.exports = { ConsoleTask: 'readonly', // TOOD: Figure out what the official name of this will be. ReturnType: 'readonly', AnimationFrameID: 'readonly', + WeakRef: 'readonly', // For Flow type annotation. Only `BigInt` is valid at runtime. bigint: 'readonly', BigInt: 'readonly', BigInt64Array: 'readonly', BigUint64Array: 'readonly', + CacheType: 'readonly', Class: 'readonly', ClientRect: 'readonly', CopyInspectedElementPath: 'readonly', @@ -575,15 +577,19 @@ module.exports = { $AsyncIterator: 'readonly', Iterator: 'readonly', AsyncIterator: 'readonly', + IntervalID: 'readonly', IteratorResult: 'readonly', JSONValue: 'readonly', JSResourceReference: 'readonly', + mixin$Animatable: 'readonly', MouseEventHandler: 'readonly', + NavigateEvent: 'readonly', + PerformanceMeasureOptions: 'readonly', PropagationPhases: 'readonly', PropertyDescriptor: 'readonly', - React$AbstractComponent: 'readonly', + PropertyDescriptorMap: 'readonly', + Proxy$traps: 'readonly', React$Component: 'readonly', - React$ComponentType: 'readonly', React$Config: 'readonly', React$Context: 'readonly', React$Element: 'readonly', @@ -604,19 +610,21 @@ module.exports = { symbol: 'readonly', SyntheticEvent: 'readonly', SyntheticMouseEvent: 'readonly', + SyntheticPointerEvent: 'readonly', Thenable: 'readonly', TimeoutID: 'readonly', WheelEventHandler: 'readonly', FinalizationRegistry: 'readonly', + Exclude: 'readonly', Omit: 'readonly', Keyframe: 'readonly', PropertyIndexedKeyframes: 'readonly', KeyframeAnimationOptions: 'readonly', GetAnimationsOptions: 'readonly', - Animatable: 'readonly', ScrollTimeline: 'readonly', EventListenerOptionsOrUseCapture: 'readonly', FocusOptions: 'readonly', + OptionalEffectTiming: 'readonly', spyOnDev: 'readonly', spyOnDevAndProd: 'readonly', @@ -634,5 +642,6 @@ module.exports = { AsyncLocalStorage: 'readonly', async_hooks: 'readonly', globalThis: 'readonly', + navigation: 'readonly', }, }; diff --git a/.github/workflows/compiler_discord_notify.yml b/.github/workflows/compiler_discord_notify.yml index 71aea56e8492f..5a57cf6a32c19 100644 --- a/.github/workflows/compiler_discord_notify.yml +++ b/.github/workflows/compiler_discord_notify.yml @@ -11,10 +11,12 @@ permissions: {} jobs: check_access: + if: ${{ github.event.pull_request.draft == false }} runs-on: ubuntu-latest outputs: is_member_or_collaborator: ${{ steps.check_is_member_or_collaborator.outputs.is_member_or_collaborator }} steps: + - run: echo ${{ github.event.pull_request.author_association }} - name: Check is member or collaborator id: check_is_member_or_collaborator if: ${{ github.event.pull_request.author_association == 'MEMBER' || github.event.pull_request.author_association == 'COLLABORATOR' }} diff --git a/.github/workflows/compiler_playground.yml b/.github/workflows/compiler_playground.yml index 34349f584ef26..a19e87e25e78e 100644 --- a/.github/workflows/compiler_playground.yml +++ b/.github/workflows/compiler_playground.yml @@ -57,8 +57,6 @@ jobs: key: playwright-browsers-v6-${{ runner.arch }}-${{ runner.os }}-${{ steps.playwright_version.outputs.playwright_version }} - run: npx playwright install --with-deps chromium if: steps.cache_playwright_browsers.outputs.cache-hit != 'true' - - run: npx playwright install-deps - if: steps.cache_playwright_browsers.outputs.cache-hit == 'true' - run: CI=true yarn test - run: ls -R test-results if: '!cancelled()' diff --git a/.github/workflows/runtime_build_and_test.yml b/.github/workflows/runtime_build_and_test.yml index 1d0a896984e26..d9fb47da3b204 100644 --- a/.github/workflows/runtime_build_and_test.yml +++ b/.github/workflows/runtime_build_and_test.yml @@ -6,6 +6,12 @@ on: pull_request: paths-ignore: - compiler/** + workflow_dispatch: + inputs: + commit_sha: + required: false + type: string + default: '' permissions: {} @@ -28,7 +34,7 @@ jobs: steps: - uses: actions/checkout@v4 with: - ref: ${{ github.event.pull_request.head.sha || github.sha }} + ref: ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }} - name: Check cache hit uses: actions/cache/restore@v4 id: node_modules @@ -69,7 +75,7 @@ jobs: steps: - uses: actions/checkout@v4 with: - ref: ${{ github.event.pull_request.head.sha || github.sha }} + ref: ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }} - name: Check cache hit uses: actions/cache/restore@v4 id: node_modules @@ -117,7 +123,7 @@ jobs: steps: - uses: actions/checkout@v4 with: - ref: ${{ github.event.pull_request.head.sha || github.sha }} + ref: ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }} - uses: actions/github-script@v7 id: set-matrix with: @@ -136,7 +142,7 @@ jobs: steps: - uses: actions/checkout@v4 with: - ref: ${{ github.event.pull_request.head.sha || github.sha }} + ref: ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }} - uses: actions/setup-node@v4 with: node-version-file: '.nvmrc' @@ -166,7 +172,7 @@ jobs: steps: - uses: actions/checkout@v4 with: - ref: ${{ github.event.pull_request.head.sha || github.sha }} + ref: ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }} - uses: actions/setup-node@v4 with: node-version-file: '.nvmrc' @@ -198,7 +204,7 @@ jobs: steps: - uses: actions/checkout@v4 with: - ref: ${{ github.event.pull_request.head.sha || github.sha }} + ref: ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }} - uses: actions/setup-node@v4 with: node-version-file: '.nvmrc' @@ -254,7 +260,7 @@ jobs: steps: - uses: actions/checkout@v4 with: - ref: ${{ github.event.pull_request.head.sha || github.sha }} + ref: ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }} - uses: actions/setup-node@v4 with: node-version-file: '.nvmrc' @@ -280,6 +286,37 @@ jobs: if: steps.node_modules.outputs.cache-hit != 'true' - run: yarn test ${{ matrix.params }} --ci --shard=${{ matrix.shard }} + # Hardcoded to improve parallelism + test-linter: + name: Test eslint-plugin-react-hooks + needs: [runtime_compiler_node_modules_cache] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version-file: '.nvmrc' + cache: yarn + cache-dependency-path: | + yarn.lock + compiler/yarn.lock + - name: Restore cached node_modules + uses: actions/cache@v4 + id: node_modules + with: + path: | + **/node_modules + key: runtime-and-compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }} + - name: Install runtime dependencies + run: yarn install --frozen-lockfile + if: steps.node_modules.outputs.cache-hit != 'true' + - name: Install compiler dependencies + run: yarn install --frozen-lockfile + working-directory: compiler + if: steps.node_modules.outputs.cache-hit != 'true' + - run: ./scripts/react-compiler/build-compiler.sh && ./scripts/react-compiler/link-compiler.sh + - run: yarn workspace eslint-plugin-react-hooks test + # ----- BUILD ----- build_and_lint: name: yarn build and lint @@ -294,7 +331,7 @@ jobs: steps: - uses: actions/checkout@v4 with: - ref: ${{ github.event.pull_request.head.sha || github.sha }} + ref: ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }} - uses: actions/setup-node@v4 with: node-version-file: '.nvmrc' @@ -389,7 +426,7 @@ jobs: steps: - uses: actions/checkout@v4 with: - ref: ${{ github.event.pull_request.head.sha || github.sha }} + ref: ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }} - uses: actions/setup-node@v4 with: node-version-file: '.nvmrc' @@ -434,7 +471,7 @@ jobs: steps: - uses: actions/checkout@v4 with: - ref: ${{ github.event.pull_request.head.sha || github.sha }} + ref: ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }} - uses: actions/setup-node@v4 with: node-version-file: '.nvmrc' @@ -462,7 +499,7 @@ jobs: merge-multiple: true - name: Display structure of build run: ls -R build - - run: echo ${{ github.event.pull_request.head.sha || github.sha }} >> build/COMMIT_SHA + - run: echo ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }} >> build/COMMIT_SHA - name: Scrape warning messages run: | mkdir -p ./build/__test_utils__ @@ -499,7 +536,7 @@ jobs: steps: - uses: actions/checkout@v4 with: - ref: ${{ github.event.pull_request.head.sha || github.sha }} + ref: ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }} - uses: actions/setup-node@v4 with: node-version-file: '.nvmrc' @@ -539,7 +576,7 @@ jobs: steps: - uses: actions/checkout@v4 with: - ref: ${{ github.event.pull_request.head.sha || github.sha }} + ref: ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }} - uses: actions/setup-node@v4 with: node-version-file: '.nvmrc' @@ -576,7 +613,7 @@ jobs: steps: - uses: actions/checkout@v4 with: - ref: ${{ github.event.pull_request.head.sha || github.sha }} + ref: ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }} - uses: actions/setup-node@v4 with: node-version-file: '.nvmrc' @@ -617,7 +654,7 @@ jobs: steps: - uses: actions/checkout@v4 with: - ref: ${{ github.event.pull_request.head.sha || github.sha }} + ref: ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }} - uses: actions/setup-node@v4 with: node-version-file: '.nvmrc' @@ -691,7 +728,7 @@ jobs: steps: - uses: actions/checkout@v4 with: - ref: ${{ github.event.pull_request.head.sha || github.sha }} + ref: ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }} - uses: actions/setup-node@v4 with: node-version-file: '.nvmrc' @@ -748,7 +785,7 @@ jobs: steps: - uses: actions/checkout@v4 with: - ref: ${{ github.event.pull_request.head.sha || github.sha }} + ref: ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }} - uses: actions/setup-node@v4 with: node-version-file: '.nvmrc' @@ -774,9 +811,18 @@ jobs: pattern: _build_* path: build merge-multiple: true - - run: | - npx playwright install - sudo npx playwright install-deps + - name: Check Playwright version + id: playwright_version + run: echo "playwright_version=$(npm ls @playwright/test | grep @playwright | sed 's/.*@//' | head -1)" >> "$GITHUB_OUTPUT" + - name: Cache Playwright Browsers for version ${{ steps.playwright_version.outputs.playwright_version }} + id: cache_playwright_browsers + uses: actions/cache@v4 + with: + path: ~/.cache/ms-playwright + key: playwright-browsers-v6-${{ runner.arch }}-${{ runner.os }}-${{ steps.playwright_version.outputs.playwright_version }} + - name: Playwright install deps + if: steps.cache_playwright_browsers.outputs.cache-hit != 'true' + run: npx playwright install --with-deps chromium - run: ./scripts/ci/run_devtools_e2e_tests.js env: RELEASE_CHANNEL: experimental @@ -793,7 +839,7 @@ jobs: steps: - uses: actions/checkout@v4 with: - ref: ${{ github.event.pull_request.head.sha || github.sha }} + ref: ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }} - uses: actions/setup-node@v4 with: node-version-file: '.nvmrc' @@ -842,7 +888,7 @@ jobs: node ./scripts/print-warnings/print-warnings.js > build/__test_utils__/ReactAllWarnings.js - name: Display structure of build for PR run: ls -R build - - run: echo ${{ github.event.pull_request.head.sha || github.sha }} >> build/COMMIT_SHA + - run: echo ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }} >> build/COMMIT_SHA - run: node ./scripts/tasks/danger - name: Archive sizebot results uses: actions/upload-artifact@v4 diff --git a/.github/workflows/runtime_commit_artifacts.yml b/.github/workflows/runtime_commit_artifacts.yml index ab0e71b83cfc7..b982d561ed71c 100644 --- a/.github/workflows/runtime_commit_artifacts.yml +++ b/.github/workflows/runtime_commit_artifacts.yml @@ -332,10 +332,10 @@ jobs: git --no-pager diff -U0 --cached | grep '^[+-]' | head -n 100 echo "====================" # Ignore REVISION or lines removing @generated headers. - if git diff --cached ':(exclude)*REVISION' | grep -vE "^(@@|diff|index|\-\-\-|\+\+\+|\- \* @generated SignedSource)" | grep "^[+-]" > /dev/null; then + if git diff --cached ':(exclude)*REVISION' ':(exclude)*/eslint-plugin-react-hooks/package.json' | grep -vE "^(@@|diff|index|\-\-\-|\+\+\+|\- \* @generated SignedSource)" | grep "^[+-]" > /dev/null; then echo "Changes detected" echo "===== Changes =====" - git --no-pager diff --cached ':(exclude)*REVISION' | grep -vE "^(@@|diff|index|\-\-\-|\+\+\+|\- \* @generated SignedSource)" | grep "^[+-]" | head -n 50 + git --no-pager diff --cached ':(exclude)*REVISION' ':(exclude)*/eslint-plugin-react-hooks/package.json' | grep -vE "^(@@|diff|index|\-\-\-|\+\+\+|\- \* @generated SignedSource)" | grep "^[+-]" | head -n 50 echo "===================" echo "should_commit=true" >> "$GITHUB_OUTPUT" else diff --git a/.github/workflows/runtime_discord_notify.yml b/.github/workflows/runtime_discord_notify.yml index 44775fbe78880..8d047e697640d 100644 --- a/.github/workflows/runtime_discord_notify.yml +++ b/.github/workflows/runtime_discord_notify.yml @@ -11,10 +11,12 @@ permissions: {} jobs: check_access: + if: ${{ github.event.pull_request.draft == false }} runs-on: ubuntu-latest outputs: is_member_or_collaborator: ${{ steps.check_is_member_or_collaborator.outputs.is_member_or_collaborator }} steps: + - run: echo ${{ github.event.pull_request.author_association }} - name: Check is member or collaborator id: check_is_member_or_collaborator if: ${{ github.event.pull_request.author_association == 'MEMBER' || github.event.pull_request.author_association == 'COLLABORATOR' }} diff --git a/.github/workflows/runtime_prereleases.yml b/.github/workflows/runtime_prereleases.yml index ee8dd72ce9665..ee340e079f3d3 100644 --- a/.github/workflows/runtime_prereleases.yml +++ b/.github/workflows/runtime_prereleases.yml @@ -17,6 +17,17 @@ on: description: 'Whether to notify the team on Discord when the release fails. Useful if this workflow is called from an automation.' required: false type: boolean + only_packages: + description: Packages to publish (space separated) + type: string + skip_packages: + description: Packages to NOT publish (space separated) + type: string + dry: + required: true + description: Dry run instead of publish? + type: boolean + default: true secrets: DISCORD_WEBHOOK_URL: description: 'Discord webhook URL to notify on failure. Only required if enableFailureNotification is true.' @@ -61,15 +72,41 @@ jobs: if: steps.node_modules.outputs.cache-hit != 'true' - run: yarn --cwd scripts/release install --frozen-lockfile if: steps.node_modules.outputs.cache-hit != 'true' + - run: cp ./scripts/release/ci-npmrc ~/.npmrc - run: | GH_TOKEN=${{ secrets.GH_TOKEN }} scripts/release/prepare-release-from-ci.js --skipTests -r ${{ inputs.release_channel }} --commit=${{ inputs.commit_sha }} - cp ./scripts/release/ci-npmrc ~/.npmrc - scripts/release/publish.js --ci --tags ${{ inputs.dist_tag }} + - name: Check prepared files + run: ls -R build/node_modules + - if: '${{ inputs.only_packages }}' + name: 'Publish ${{ inputs.only_packages }}' + run: | + scripts/release/publish.js \ + --ci \ + --skipTests \ + --tags=${{ inputs.dist_tag }} \ + --onlyPackages=${{ inputs.only_packages }} ${{ (inputs.dry && '') || '\'}} + ${{ inputs.dry && '--dry' || '' }} + - if: '${{ inputs.skip_packages }}' + name: 'Publish all packages EXCEPT ${{ inputs.skip_packages }}' + run: | + scripts/release/publish.js \ + --ci \ + --skipTests \ + --tags=${{ inputs.dist_tag }} \ + --skipPackages=${{ inputs.skip_packages }} ${{ (inputs.dry && '') || '\'}} + ${{ inputs.dry && '--dry' || '' }} + - if: '${{ !(inputs.skip_packages && inputs.only_packages) }}' + name: 'Publish all packages' + run: | + scripts/release/publish.js \ + --ci \ + --tags=${{ inputs.dist_tag }} ${{ (inputs.dry && '') || '\'}} + ${{ inputs.dry && '--dry' || '' }} - name: Notify Discord on failure if: failure() && inputs.enableFailureNotification == true uses: tsickert/discord-webhook@86dc739f3f165f16dadc5666051c367efa1692f4 with: webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL }} embed-author-name: "GitHub Actions" - embed-title: 'Publish of $${{ inputs.release_channel }} release failed' + embed-title: '[Runtime] Publish of ${{ inputs.release_channel }}@${{ inputs.dist_tag}} release failed' embed-url: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}/attempts/${{ github.run_attempt }} diff --git a/.github/workflows/runtime_prereleases_manual.yml b/.github/workflows/runtime_prereleases_manual.yml index 71e25ba073a83..407d931e90738 100644 --- a/.github/workflows/runtime_prereleases_manual.yml +++ b/.github/workflows/runtime_prereleases_manual.yml @@ -5,6 +5,25 @@ on: inputs: prerelease_commit_sha: required: true + only_packages: + description: Packages to publish (space separated) + type: string + skip_packages: + description: Packages to NOT publish (space separated) + type: string + dry: + required: true + description: Dry run instead of publish? + type: boolean + default: true + experimental_only: + type: boolean + description: Only publish to the experimental tag + default: false + force_notify: + description: Force a Discord notification? + type: boolean + default: false permissions: {} @@ -12,8 +31,26 @@ env: TZ: /usr/share/zoneinfo/America/Los_Angeles jobs: + notify: + if: ${{ inputs.force_notify || inputs.dry == false || inputs.dry == 'false' }} + runs-on: ubuntu-latest + steps: + - name: Discord Webhook Action + uses: tsickert/discord-webhook@86dc739f3f165f16dadc5666051c367efa1692f4 + with: + webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL }} + embed-author-name: ${{ github.event.sender.login }} + embed-author-url: ${{ github.event.sender.html_url }} + embed-author-icon-url: ${{ github.event.sender.avatar_url }} + embed-title: "⚠️ Publishing ${{ inputs.experimental_only && 'EXPERIMENTAL' || 'CANARY & EXPERIMENTAL' }} release ${{ (inputs.dry && ' (dry run)') || '' }}" + embed-description: | + ```json + ${{ toJson(inputs) }} + ``` + embed-url: https://github.com/facebook/react/actions/runs/${{ github.run_id }} publish_prerelease_canary: + if: ${{ !inputs.experimental_only }} name: Publish to Canary channel uses: facebook/react/.github/workflows/runtime_prereleases.yml@main permissions: @@ -33,6 +70,9 @@ jobs: # downstream consumers might still expect that tag. We can remove this # after some time has elapsed and the change has been communicated. dist_tag: canary,next + only_packages: ${{ inputs.only_packages }} + skip_packages: ${{ inputs.skip_packages }} + dry: ${{ inputs.dry }} secrets: NPM_TOKEN: ${{ secrets.NPM_TOKEN }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -48,10 +88,15 @@ jobs: # different versions of the same package, even if they use different # dist tags. needs: publish_prerelease_canary + # Ensures the job runs even if canary is skipped + if: always() with: commit_sha: ${{ inputs.prerelease_commit_sha }} release_channel: experimental dist_tag: experimental + only_packages: ${{ inputs.only_packages }} + skip_packages: ${{ inputs.skip_packages }} + dry: ${{ inputs.dry }} secrets: NPM_TOKEN: ${{ secrets.NPM_TOKEN }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/runtime_prereleases_nightly.yml b/.github/workflows/runtime_prereleases_nightly.yml index a38e241d53996..f13a92e46f401 100644 --- a/.github/workflows/runtime_prereleases_nightly.yml +++ b/.github/workflows/runtime_prereleases_nightly.yml @@ -22,6 +22,7 @@ jobs: release_channel: stable dist_tag: canary,next enableFailureNotification: true + dry: false secrets: DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }} NPM_TOKEN: ${{ secrets.NPM_TOKEN }} @@ -43,6 +44,7 @@ jobs: release_channel: experimental dist_tag: experimental enableFailureNotification: true + dry: false secrets: DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }} NPM_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.github/workflows/runtime_releases_from_npm_manual.yml b/.github/workflows/runtime_releases_from_npm_manual.yml index 51e38439553de..f164e9f080660 100644 --- a/.github/workflows/runtime_releases_from_npm_manual.yml +++ b/.github/workflows/runtime_releases_from_npm_manual.yml @@ -110,7 +110,7 @@ jobs: --tags=${{ inputs.tags }} \ --publishVersion=${{ inputs.version_to_publish }} \ --onlyPackages=${{ inputs.only_packages }} ${{ (inputs.dry && '') || '\'}} - ${{ inputs.dry && '--dry'}} + ${{ inputs.dry && '--dry' || '' }} - if: '${{ inputs.skip_packages }}' name: 'Publish all packages EXCEPT ${{ inputs.skip_packages }}' run: | @@ -119,7 +119,7 @@ jobs: --tags=${{ inputs.tags }} \ --publishVersion=${{ inputs.version_to_publish }} \ --skipPackages=${{ inputs.skip_packages }} ${{ (inputs.dry && '') || '\'}} - ${{ inputs.dry && '--dry'}} + ${{ inputs.dry && '--dry' || '' }} - name: Archive released package for debugging uses: actions/upload-artifact@v4 with: diff --git a/.github/workflows/shared_close_direct_sync_branch_prs.yml b/.github/workflows/shared_close_direct_sync_branch_prs.yml index 01db0907401c0..caa4da880b5ff 100644 --- a/.github/workflows/shared_close_direct_sync_branch_prs.yml +++ b/.github/workflows/shared_close_direct_sync_branch_prs.yml @@ -18,6 +18,7 @@ jobs: permissions: # Used to create a review and close PRs pull-requests: write + contents: write steps: - name: Close PR uses: actions/github-script@v7 diff --git a/.github/workflows/shared_label_core_team_prs.yml b/.github/workflows/shared_label_core_team_prs.yml index fd4aa9399e386..cc10e87dcc2cf 100644 --- a/.github/workflows/shared_label_core_team_prs.yml +++ b/.github/workflows/shared_label_core_team_prs.yml @@ -17,6 +17,7 @@ jobs: outputs: is_member_or_collaborator: ${{ steps.check_is_member_or_collaborator.outputs.is_member_or_collaborator }} steps: + - run: echo ${{ github.event.pull_request.author_association }} - name: Check is member or collaborator id: check_is_member_or_collaborator if: ${{ github.event.pull_request.author_association == 'MEMBER' || github.event.pull_request.author_association == 'COLLABORATOR' }} diff --git a/.github/workflows/shared_stale.yml b/.github/workflows/shared_stale.yml index a2c707973c927..c24895edc5da7 100644 --- a/.github/workflows/shared_stale.yml +++ b/.github/workflows/shared_stale.yml @@ -6,7 +6,10 @@ on: - cron: '0 * * * *' workflow_dispatch: -permissions: {} +permissions: + # https://github.com/actions/stale/tree/v9/?tab=readme-ov-file#recommended-permissions + issues: write + pull-requests: write env: TZ: /usr/share/zoneinfo/America/Los_Angeles diff --git a/.prettierrc.js b/.prettierrc.js index 37cf9c9d3a89b..aa54cbae1f9f8 100644 --- a/.prettierrc.js +++ b/.prettierrc.js @@ -3,13 +3,12 @@ const {esNextPaths} = require('./scripts/shared/pathsByLanguageVersion'); module.exports = { - plugins: ['prettier-plugin-hermes-parser'], bracketSpacing: false, singleQuote: true, bracketSameLine: true, trailingComma: 'es5', printWidth: 80, - parser: 'hermes', + parser: 'flow', arrowParens: 'avoid', overrides: [ { diff --git a/CHANGELOG.md b/CHANGELOG.md index d5551df26c7de..1836850ff3ea2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +## 19.1.1 (July 28, 2025) + +### React +* Fixed Owner Stacks to work with ES2015 function.name semantics ([#33680](https://github.com/facebook/react/pull/33680) by @hoxyq) + ## 19.1.0 (March 28, 2025) ### Owner Stack @@ -19,11 +24,11 @@ An Owner Stack is a string representing the components that are directly respons * Updated `useId` to use valid CSS selectors, changing format from `:r123:` to `«r123»`. [#32001](https://github.com/facebook/react/pull/32001) * Added a dev-only warning for null/undefined created in useEffect, useInsertionEffect, and useLayoutEffect. [#32355](https://github.com/facebook/react/pull/32355) * Fixed a bug where dev-only methods were exported in production builds. React.act is no longer available in production builds. [#32200](https://github.com/facebook/react/pull/32200) -* Improved consistency across prod and dev to improve compatibility with Google Closure Complier and bindings [#31808](https://github.com/facebook/react/pull/31808) +* Improved consistency across prod and dev to improve compatibility with Google Closure Compiler and bindings [#31808](https://github.com/facebook/react/pull/31808) * Improve passive effect scheduling for consistent task yielding. [#31785](https://github.com/facebook/react/pull/31785) * Fixed asserts in React Native when passChildrenWhenCloningPersistedNodes is enabled for OffscreenComponent rendering. [#32528](https://github.com/facebook/react/pull/32528) * Fixed component name resolution for Portal [#32640](https://github.com/facebook/react/pull/32640) -* Added support for beforetoggle and toggle events on the dialog element. #32479 [#32479](https://github.com/facebook/react/pull/32479) +* Added support for beforetoggle and toggle events on the dialog element. [#32479](https://github.com/facebook/react/pull/32479) ### React DOM * Fixed double warning when the `href` attribute is an empty string [#31783](https://github.com/facebook/react/pull/31783) diff --git a/babel.config-ts.js b/babel.config-ts.js index 45efe2a870820..02ffb447b3719 100644 --- a/babel.config-ts.js +++ b/babel.config-ts.js @@ -8,6 +8,7 @@ module.exports = { '@babel/plugin-syntax-jsx', '@babel/plugin-transform-flow-strip-types', ['@babel/plugin-transform-class-properties', {loose: true}], + ['@babel/plugin-transform-private-methods', {loose: true}], '@babel/plugin-transform-classes', ], presets: [ diff --git a/compiler/.gitignore b/compiler/.gitignore index d41f59333aa09..77e4c01bef707 100644 --- a/compiler/.gitignore +++ b/compiler/.gitignore @@ -1,28 +1,14 @@ .DS_Store .spr.yml -# Generated by Cargo -# will have compiled files and executables -debug/ -target/ - -# These are backup files generated by rustfmt -**/*.rs.bk - -# MSVC Windows builds of rustc generate these, which store debugging information -*.pdb - node_modules .watchmanconfig .watchman-cookie-* dist .vscode !packages/playground/.vscode -.spr.yml testfilter.txt -bundle-oss.sh - # forgive *.vsix .vscode-test diff --git a/compiler/CHANGELOG.md b/compiler/CHANGELOG.md index 022d066b2202f..32e21efba0cd5 100644 --- a/compiler/CHANGELOG.md +++ b/compiler/CHANGELOG.md @@ -1,3 +1,9 @@ +## 19.1.0-rc.2 (May 14, 2025) + +## babel-plugin-react-compiler + +* Fix for string attribute values with emoji [#33096](https://github.com/facebook/react/pull/33096) by [@josephsavona](https://github.com/josephsavona) + ## 19.1.0-rc.1 (April 21, 2025) ## eslint-plugin-react-hooks diff --git a/compiler/apps/playground/__tests__/e2e/__snapshots__/page.spec.ts/compilationMode-all-output.txt b/compiler/apps/playground/__tests__/e2e/__snapshots__/page.spec.ts/compilationMode-all-output.txt index 0f35215e86d71..0084911eec1b7 100644 --- a/compiler/apps/playground/__tests__/e2e/__snapshots__/page.spec.ts/compilationMode-all-output.txt +++ b/compiler/apps/playground/__tests__/e2e/__snapshots__/page.spec.ts/compilationMode-all-output.txt @@ -1,5 +1,5 @@ import { c as _c } from "react/compiler-runtime"; //  -        @compilationMode:"all" +@compilationMode:"all" function nonReactFn() {   const $ = _c(1);   let t0; diff --git a/compiler/apps/playground/components/Editor/ConfigEditor.tsx b/compiler/apps/playground/components/Editor/ConfigEditor.tsx new file mode 100644 index 0000000000000..ce0e502fac21d --- /dev/null +++ b/compiler/apps/playground/components/Editor/ConfigEditor.tsx @@ -0,0 +1,101 @@ +/** + * 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 MonacoEditor, {loader, type Monaco} from '@monaco-editor/react'; +import type {editor} from 'monaco-editor'; +import * as monaco from 'monaco-editor'; +import {useState} from 'react'; +import {Resizable} from 're-resizable'; +import {useStore, useStoreDispatch} from '../StoreContext'; +import {monacoOptions} from './monacoOptions'; +import { + generateOverridePragmaFromConfig, + updateSourceWithOverridePragma, +} from '../../lib/configUtils'; + +loader.config({monaco}); + +export default function ConfigEditor(): JSX.Element { + const [, setMonaco] = useState(null); + const store = useStore(); + const dispatchStore = useStoreDispatch(); + + const handleChange: (value: string | undefined) => void = async value => { + if (value === undefined) return; + + try { + const newPragma = await generateOverridePragmaFromConfig(value); + const updatedSource = updateSourceWithOverridePragma( + store.source, + newPragma, + ); + + // Update the store with both the new config and updated source + dispatchStore({ + type: 'updateFile', + payload: { + source: updatedSource, + config: value, + }, + }); + } catch (_) { + dispatchStore({ + type: 'updateFile', + payload: { + source: store.source, + config: value, + }, + }); + } + }; + + const handleMount: ( + _: editor.IStandaloneCodeEditor, + monaco: Monaco, + ) => void = (_, monaco) => { + setMonaco(monaco); + + const uri = monaco.Uri.parse(`file:///config.js`); + const model = monaco.editor.getModel(uri); + if (model) { + model.updateOptions({tabSize: 2}); + } + }; + + return ( +
+

+ Config Overrides +

+ + + +
+ ); +} diff --git a/compiler/apps/playground/components/Editor/EditorImpl.tsx b/compiler/apps/playground/components/Editor/EditorImpl.tsx index 39571fa092593..6d7dd73e6d148 100644 --- a/compiler/apps/playground/components/Editor/EditorImpl.tsx +++ b/compiler/apps/playground/components/Editor/EditorImpl.tsx @@ -11,6 +11,7 @@ import * as t from '@babel/types'; import BabelPluginReactCompiler, { CompilerError, CompilerErrorDetail, + CompilerDiagnostic, Effect, ErrorSeverity, parseConfigPragmaForTests, @@ -36,6 +37,7 @@ import { type Store, } from '../../lib/stores'; import {useStore, useStoreDispatch} from '../StoreContext'; +import ConfigEditor from './ConfigEditor'; import Input from './Input'; import { CompilerOutput, @@ -44,6 +46,9 @@ import { PrintedCompilerPipelineValue, } from './Output'; import {transformFromAstSync} from '@babel/core'; +import {LoggerEvent} from 'babel-plugin-react-compiler/dist/Entrypoint'; +import {useSearchParams} from 'next/navigation'; +import {parseAndFormatConfig} from '../../lib/configUtils'; function parseInput( input: string, @@ -140,9 +145,13 @@ const COMMON_HOOKS: Array<[string, Hook]> = [ ], ]; -function compile(source: string): [CompilerOutput, 'flow' | 'typescript'] { +function compile( + source: string, + mode: 'compiler' | 'linter', +): [CompilerOutput, 'flow' | 'typescript'] { const results = new Map>(); const error = new CompilerError(); + const otherErrors: Array = []; const upsert: (result: PrintedCompilerPipelineValue) => void = result => { const entry = results.get(result.name); if (Array.isArray(entry)) { @@ -201,6 +210,23 @@ function compile(source: string): [CompilerOutput, 'flow' | 'typescript'] { }; const parsedOptions = parseConfigPragmaForTests(pragma, { compilationMode: 'infer', + environment: + mode === 'linter' + ? { + // enabled in compiler + validateRefAccessDuringRender: false, + // enabled in linter + validateNoSetStateInRender: true, + validateNoSetStateInEffects: true, + validateNoJSXInTryStatements: true, + validateNoImpureFunctionsInRender: true, + validateStaticComponents: true, + validateNoFreezingKnownMutableFunctions: true, + validateNoVoidUseMemo: true, + } + : { + /* use defaults for compiler mode */ + }, }); const opts: PluginOptions = parsePluginOptions({ ...parsedOptions, @@ -210,7 +236,11 @@ function compile(source: string): [CompilerOutput, 'flow' | 'typescript'] { }, logger: { debugLogIRs: logIR, - logEvent: () => {}, + logEvent: (_filename: string | null, event: LoggerEvent) => { + if (event.kind === 'CompileError') { + otherErrors.push(event.detail); + } + }, }, }); transformOutput = invokeCompiler(source, language, opts); @@ -220,7 +250,7 @@ function compile(source: string): [CompilerOutput, 'flow' | 'typescript'] { * (i.e. object shape that is not CompilerError) */ if (err instanceof CompilerError && err.details.length > 0) { - error.details.push(...err.details); + error.merge(err); } else { /** * Handle unexpected failures by logging (to get a stack trace) @@ -237,10 +267,17 @@ function compile(source: string): [CompilerOutput, 'flow' | 'typescript'] { ); } } + // Only include logger errors if there weren't other errors + if (!error.hasErrors() && otherErrors.length !== 0) { + otherErrors.forEach(e => error.details.push(e)); + } if (error.hasErrors()) { - return [{kind: 'err', results, error: error}, language]; + return [{kind: 'err', results, error}, language]; } - return [{kind: 'ok', results, transformOutput}, language]; + return [ + {kind: 'ok', results, transformOutput, errors: error.details}, + language, + ]; } export default function Editor(): JSX.Element { @@ -249,11 +286,21 @@ export default function Editor(): JSX.Element { const dispatchStore = useStoreDispatch(); const {enqueueSnackbar} = useSnackbar(); const [compilerOutput, language] = useMemo( - () => compile(deferredStore.source), + () => compile(deferredStore.source, 'compiler'), [deferredStore.source], ); + const [linterOutput] = useMemo( + () => compile(deferredStore.source, 'linter'), + [deferredStore.source], + ); + + // TODO: Remove this once the config editor is more stable + const searchParams = useSearchParams(); + const search = searchParams.get('showConfig'); + const shouldShowConfig = search === 'true'; useMountEffect(() => { + // Initialize store let mountStore: Store; try { mountStore = initStoreFromUrlOrLocalStorage(); @@ -269,25 +316,41 @@ export default function Editor(): JSX.Element { }); mountStore = defaultStore; } - dispatchStore({ - type: 'setStore', - payload: {store: mountStore}, + + parseAndFormatConfig(mountStore.source).then(config => { + dispatchStore({ + type: 'setStore', + payload: { + store: { + ...mountStore, + config, + }, + }, + }); }); }); + let mergedOutput: CompilerOutput; + let errors: Array; + if (compilerOutput.kind === 'ok') { + errors = linterOutput.kind === 'ok' ? [] : linterOutput.error.details; + mergedOutput = { + ...compilerOutput, + errors, + }; + } else { + mergedOutput = compilerOutput; + errors = compilerOutput.error.details; + } return ( <>
+ {shouldShowConfig && }
- +
- +
diff --git a/compiler/apps/playground/components/Editor/Input.tsx b/compiler/apps/playground/components/Editor/Input.tsx index 0992591183c15..0441f3f9a4be0 100644 --- a/compiler/apps/playground/components/Editor/Input.tsx +++ b/compiler/apps/playground/components/Editor/Input.tsx @@ -17,6 +17,7 @@ import {useStore, useStoreDispatch} from '../StoreContext'; import {monacoOptions} from './monacoOptions'; // @ts-expect-error TODO: Make TS recognize .d.ts files, in addition to loading them with webpack. import React$Types from '../../node_modules/@types/react/index.d.ts'; +import {parseAndFormatConfig} from '../../lib/configUtils.ts'; loader.config({monaco}); @@ -36,13 +37,18 @@ export default function Input({errors, language}: Props): JSX.Element { const uri = monaco.Uri.parse(`file:///index.js`); const model = monaco.editor.getModel(uri); invariant(model, 'Model must exist for the selected input file.'); - renderReactCompilerMarkers({monaco, model, details: errors}); + renderReactCompilerMarkers({ + monaco, + model, + details: errors, + source: store.source, + }); /** * N.B. that `tabSize` is a model property, not an editor property. * So, the tab size has to be set per model. */ model.updateOptions({tabSize: 2}); - }, [monaco, errors]); + }, [monaco, errors, store.source]); useEffect(() => { /** @@ -74,13 +80,17 @@ export default function Input({errors, language}: Props): JSX.Element { }); }, [monaco, language]); - const handleChange: (value: string | undefined) => void = value => { + const handleChange: (value: string | undefined) => void = async value => { if (!value) return; + // Parse and format the config + const config = await parseAndFormatConfig(value); + dispatchStore({ type: 'updateFile', payload: { source: value, + config, }, }); }; diff --git a/compiler/apps/playground/components/Editor/Output.tsx b/compiler/apps/playground/components/Editor/Output.tsx index 7886f11e62370..bf7bd3eb65078 100644 --- a/compiler/apps/playground/components/Editor/Output.tsx +++ b/compiler/apps/playground/components/Editor/Output.tsx @@ -11,7 +11,11 @@ import { InformationCircleIcon, } from '@heroicons/react/outline'; import MonacoEditor, {DiffEditor} from '@monaco-editor/react'; -import {type CompilerError} from 'babel-plugin-react-compiler'; +import { + CompilerErrorDetail, + CompilerDiagnostic, + type CompilerError, +} from 'babel-plugin-react-compiler'; import parserBabel from 'prettier/plugins/babel'; import * as prettierPluginEstree from 'prettier/plugins/estree'; import * as prettier from 'prettier/standalone'; @@ -44,6 +48,7 @@ export type CompilerOutput = kind: 'ok'; transformOutput: CompilerTransformOutput; results: Map>; + errors: Array; } | { kind: 'err'; @@ -123,10 +128,36 @@ async function tabify( parser: transformOutput.language === 'flow' ? 'babel-flow' : 'babel-ts', plugins: [parserBabel, prettierPluginEstree], }); + + let output: string; + let language: string; + if (compilerOutput.errors.length === 0) { + output = code; + language = 'javascript'; + } else { + language = 'markdown'; + output = ` +# Summary + +React Compiler compiled this function successfully, but there are lint errors that indicate potential issues with the original code. + +## ${compilerOutput.errors.length} Lint Errors + +${compilerOutput.errors.map(e => e.printErrorMessage(source, {eslint: false})).join('\n\n')} + +## Output + +\`\`\`js +${code} +\`\`\` +`.trim(); + } + reorderedTabs.set( - 'JS', + 'Output', , ); @@ -142,6 +173,18 @@ async function tabify( , ); } + } else if (compilerOutput.kind === 'err') { + const errors = compilerOutput.error.printErrorMessage(source, { + eslint: false, + }); + reorderedTabs.set( + 'Output', + , + ); } tabs.forEach((tab, name) => { reorderedTabs.set(name, tab); @@ -162,17 +205,32 @@ function getSourceMapUrl(code: string, map: string): string | null { } function Output({store, compilerOutput}: Props): JSX.Element { - const [tabsOpen, setTabsOpen] = useState>(() => new Set(['JS'])); + const [tabsOpen, setTabsOpen] = useState>( + () => new Set(['Output']), + ); const [tabs, setTabs] = useState>( () => new Map(), ); + + /* + * Update the active tab back to the output or errors tab when the compilation state + * changes between success/failure. + */ + const [previousOutputKind, setPreviousOutputKind] = useState( + compilerOutput.kind, + ); + if (compilerOutput.kind !== previousOutputKind) { + setPreviousOutputKind(compilerOutput.kind); + setTabsOpen(new Set(['Output'])); + } + useEffect(() => { tabify(store.source, compilerOutput).then(tabs => { setTabs(tabs); }); }, [store.source, compilerOutput]); - const changedPasses: Set = new Set(['JS', 'HIR']); // Initial and final passes should always be bold + const changedPasses: Set = new Set(['Output', 'HIR']); // Initial and final passes should always be bold let lastResult: string = ''; for (const [passName, results] of compilerOutput.results) { for (const result of results) { @@ -196,20 +254,6 @@ function Output({store, compilerOutput}: Props): JSX.Element { tabs={tabs} changedPasses={changedPasses} /> - {compilerOutput.kind === 'err' ? ( -
-
-

COMPILER ERRORS

-
-
-            {compilerOutput.error.toString()}
-          
-
- ) : null} ); } @@ -218,10 +262,12 @@ function TextTabContent({ output, diff, showInfoPanel, + language, }: { output: string; diff: string | null; showInfoPanel: boolean; + language: string; }): JSX.Element { const [diffMode, setDiffMode] = useState(false); return ( @@ -272,7 +318,7 @@ function TextTabContent({ /> ) : ( = { automaticLayout: true, wordWrap: 'on', - wrappingIndent: 'deepIndent', + wrappingIndent: 'same', }; diff --git a/compiler/apps/playground/components/StoreContext.tsx b/compiler/apps/playground/components/StoreContext.tsx index 10ad614b05554..3dfe26cba75ff 100644 --- a/compiler/apps/playground/components/StoreContext.tsx +++ b/compiler/apps/playground/components/StoreContext.tsx @@ -56,6 +56,7 @@ type ReducerAction = type: 'updateFile'; payload: { source: string; + config?: string; }; }; @@ -66,10 +67,11 @@ function storeReducer(store: Store, action: ReducerAction): Store { return newStore; } case 'updateFile': { - const {source} = action.payload; + const {source, config} = action.payload; const newStore = { ...store, source, + config, }; return newStore; } diff --git a/compiler/apps/playground/lib/configUtils.ts b/compiler/apps/playground/lib/configUtils.ts new file mode 100644 index 0000000000000..d987406f99892 --- /dev/null +++ b/compiler/apps/playground/lib/configUtils.ts @@ -0,0 +1,87 @@ +/** + * 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 parserBabel from 'prettier/plugins/babel'; +import prettierPluginEstree from 'prettier/plugins/estree'; +import * as prettier from 'prettier/standalone'; +import {parseConfigPragmaAsString} from '../../../packages/babel-plugin-react-compiler/src/Utils/TestUtils'; + +/** + * Parse config from pragma and format it with prettier + */ +export async function parseAndFormatConfig(source: string): Promise { + const pragma = source.substring(0, source.indexOf('\n')); + let configString = parseConfigPragmaAsString(pragma); + if (configString !== '') { + configString = `(${configString})`; + } + + try { + const formatted = await prettier.format(configString, { + semi: true, + parser: 'babel-ts', + plugins: [parserBabel, prettierPluginEstree], + }); + return formatted; + } catch (error) { + console.error('Error formatting config:', error); + return ''; // Return empty string if not valid for now + } +} + +function extractCurlyBracesContent(input: string): string { + const startIndex = input.indexOf('{'); + const endIndex = input.lastIndexOf('}'); + if (startIndex === -1 || endIndex === -1 || endIndex <= startIndex) { + throw new Error('No outer curly braces found in input'); + } + return input.slice(startIndex, endIndex + 1); +} + +function cleanContent(content: string): string { + return content + .replace(/[\r\n]+/g, ' ') + .replace(/\s+/g, ' ') + .trim(); +} + +/** + * Generate a the override pragma comment from a formatted config object string + */ +export async function generateOverridePragmaFromConfig( + formattedConfigString: string, +): Promise { + const content = extractCurlyBracesContent(formattedConfigString); + const cleanConfig = cleanContent(content); + + // Format the config to ensure it's valid + await prettier.format(`(${cleanConfig})`, { + semi: false, + parser: 'babel-ts', + plugins: [parserBabel, prettierPluginEstree], + }); + + return `// @OVERRIDE:${cleanConfig}`; +} + +/** + * Update the override pragma comment in source code. + */ +export function updateSourceWithOverridePragma( + source: string, + newPragma: string, +): string { + const firstLineEnd = source.indexOf('\n'); + const firstLine = source.substring(0, firstLineEnd); + + const pragmaRegex = /^\/\/\s*@/; + if (firstLineEnd !== -1 && pragmaRegex.test(firstLine.trim())) { + return newPragma + source.substring(firstLineEnd); + } else { + return newPragma + '\n' + source; + } +} diff --git a/compiler/apps/playground/lib/defaultStore.ts b/compiler/apps/playground/lib/defaultStore.ts index 132ab445e18ce..1031a830fa0d9 100644 --- a/compiler/apps/playground/lib/defaultStore.ts +++ b/compiler/apps/playground/lib/defaultStore.ts @@ -15,8 +15,10 @@ export default function MyApp() { export const defaultStore: Store = { source: index, + config: '', }; export const emptyStore: Store = { source: '', + config: '', }; diff --git a/compiler/apps/playground/lib/reactCompilerMonacoDiagnostics.ts b/compiler/apps/playground/lib/reactCompilerMonacoDiagnostics.ts index a800e25773295..cece2fa7c6dd3 100644 --- a/compiler/apps/playground/lib/reactCompilerMonacoDiagnostics.ts +++ b/compiler/apps/playground/lib/reactCompilerMonacoDiagnostics.ts @@ -6,7 +6,11 @@ */ import {Monaco} from '@monaco-editor/react'; -import {CompilerErrorDetail, ErrorSeverity} from 'babel-plugin-react-compiler'; +import { + CompilerDiagnostic, + CompilerErrorDetail, + ErrorSeverity, +} from 'babel-plugin-react-compiler'; import {MarkerSeverity, type editor} from 'monaco-editor'; function mapReactCompilerSeverityToMonaco( @@ -22,38 +26,46 @@ function mapReactCompilerSeverityToMonaco( } function mapReactCompilerDiagnosticToMonacoMarker( - detail: CompilerErrorDetail, + detail: CompilerErrorDetail | CompilerDiagnostic, monaco: Monaco, + source: string, ): editor.IMarkerData | null { - if (detail.loc == null || typeof detail.loc === 'symbol') { + const loc = detail.primaryLocation(); + if (loc == null || typeof loc === 'symbol') { return null; } const severity = mapReactCompilerSeverityToMonaco(detail.severity, monaco); - let message = detail.printErrorMessage(); + let message = detail.printErrorMessage(source, {eslint: true}); return { severity, message, - startLineNumber: detail.loc.start.line, - startColumn: detail.loc.start.column + 1, - endLineNumber: detail.loc.end.line, - endColumn: detail.loc.end.column + 1, + startLineNumber: loc.start.line, + startColumn: loc.start.column + 1, + endLineNumber: loc.end.line, + endColumn: loc.end.column + 1, }; } type ReactCompilerMarkerConfig = { monaco: Monaco; model: editor.ITextModel; - details: Array; + details: Array; + source: string; }; let decorations: Array = []; export function renderReactCompilerMarkers({ monaco, model, details, + source, }: ReactCompilerMarkerConfig): void { const markers: Array = []; for (const detail of details) { - const marker = mapReactCompilerDiagnosticToMonacoMarker(detail, monaco); + const marker = mapReactCompilerDiagnosticToMonacoMarker( + detail, + monaco, + source, + ); if (marker == null) { continue; } diff --git a/compiler/apps/playground/lib/stores/store.ts b/compiler/apps/playground/lib/stores/store.ts index ad4a57cf914a9..e37140cb25259 100644 --- a/compiler/apps/playground/lib/stores/store.ts +++ b/compiler/apps/playground/lib/stores/store.ts @@ -17,6 +17,7 @@ import {defaultStore} from '../defaultStore'; */ export interface Store { source: string; + config?: string; } export function encodeStore(store: Store): string { return compressToEncodedURIComponent(JSON.stringify(store)); @@ -65,5 +66,14 @@ export function initStoreFromUrlOrLocalStorage(): Store { const raw = decodeStore(encodedSource); invariant(isValidStore(raw), 'Invalid Store'); + + // Add config property if missing for backwards compatibility + if (!('config' in raw)) { + return { + ...raw, + config: '', + }; + } + return raw; } diff --git a/compiler/apps/playground/next-env.d.ts b/compiler/apps/playground/next-env.d.ts index 1b3be0840f3f6..830fb594ca297 100644 --- a/compiler/apps/playground/next-env.d.ts +++ b/compiler/apps/playground/next-env.d.ts @@ -1,5 +1,6 @@ /// /// +/// // NOTE: This file should not be edited // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/compiler/apps/playground/package.json b/compiler/apps/playground/package.json index c17d0e4847ee6..44c1f101230cd 100644 --- a/compiler/apps/playground/package.json +++ b/compiler/apps/playground/package.json @@ -34,26 +34,30 @@ "invariant": "^2.2.4", "lz-string": "^1.5.0", "monaco-editor": "^0.52.0", - "next": "^15.2.0-canary.64", + "next": "15.5.2", "notistack": "^3.0.0-alpha.7", "prettier": "^3.3.3", "pretty-format": "^29.3.1", "re-resizable": "^6.9.16", - "react": "^19.0.0", - "react-dom": "^19.0.0" + "react": "19.1.1", + "react-dom": "19.1.1" }, "devDependencies": { "@types/node": "18.11.9", - "@types/react": "^19.0.0", - "@types/react-dom": "^19.0.0", + "@types/react": "19.1.12", + "@types/react-dom": "19.1.9", "autoprefixer": "^10.4.13", "clsx": "^1.2.1", "concurrently": "^7.4.0", "eslint": "^8.28.0", - "eslint-config-next": "^15.0.1", + "eslint-config-next": "15.5.2", "monaco-editor-webpack-plugin": "^7.1.0", "postcss": "^8.4.31", "tailwindcss": "^3.2.4", "wait-on": "^7.2.0" + }, + "resolutions": { + "@types/react": "19.1.12", + "@types/react-dom": "19.1.9" } } diff --git a/compiler/apps/playground/scripts/link-compiler.sh b/compiler/apps/playground/scripts/link-compiler.sh index 1ee5f0b81bf09..96188f7b45137 100755 --- a/compiler/apps/playground/scripts/link-compiler.sh +++ b/compiler/apps/playground/scripts/link-compiler.sh @@ -8,8 +8,8 @@ set -eo pipefail HERE=$(pwd) -cd ../../packages/react-compiler-runtime && yarn --silent link && cd $HERE -cd ../../packages/babel-plugin-react-compiler && yarn --silent link && cd $HERE +cd ../../packages/react-compiler-runtime && yarn --silent link && cd "$HERE" +cd ../../packages/babel-plugin-react-compiler && yarn --silent link && cd "$HERE" yarn --silent link babel-plugin-react-compiler yarn --silent link react-compiler-runtime diff --git a/compiler/apps/playground/yarn.lock b/compiler/apps/playground/yarn.lock index f40fffe675519..9bf1bb0687baf 100644 --- a/compiler/apps/playground/yarn.lock +++ b/compiler/apps/playground/yarn.lock @@ -445,10 +445,10 @@ "@babel/helper-string-parser" "^7.25.9" "@babel/helper-validator-identifier" "^7.25.9" -"@emnapi/runtime@^1.2.0": - version "1.3.1" - resolved "https://registry.yarnpkg.com/@emnapi/runtime/-/runtime-1.3.1.tgz#0fcaa575afc31f455fd33534c19381cfce6c6f60" - integrity sha512-kEBmG8KyqtxJZv+ygbEim+KCGtIq1fC22Ms3S4ziXmYKm8uyoLX0MHONVKwp+9opg390VaKRNt4a7A9NwmpNhw== +"@emnapi/runtime@^1.4.4": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@emnapi/runtime/-/runtime-1.5.0.tgz#9aebfcb9b17195dce3ab53c86787a6b7d058db73" + integrity sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ== dependencies: tslib "^2.4.0" @@ -520,118 +520,135 @@ resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz#4a2868d75d6d6963e423bcf90b7fd1be343409d3" integrity sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA== -"@img/sharp-darwin-arm64@0.33.5": - version "0.33.5" - resolved "https://registry.yarnpkg.com/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz#ef5b5a07862805f1e8145a377c8ba6e98813ca08" - integrity sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ== +"@img/sharp-darwin-arm64@0.34.3": + version "0.34.3" + resolved "https://registry.yarnpkg.com/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.3.tgz#4850c8ace3c1dc13607fa07d43377b1f9aa774da" + integrity sha512-ryFMfvxxpQRsgZJqBd4wsttYQbCxsJksrv9Lw/v798JcQ8+w84mBWuXwl+TT0WJ/WrYOLaYpwQXi3sA9nTIaIg== optionalDependencies: - "@img/sharp-libvips-darwin-arm64" "1.0.4" + "@img/sharp-libvips-darwin-arm64" "1.2.0" -"@img/sharp-darwin-x64@0.33.5": - version "0.33.5" - resolved "https://registry.yarnpkg.com/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz#e03d3451cd9e664faa72948cc70a403ea4063d61" - integrity sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q== +"@img/sharp-darwin-x64@0.34.3": + version "0.34.3" + resolved "https://registry.yarnpkg.com/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.3.tgz#edf93fb01479604f14ad6a64a716e2ef2bb23100" + integrity sha512-yHpJYynROAj12TA6qil58hmPmAwxKKC7reUqtGLzsOHfP7/rniNGTL8tjWX6L3CTV4+5P4ypcS7Pp+7OB+8ihA== optionalDependencies: - "@img/sharp-libvips-darwin-x64" "1.0.4" + "@img/sharp-libvips-darwin-x64" "1.2.0" -"@img/sharp-libvips-darwin-arm64@1.0.4": - version "1.0.4" - resolved "https://registry.yarnpkg.com/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz#447c5026700c01a993c7804eb8af5f6e9868c07f" - integrity sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg== +"@img/sharp-libvips-darwin-arm64@1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.0.tgz#e20e9041031acde1de19da121dc5162c7d2cf251" + integrity sha512-sBZmpwmxqwlqG9ueWFXtockhsxefaV6O84BMOrhtg/YqbTaRdqDE7hxraVE3y6gVM4eExmfzW4a8el9ArLeEiQ== -"@img/sharp-libvips-darwin-x64@1.0.4": - version "1.0.4" - resolved "https://registry.yarnpkg.com/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz#e0456f8f7c623f9dbfbdc77383caa72281d86062" - integrity sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ== +"@img/sharp-libvips-darwin-x64@1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.0.tgz#918ca81c5446f31114834cb908425a7532393185" + integrity sha512-M64XVuL94OgiNHa5/m2YvEQI5q2cl9d/wk0qFTDVXcYzi43lxuiFTftMR1tOnFQovVXNZJ5TURSDK2pNe9Yzqg== -"@img/sharp-libvips-linux-arm64@1.0.4": - version "1.0.4" - resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz#979b1c66c9a91f7ff2893556ef267f90ebe51704" - integrity sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA== +"@img/sharp-libvips-linux-arm64@1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.0.tgz#1a5beafc857b43f378c3030427aa981ee3edbc54" + integrity sha512-RXwd0CgG+uPRX5YYrkzKyalt2OJYRiJQ8ED/fi1tq9WQW2jsQIn0tqrlR5l5dr/rjqq6AHAxURhj2DVjyQWSOA== -"@img/sharp-libvips-linux-arm@1.0.5": - version "1.0.5" - resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz#99f922d4e15216ec205dcb6891b721bfd2884197" - integrity sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g== +"@img/sharp-libvips-linux-arm@1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.0.tgz#bff51182d5238ca35c5fe9e9f594a18ad6a5480d" + integrity sha512-mWd2uWvDtL/nvIzThLq3fr2nnGfyr/XMXlq8ZJ9WMR6PXijHlC3ksp0IpuhK6bougvQrchUAfzRLnbsen0Cqvw== -"@img/sharp-libvips-linux-s390x@1.0.4": - version "1.0.4" - resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz#f8a5eb1f374a082f72b3f45e2fb25b8118a8a5ce" - integrity sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA== +"@img/sharp-libvips-linux-ppc64@1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.0.tgz#10c53ccf6f2d47d71fb3fa282697072c8fe9e40e" + integrity sha512-Xod/7KaDDHkYu2phxxfeEPXfVXFKx70EAFZ0qyUdOjCcxbjqyJOEUpDe6RIyaunGxT34Anf9ue/wuWOqBW2WcQ== -"@img/sharp-libvips-linux-x64@1.0.4": - version "1.0.4" - resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz#d4c4619cdd157774906e15770ee119931c7ef5e0" - integrity sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw== +"@img/sharp-libvips-linux-s390x@1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.0.tgz#392fd7557ddc5c901f1bed7ab3c567c08833ef3b" + integrity sha512-eMKfzDxLGT8mnmPJTNMcjfO33fLiTDsrMlUVcp6b96ETbnJmd4uvZxVJSKPQfS+odwfVaGifhsB07J1LynFehw== -"@img/sharp-libvips-linuxmusl-arm64@1.0.4": - version "1.0.4" - resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz#166778da0f48dd2bded1fa3033cee6b588f0d5d5" - integrity sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA== +"@img/sharp-libvips-linux-x64@1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.0.tgz#9315cf90a2fdcdc0e29ea7663cbd8b0f15254400" + integrity sha512-ZW3FPWIc7K1sH9E3nxIGB3y3dZkpJlMnkk7z5tu1nSkBoCgw2nSRTFHI5pB/3CQaJM0pdzMF3paf9ckKMSE9Tg== -"@img/sharp-libvips-linuxmusl-x64@1.0.4": - version "1.0.4" - resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz#93794e4d7720b077fcad3e02982f2f1c246751ff" - integrity sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw== +"@img/sharp-libvips-linuxmusl-arm64@1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.0.tgz#705e03e67d477f6f842f37eb7f66285b1150dc06" + integrity sha512-UG+LqQJbf5VJ8NWJ5Z3tdIe/HXjuIdo4JeVNADXBFuG7z9zjoegpzzGIyV5zQKi4zaJjnAd2+g2nna8TZvuW9Q== + +"@img/sharp-libvips-linuxmusl-x64@1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.0.tgz#ec905071cc538df64848d5900e0d386d77c55f13" + integrity sha512-SRYOLR7CXPgNze8akZwjoGBoN1ThNZoqpOgfnOxmWsklTGVfJiGJoC/Lod7aNMGA1jSsKWM1+HRX43OP6p9+6Q== + +"@img/sharp-linux-arm64@0.34.3": + version "0.34.3" + resolved "https://registry.yarnpkg.com/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.3.tgz#476f8f13ce192555391ae9d4bc658637a6acf3e5" + integrity sha512-QdrKe3EvQrqwkDrtuTIjI0bu6YEJHTgEeqdzI3uWJOH6G1O8Nl1iEeVYRGdj1h5I21CqxSvQp1Yv7xeU3ZewbA== + optionalDependencies: + "@img/sharp-libvips-linux-arm64" "1.2.0" -"@img/sharp-linux-arm64@0.33.5": - version "0.33.5" - resolved "https://registry.yarnpkg.com/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz#edb0697e7a8279c9fc829a60fc35644c4839bb22" - integrity sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA== +"@img/sharp-linux-arm@0.34.3": + version "0.34.3" + resolved "https://registry.yarnpkg.com/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.3.tgz#9898cd68ea3e3806b94fe25736d5d7ecb5eac121" + integrity sha512-oBK9l+h6KBN0i3dC8rYntLiVfW8D8wH+NPNT3O/WBHeW0OQWCjfWksLUaPidsrDKpJgXp3G3/hkmhptAW0I3+A== optionalDependencies: - "@img/sharp-libvips-linux-arm64" "1.0.4" + "@img/sharp-libvips-linux-arm" "1.2.0" -"@img/sharp-linux-arm@0.33.5": - version "0.33.5" - resolved "https://registry.yarnpkg.com/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz#422c1a352e7b5832842577dc51602bcd5b6f5eff" - integrity sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ== +"@img/sharp-linux-ppc64@0.34.3": + version "0.34.3" + resolved "https://registry.yarnpkg.com/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.3.tgz#6a7cd4c608011333a0ddde6d96e03ac042dd9079" + integrity sha512-GLtbLQMCNC5nxuImPR2+RgrviwKwVql28FWZIW1zWruy6zLgA5/x2ZXk3mxj58X/tszVF69KK0Is83V8YgWhLA== optionalDependencies: - "@img/sharp-libvips-linux-arm" "1.0.5" + "@img/sharp-libvips-linux-ppc64" "1.2.0" -"@img/sharp-linux-s390x@0.33.5": - version "0.33.5" - resolved "https://registry.yarnpkg.com/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz#f5c077926b48e97e4a04d004dfaf175972059667" - integrity sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q== +"@img/sharp-linux-s390x@0.34.3": + version "0.34.3" + resolved "https://registry.yarnpkg.com/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.3.tgz#48e27ab969efe97d270e39297654c0e0c9b42919" + integrity sha512-3gahT+A6c4cdc2edhsLHmIOXMb17ltffJlxR0aC2VPZfwKoTGZec6u5GrFgdR7ciJSsHT27BD3TIuGcuRT0KmQ== optionalDependencies: - "@img/sharp-libvips-linux-s390x" "1.0.4" + "@img/sharp-libvips-linux-s390x" "1.2.0" -"@img/sharp-linux-x64@0.33.5": - version "0.33.5" - resolved "https://registry.yarnpkg.com/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz#d806e0afd71ae6775cc87f0da8f2d03a7c2209cb" - integrity sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA== +"@img/sharp-linux-x64@0.34.3": + version "0.34.3" + resolved "https://registry.yarnpkg.com/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.3.tgz#5aa77ad4aa447ddf6d642e2a2c5599eb1292dfaa" + integrity sha512-8kYso8d806ypnSq3/Ly0QEw90V5ZoHh10yH0HnrzOCr6DKAPI6QVHvwleqMkVQ0m+fc7EH8ah0BB0QPuWY6zJQ== optionalDependencies: - "@img/sharp-libvips-linux-x64" "1.0.4" + "@img/sharp-libvips-linux-x64" "1.2.0" -"@img/sharp-linuxmusl-arm64@0.33.5": - version "0.33.5" - resolved "https://registry.yarnpkg.com/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz#252975b915894fb315af5deea174651e208d3d6b" - integrity sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g== +"@img/sharp-linuxmusl-arm64@0.34.3": + version "0.34.3" + resolved "https://registry.yarnpkg.com/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.3.tgz#62053a9d77c7d4632c677619325b741254689dd7" + integrity sha512-vAjbHDlr4izEiXM1OTggpCcPg9tn4YriK5vAjowJsHwdBIdx0fYRsURkxLG2RLm9gyBq66gwtWI8Gx0/ov+JKQ== optionalDependencies: - "@img/sharp-libvips-linuxmusl-arm64" "1.0.4" + "@img/sharp-libvips-linuxmusl-arm64" "1.2.0" -"@img/sharp-linuxmusl-x64@0.33.5": - version "0.33.5" - resolved "https://registry.yarnpkg.com/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz#3f4609ac5d8ef8ec7dadee80b560961a60fd4f48" - integrity sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw== +"@img/sharp-linuxmusl-x64@0.34.3": + version "0.34.3" + resolved "https://registry.yarnpkg.com/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.3.tgz#5107c7709c7e0a44fe5abef59829f1de86fa0a3a" + integrity sha512-gCWUn9547K5bwvOn9l5XGAEjVTTRji4aPTqLzGXHvIr6bIDZKNTA34seMPgM0WmSf+RYBH411VavCejp3PkOeQ== optionalDependencies: - "@img/sharp-libvips-linuxmusl-x64" "1.0.4" + "@img/sharp-libvips-linuxmusl-x64" "1.2.0" -"@img/sharp-wasm32@0.33.5": - version "0.33.5" - resolved "https://registry.yarnpkg.com/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz#6f44f3283069d935bb5ca5813153572f3e6f61a1" - integrity sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg== +"@img/sharp-wasm32@0.34.3": + version "0.34.3" + resolved "https://registry.yarnpkg.com/@img/sharp-wasm32/-/sharp-wasm32-0.34.3.tgz#c1dcabb834ec2f71308a810b399bb6e6e3b79619" + integrity sha512-+CyRcpagHMGteySaWos8IbnXcHgfDn7pO2fiC2slJxvNq9gDipYBN42/RagzctVRKgxATmfqOSulgZv5e1RdMg== dependencies: - "@emnapi/runtime" "^1.2.0" + "@emnapi/runtime" "^1.4.4" -"@img/sharp-win32-ia32@0.33.5": - version "0.33.5" - resolved "https://registry.yarnpkg.com/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz#1a0c839a40c5351e9885628c85f2e5dfd02b52a9" - integrity sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ== +"@img/sharp-win32-arm64@0.34.3": + version "0.34.3" + resolved "https://registry.yarnpkg.com/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.3.tgz#3e8654e368bb349d45799a0d7aeb29db2298628e" + integrity sha512-MjnHPnbqMXNC2UgeLJtX4XqoVHHlZNd+nPt1kRPmj63wURegwBhZlApELdtxM2OIZDRv/DFtLcNhVbd1z8GYXQ== -"@img/sharp-win32-x64@0.33.5": - version "0.33.5" - resolved "https://registry.yarnpkg.com/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz#56f00962ff0c4e0eb93d34a047d29fa995e3e342" - integrity sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg== +"@img/sharp-win32-ia32@0.34.3": + version "0.34.3" + resolved "https://registry.yarnpkg.com/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.3.tgz#9d4c105e8d5074a351a81a0b6d056e0af913bf76" + integrity sha512-xuCdhH44WxuXgOM714hn4amodJMZl3OEvf0GVTm0BEyMeA2to+8HEdRPShH0SLYptJY1uBw+SCFP9WVQi1Q/cw== + +"@img/sharp-win32-x64@0.34.3": + version "0.34.3" + resolved "https://registry.yarnpkg.com/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.3.tgz#d20c89bd41b1dd3d76d8575714aaaa3c43204b6a" + integrity sha512-OWwz05d++TxzLEv4VnsTz5CmZ6mI6S05sfQGEMrNrQcOEERbX46332IvE7pO/EUiw7jUrrS40z/M7kPyjfl04g== "@isaacs/cliui@^8.0.2": version "8.0.2" @@ -698,57 +715,57 @@ dependencies: "@monaco-editor/loader" "^1.4.0" -"@next/env@15.2.0-canary.66": - version "15.2.0-canary.66" - resolved "https://registry.yarnpkg.com/@next/env/-/env-15.2.0-canary.66.tgz#c4ca0d502ad099c68927643df9c9b5d75c7b7fbb" - integrity sha512-/RxW1GJ7a6MJOQ7LOa2bcli7VTjqB7jPyzXwNJQflcYJH4gz1kP6uzg8+IptLJGFSRB58RBKHJk+q1cD8jongA== +"@next/env@15.5.2": + version "15.5.2" + resolved "https://registry.yarnpkg.com/@next/env/-/env-15.5.2.tgz#0c6b959313cd6e71afb69bf0deb417237f1d2f8a" + integrity sha512-Qe06ew4zt12LeO6N7j8/nULSOe3fMXE4dM6xgpBQNvdzyK1sv5y4oAP3bq4LamrvGCZtmRYnW8URFCeX5nFgGg== -"@next/eslint-plugin-next@15.0.1": - version "15.0.1" - resolved "https://registry.yarnpkg.com/@next/eslint-plugin-next/-/eslint-plugin-next-15.0.1.tgz#76117d88aadc52f6e04b1892d44654d05468d53c" - integrity sha512-bKWsMaGPbiFAaGqrDJvbE8b4Z0uKicGVcgOI77YM2ui3UfjHMr4emFPrZTLeZVchi7fT1mooG2LxREfUUClIKw== +"@next/eslint-plugin-next@15.5.2": + version "15.5.2" + resolved "https://registry.yarnpkg.com/@next/eslint-plugin-next/-/eslint-plugin-next-15.5.2.tgz#6fa6b78687dbbb6f5726acd81bcdfd87dc26b6f3" + integrity sha512-lkLrRVxcftuOsJNhWatf1P2hNVfh98k/omQHrCEPPriUypR6RcS13IvLdIrEvkm9AH2Nu2YpR5vLqBuy6twH3Q== dependencies: fast-glob "3.3.1" -"@next/swc-darwin-arm64@15.2.0-canary.66": - version "15.2.0-canary.66" - resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.2.0-canary.66.tgz#368438cf713c439b5b4c44d54b5c3b31ee5b772d" - integrity sha512-sVzNJWTekcLOdqkDMistBGr84AVh9eSu4o5JQNEOdxHry4jiF8lqixpOg0+Twj2RRuv4bx32h5xaRVvCSUpITQ== - -"@next/swc-darwin-x64@15.2.0-canary.66": - version "15.2.0-canary.66" - resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-15.2.0-canary.66.tgz#3ddc3f4f6e86e204727770e5984cabf52f852472" - integrity sha512-Avv6Nf/0j0WVqY72Q0mK2glGhvN7LT7iVF31iBYUe/Cbf2cXBjgpXUVmksJjg+2Fi6uTEpaMoZWSVEpJyPkjVQ== - -"@next/swc-linux-arm64-gnu@15.2.0-canary.66": - version "15.2.0-canary.66" - resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.2.0-canary.66.tgz#cd3683bf569c66444340b1e4d876913584e93aea" - integrity sha512-kUPejaStjKpF79fz4525DKQKADtUuE+T6j7IvLQsZuWrSX3a5Mix+i52fdTzMJ+sFGg3v147wopZt6L6JMIxxA== - -"@next/swc-linux-arm64-musl@15.2.0-canary.66": - version "15.2.0-canary.66" - resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.2.0-canary.66.tgz#453836b11efdb50b91cf8a6cfbce8779f6778dd9" - integrity sha512-U8l8jaZ+BAU5wn3bw7PRqq4vGTpObBt+7JbJLpbDqB1GfkZdCDc4nGtqAfLy3pY0O4lEfqal9jrsEVtUBCbfHg== - -"@next/swc-linux-x64-gnu@15.2.0-canary.66": - version "15.2.0-canary.66" - resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.2.0-canary.66.tgz#f02b295febaacf8d041f9f149c30c41aea16a81f" - integrity sha512-c+AV8ZN1znGBHu5BACGym+9FhV8+213XVHFI7i2J7TSsJ6T+Eofhwn0tSn1Vy/XpVmpyoEdkwepL+djbdQAGhw== - -"@next/swc-linux-x64-musl@15.2.0-canary.66": - version "15.2.0-canary.66" - resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.2.0-canary.66.tgz#f942c000ba3ffc0289520d25fa1067a75e72fa41" - integrity sha512-4mTIv86qyXuo8NfjigSQ7rk2cDwM8/f8R/kf3hNh8NF1Aaat2RrEet9a/SbsuNpdhhNnPI5RcRwpIJx2JQSPcQ== - -"@next/swc-win32-arm64-msvc@15.2.0-canary.66": - version "15.2.0-canary.66" - resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.2.0-canary.66.tgz#0ffdcf5c74b5aa6214307f2ae4aa84f1526e6bf9" - integrity sha512-NPnfsDQXk44h8VtncWq2AgLjHDbUMsc8Tpz7pcLe9qb8lZSxZ9jYbV7NwKzgd+qJbjy/58vgCWhL5PhyXDlWwQ== - -"@next/swc-win32-x64-msvc@15.2.0-canary.66": - version "15.2.0-canary.66" - resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.2.0-canary.66.tgz#458245850cf407d2551155e4662785c109f58bda" - integrity sha512-L/ef++GJqW+T3g2x6mrZ2vrBK+6QS9Ieam8EqK9dG7cFKv7Gqm9yrHvDuVse62hnNB11ZdxfDDKrs9vabuQLlw== +"@next/swc-darwin-arm64@15.5.2": + version "15.5.2" + resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.2.tgz#f69713326fc08f2eff3726fe19165cdb429d67c7" + integrity sha512-8bGt577BXGSd4iqFygmzIfTYizHb0LGWqH+qgIF/2EDxS5JsSdERJKA8WgwDyNBZgTIIA4D8qUtoQHmxIIquoQ== + +"@next/swc-darwin-x64@15.5.2": + version "15.5.2" + resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.2.tgz#560a9da4126bae75cbbd6899646ad7a2e4fdcc9b" + integrity sha512-2DjnmR6JHK4X+dgTXt5/sOCu/7yPtqpYt8s8hLkHFK3MGkka2snTv3yRMdHvuRtJVkPwCGsvBSwmoQCHatauFQ== + +"@next/swc-linux-arm64-gnu@15.5.2": + version "15.5.2" + resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.2.tgz#80b2be276e775e5a9286369ae54e536b0cdf8c3a" + integrity sha512-3j7SWDBS2Wov/L9q0mFJtEvQ5miIqfO4l7d2m9Mo06ddsgUK8gWfHGgbjdFlCp2Ek7MmMQZSxpGFqcC8zGh2AA== + +"@next/swc-linux-arm64-musl@15.5.2": + version "15.5.2" + resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.2.tgz#68cf676301755fd99aca11a7ebdb5eae88d7c2e4" + integrity sha512-s6N8k8dF9YGc5T01UPQ08yxsK6fUow5gG1/axWc1HVVBYQBgOjca4oUZF7s4p+kwhkB1bDSGR8QznWrFZ/Rt5g== + +"@next/swc-linux-x64-gnu@15.5.2": + version "15.5.2" + resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.2.tgz#209d9a79d0f2333544f863b0daca3f7e29f2eaff" + integrity sha512-o1RV/KOODQh6dM6ZRJGZbc+MOAHww33Vbs5JC9Mp1gDk8cpEO+cYC/l7rweiEalkSm5/1WGa4zY7xrNwObN4+Q== + +"@next/swc-linux-x64-musl@15.5.2": + version "15.5.2" + resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.2.tgz#d4ad1cfb5e99e51db669fe2145710c1abeadbd7f" + integrity sha512-/VUnh7w8RElYZ0IV83nUcP/J4KJ6LLYliiBIri3p3aW2giF+PAVgZb6mk8jbQSB3WlTai8gEmCAr7kptFa1H6g== + +"@next/swc-win32-arm64-msvc@15.5.2": + version "15.5.2" + resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.2.tgz#070e10e370a5447a198c2db100389646aca2c496" + integrity sha512-sMPyTvRcNKXseNQ/7qRfVRLa0VhR0esmQ29DD6pqvG71+JdVnESJaHPA8t7bc67KD5spP3+DOCNLhqlEI2ZgQg== + +"@next/swc-win32-x64-msvc@15.5.2": + version "15.5.2" + resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.2.tgz#9237d40b82eaf2efc88baeba15b784d4126caf4a" + integrity sha512-W5VvyZHnxG/2ukhZF/9Ikdra5fdNftxI6ybeVKYvBPDtyx7x4jPPSNduUkfH5fo3zG0JQ0bPxgy41af2JX5D4Q== "@nodelib/fs.scandir@2.1.5": version "2.1.5" @@ -820,11 +837,6 @@ resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.27.8.tgz#6667fac16c436b5434a387a34dedb013198f6e6e" integrity sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA== -"@swc/counter@0.1.3": - version "0.1.3" - resolved "https://registry.yarnpkg.com/@swc/counter/-/counter-0.1.3.tgz#cc7463bd02949611c6329596fccd2b0ec782b0e9" - integrity sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ== - "@swc/helpers@0.5.15": version "0.5.15" resolved "https://registry.yarnpkg.com/@swc/helpers/-/helpers-0.5.15.tgz#79efab344c5819ecf83a43f3f9f811fc84b516d7" @@ -842,15 +854,15 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-18.11.9.tgz#02d013de7058cea16d36168ef2fc653464cfbad4" integrity sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg== -"@types/react-dom@^19.0.0": - version "19.0.4" - resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-19.0.4.tgz#bedba97f9346bd4c0fe5d39e689713804ec9ac89" - integrity sha512-4fSQ8vWFkg+TGhePfUzVmat3eC14TXYSsiiDSLI0dVLsrm9gZFABjPy/Qu6TKgl1tq1Bu1yDsuQgY3A3DOjCcg== +"@types/react-dom@19.1.9": + version "19.1.9" + resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-19.1.9.tgz#5ab695fce1e804184767932365ae6569c11b4b4b" + integrity sha512-qXRuZaOsAdXKFyOhRBg6Lqqc0yay13vN7KrIg4L7N4aaHN68ma9OK3NE1BoDFgFOTfM7zg+3/8+2n8rLUH3OKQ== -"@types/react@^19.0.0": - version "19.0.10" - resolved "https://registry.yarnpkg.com/@types/react/-/react-19.0.10.tgz#d0c66dafd862474190fe95ce11a68de69ed2b0eb" - integrity sha512-JuRQ9KXLEjaUNjTWpzuR231Z2WpIwczOkBEIvbHNCzQefFIT0L8IqE6NV6ULLyC1SI/i234JnDoMkfg+RjQj2g== +"@types/react@19.1.12": + version "19.1.12" + resolved "https://registry.yarnpkg.com/@types/react/-/react-19.1.12.tgz#7bfaa76aabbb0b4fe0493c21a3a7a93d33e8937b" + integrity sha512-cMoR+FoAf/Jyq6+Df2/Z41jISvGZZ2eTlnsaJRptmZ76Caldwy1odD4xTr/gNV9VLj0AWgg/nmkevIyUfIIq5w== dependencies: csstype "^3.0.2" @@ -1044,6 +1056,14 @@ array-buffer-byte-length@^1.0.0, array-buffer-byte-length@^1.0.1: call-bind "^1.0.5" is-array-buffer "^3.0.4" +array-buffer-byte-length@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz#384d12a37295aec3769ab022ad323a18a51ccf8b" + integrity sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw== + dependencies: + call-bound "^1.0.3" + is-array-buffer "^3.0.5" + array-includes@^3.1.6, array-includes@^3.1.8: version "3.1.8" resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.8.tgz#5e370cbe172fdd5dd6530c1d4aadda25281ba97d" @@ -1100,6 +1120,16 @@ array.prototype.flatmap@^1.3.2: es-abstract "^1.22.1" es-shim-unscopables "^1.0.0" +array.prototype.flatmap@^1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz#712cc792ae70370ae40586264629e33aab5dd38b" + integrity sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg== + dependencies: + call-bind "^1.0.8" + define-properties "^1.2.1" + es-abstract "^1.23.5" + es-shim-unscopables "^1.0.2" + array.prototype.tosorted@^1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz#fe954678ff53034e717ea3352a03f0b0b86f7ffc" @@ -1125,6 +1155,19 @@ arraybuffer.prototype.slice@^1.0.3: is-array-buffer "^3.0.4" is-shared-array-buffer "^1.0.2" +arraybuffer.prototype.slice@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz#9d760d84dbdd06d0cbf92c8849615a1a7ab3183c" + integrity sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ== + dependencies: + array-buffer-byte-length "^1.0.1" + call-bind "^1.0.8" + define-properties "^1.2.1" + es-abstract "^1.23.5" + es-errors "^1.3.0" + get-intrinsic "^1.2.6" + is-array-buffer "^3.0.4" + ast-types-flow@^0.0.8: version "0.0.8" resolved "https://registry.yarnpkg.com/ast-types-flow/-/ast-types-flow-0.0.8.tgz#0a85e1c92695769ac13a428bb653e7538bea27d6" @@ -1220,12 +1263,13 @@ browserslist@^4.23.1, browserslist@^4.23.3: node-releases "^2.0.18" update-browserslist-db "^1.1.0" -busboy@1.6.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/busboy/-/busboy-1.6.0.tgz#966ea36a9502e43cdb9146962523b92f531f6893" - integrity sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA== +call-bind-apply-helpers@^1.0.0, call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz#4b5428c222be985d79c3d82657479dbe0b59b2d6" + integrity sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ== dependencies: - streamsearch "^1.1.0" + es-errors "^1.3.0" + function-bind "^1.1.2" call-bind@^1.0.2, call-bind@^1.0.5, call-bind@^1.0.6, call-bind@^1.0.7: version "1.0.7" @@ -1238,6 +1282,24 @@ call-bind@^1.0.2, call-bind@^1.0.5, call-bind@^1.0.6, call-bind@^1.0.7: get-intrinsic "^1.2.4" set-function-length "^1.2.1" +call-bind@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.8.tgz#0736a9660f537e3388826f440d5ec45f744eaa4c" + integrity sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww== + dependencies: + call-bind-apply-helpers "^1.0.0" + es-define-property "^1.0.0" + get-intrinsic "^1.2.4" + set-function-length "^1.2.2" + +call-bound@^1.0.2, call-bound@^1.0.3, call-bound@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/call-bound/-/call-bound-1.0.4.tgz#238de935d2a2a692928c538c7ccfa91067fd062a" + integrity sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg== + dependencies: + call-bind-apply-helpers "^1.0.2" + get-intrinsic "^1.3.0" + callsites@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" @@ -1248,12 +1310,7 @@ camelcase-css@^2.0.1: resolved "https://registry.yarnpkg.com/camelcase-css/-/camelcase-css-2.0.1.tgz#ee978f6947914cc30c6b44741b6ed1df7f043fd5" integrity sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA== -caniuse-lite@^1.0.30001579: - version "1.0.30001715" - resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001715.tgz" - integrity sha512-7ptkFGMm2OAOgvZpwgA4yjQ5SQbrNVGdRjzH0pBdy1Fasvcr+KAeECmbCAECzTuDuoX0FCY8KzUxjf9+9kfZEw== - -caniuse-lite@^1.0.30001646, caniuse-lite@^1.0.30001663: +caniuse-lite@^1.0.30001579, caniuse-lite@^1.0.30001646, caniuse-lite@^1.0.30001663: version "1.0.30001715" resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001715.tgz" integrity sha512-7ptkFGMm2OAOgvZpwgA4yjQ5SQbrNVGdRjzH0pBdy1Fasvcr+KAeECmbCAECzTuDuoX0FCY8KzUxjf9+9kfZEw== @@ -1419,6 +1476,15 @@ data-view-buffer@^1.0.1: es-errors "^1.3.0" is-data-view "^1.0.1" +data-view-buffer@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/data-view-buffer/-/data-view-buffer-1.0.2.tgz#211a03ba95ecaf7798a8c7198d79536211f88570" + integrity sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ== + dependencies: + call-bound "^1.0.3" + es-errors "^1.3.0" + is-data-view "^1.0.2" + data-view-byte-length@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/data-view-byte-length/-/data-view-byte-length-1.0.1.tgz#90721ca95ff280677eb793749fce1011347669e2" @@ -1428,6 +1494,15 @@ data-view-byte-length@^1.0.1: es-errors "^1.3.0" is-data-view "^1.0.1" +data-view-byte-length@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz#9e80f7ca52453ce3e93d25a35318767ea7704735" + integrity sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ== + dependencies: + call-bound "^1.0.3" + es-errors "^1.3.0" + is-data-view "^1.0.2" + data-view-byte-offset@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/data-view-byte-offset/-/data-view-byte-offset-1.0.0.tgz#5e0bbfb4828ed2d1b9b400cd8a7d119bca0ff18a" @@ -1437,6 +1512,15 @@ data-view-byte-offset@^1.0.0: es-errors "^1.3.0" is-data-view "^1.0.1" +data-view-byte-offset@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz#068307f9b71ab76dbbe10291389e020856606191" + integrity sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ== + dependencies: + call-bound "^1.0.2" + es-errors "^1.3.0" + is-data-view "^1.0.1" + date-fns@^2.29.1: version "2.30.0" resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.30.0.tgz#f367e644839ff57894ec6ac480de40cae4b0f4d0" @@ -1510,10 +1594,10 @@ delayed-stream@~1.0.0: resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== -detect-libc@^2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.3.tgz#f0cd503b40f9939b894697d19ad50895e30cf700" - integrity sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw== +detect-libc@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.4.tgz#f04715b8ba815e53b4d8109655b6508a6865a7e8" + integrity sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA== didyoumean@^1.2.2: version "1.2.2" @@ -1539,6 +1623,15 @@ doctrine@^3.0.0: dependencies: esutils "^2.0.2" +dunder-proto@^1.0.0, dunder-proto@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/dunder-proto/-/dunder-proto-1.0.1.tgz#d7ae667e1dc83482f8b70fd0f6eefc50da30f58a" + integrity sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A== + dependencies: + call-bind-apply-helpers "^1.0.1" + es-errors "^1.3.0" + gopd "^1.2.0" + eastasianwidth@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb" @@ -1624,6 +1717,66 @@ es-abstract@^1.17.5, es-abstract@^1.22.1, es-abstract@^1.22.3, es-abstract@^1.23 unbox-primitive "^1.0.2" which-typed-array "^1.1.15" +es-abstract@^1.23.5, es-abstract@^1.23.6, es-abstract@^1.23.9: + version "1.24.0" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.24.0.tgz#c44732d2beb0acc1ed60df840869e3106e7af328" + integrity sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg== + dependencies: + array-buffer-byte-length "^1.0.2" + arraybuffer.prototype.slice "^1.0.4" + available-typed-arrays "^1.0.7" + call-bind "^1.0.8" + call-bound "^1.0.4" + data-view-buffer "^1.0.2" + data-view-byte-length "^1.0.2" + data-view-byte-offset "^1.0.1" + es-define-property "^1.0.1" + es-errors "^1.3.0" + es-object-atoms "^1.1.1" + es-set-tostringtag "^2.1.0" + es-to-primitive "^1.3.0" + function.prototype.name "^1.1.8" + get-intrinsic "^1.3.0" + get-proto "^1.0.1" + get-symbol-description "^1.1.0" + globalthis "^1.0.4" + gopd "^1.2.0" + has-property-descriptors "^1.0.2" + has-proto "^1.2.0" + has-symbols "^1.1.0" + hasown "^2.0.2" + internal-slot "^1.1.0" + is-array-buffer "^3.0.5" + is-callable "^1.2.7" + is-data-view "^1.0.2" + is-negative-zero "^2.0.3" + is-regex "^1.2.1" + is-set "^2.0.3" + is-shared-array-buffer "^1.0.4" + is-string "^1.1.1" + is-typed-array "^1.1.15" + is-weakref "^1.1.1" + math-intrinsics "^1.1.0" + object-inspect "^1.13.4" + object-keys "^1.1.1" + object.assign "^4.1.7" + own-keys "^1.0.1" + regexp.prototype.flags "^1.5.4" + safe-array-concat "^1.1.3" + safe-push-apply "^1.0.0" + safe-regex-test "^1.1.0" + set-proto "^1.0.0" + stop-iteration-iterator "^1.1.0" + string.prototype.trim "^1.2.10" + string.prototype.trimend "^1.0.9" + string.prototype.trimstart "^1.0.8" + typed-array-buffer "^1.0.3" + typed-array-byte-length "^1.0.3" + typed-array-byte-offset "^1.0.4" + typed-array-length "^1.0.7" + unbox-primitive "^1.1.0" + which-typed-array "^1.1.19" + es-define-property@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.0.tgz#c7faefbdff8b2696cf5f46921edfb77cc4ba3845" @@ -1631,6 +1784,11 @@ es-define-property@^1.0.0: dependencies: get-intrinsic "^1.2.4" +es-define-property@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.1.tgz#983eb2f9a6724e9303f61addf011c72e09e0b0fa" + integrity sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g== + es-errors@^1.2.1, es-errors@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" @@ -1671,6 +1829,28 @@ es-iterator-helpers@^1.0.19: iterator.prototype "^1.1.2" safe-array-concat "^1.1.2" +es-iterator-helpers@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/es-iterator-helpers/-/es-iterator-helpers-1.2.1.tgz#d1dd0f58129054c0ad922e6a9a1e65eef435fe75" + integrity sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w== + dependencies: + call-bind "^1.0.8" + call-bound "^1.0.3" + define-properties "^1.2.1" + es-abstract "^1.23.6" + es-errors "^1.3.0" + es-set-tostringtag "^2.0.3" + function-bind "^1.1.2" + get-intrinsic "^1.2.6" + globalthis "^1.0.4" + gopd "^1.2.0" + has-property-descriptors "^1.0.2" + has-proto "^1.2.0" + has-symbols "^1.1.0" + internal-slot "^1.1.0" + iterator.prototype "^1.1.4" + safe-array-concat "^1.1.3" + es-object-atoms@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.0.0.tgz#ddb55cd47ac2e240701260bc2a8e31ecb643d941" @@ -1678,6 +1858,13 @@ es-object-atoms@^1.0.0: dependencies: es-errors "^1.3.0" +es-object-atoms@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz#1c4f2c4837327597ce69d2ca190a7fdd172338c1" + integrity sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA== + dependencies: + es-errors "^1.3.0" + es-set-tostringtag@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz#8bb60f0a440c2e4281962428438d58545af39777" @@ -1687,6 +1874,16 @@ es-set-tostringtag@^2.0.3: has-tostringtag "^1.0.2" hasown "^2.0.1" +es-set-tostringtag@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz#f31dbbe0c183b00a6d26eb6325c810c0fd18bd4d" + integrity sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA== + dependencies: + es-errors "^1.3.0" + get-intrinsic "^1.2.6" + has-tostringtag "^1.0.2" + hasown "^2.0.2" + es-shim-unscopables@^1.0.0, es-shim-unscopables@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz#1f6942e71ecc7835ed1c8a83006d8771a63a3763" @@ -1703,6 +1900,15 @@ es-to-primitive@^1.2.1: is-date-object "^1.0.1" is-symbol "^1.0.2" +es-to-primitive@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.3.0.tgz#96c89c82cc49fd8794a24835ba3e1ff87f214e18" + integrity sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g== + dependencies: + is-callable "^1.2.7" + is-date-object "^1.0.5" + is-symbol "^1.0.4" + escalade@^3.1.1, escalade@^3.1.2: version "3.2.0" resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.2.0.tgz#011a3f69856ba189dffa7dc8fcce99d2a87903e5" @@ -1718,12 +1924,12 @@ escape-string-regexp@^4.0.0: resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== -eslint-config-next@^15.0.1: - version "15.0.1" - resolved "https://registry.yarnpkg.com/eslint-config-next/-/eslint-config-next-15.0.1.tgz#5f49a01d312420cdbf1e87299396ef779ae99004" - integrity sha512-3cYCrgbH6GS/ufApza7XCKz92vtq4dAdYhx++rMFNlH2cAV+/GsAKkrr4+bohYOACmzG2nAOR+uWprKC1Uld6A== +eslint-config-next@15.5.2: + version "15.5.2" + resolved "https://registry.yarnpkg.com/eslint-config-next/-/eslint-config-next-15.5.2.tgz#9629ed1deaa131e8e80cbae20acf631c8595ca3e" + integrity sha512-3hPZghsLupMxxZ2ggjIIrat/bPniM2yRpsVPVM40rp8ZMzKWOJp2CGWn7+EzoV2ddkUr5fxNfHpF+wU1hGt/3g== dependencies: - "@next/eslint-plugin-next" "15.0.1" + "@next/eslint-plugin-next" "15.5.2" "@rushstack/eslint-patch" "^1.10.3" "@typescript-eslint/eslint-plugin" "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0" "@typescript-eslint/parser" "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0" @@ -1731,7 +1937,7 @@ eslint-config-next@^15.0.1: eslint-import-resolver-typescript "^3.5.2" eslint-plugin-import "^2.31.0" eslint-plugin-jsx-a11y "^6.10.0" - eslint-plugin-react "^7.35.0" + eslint-plugin-react "^7.37.0" eslint-plugin-react-hooks "^5.0.0" eslint-import-resolver-node@^0.3.6, eslint-import-resolver-node@^0.3.9: @@ -1816,28 +2022,28 @@ eslint-plugin-react-hooks@^5.0.0: resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.0.0.tgz#72e2eefbac4b694f5324154619fee44f5f60f101" integrity sha512-hIOwI+5hYGpJEc4uPRmz2ulCjAGD/N13Lukkh8cLV0i2IRk/bdZDYjgLVHj+U9Z704kLIdIO6iueGvxNur0sgw== -eslint-plugin-react@^7.35.0: - version "7.37.1" - resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.37.1.tgz#56493d7d69174d0d828bc83afeffe96903fdadbd" - integrity sha512-xwTnwDqzbDRA8uJ7BMxPs/EXRB3i8ZfnOIp8BsxEQkT0nHPp+WWceqGgo6rKb9ctNi8GJLDT4Go5HAWELa/WMg== +eslint-plugin-react@^7.37.0: + version "7.37.5" + resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz#2975511472bdda1b272b34d779335c9b0e877065" + integrity sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA== dependencies: array-includes "^3.1.8" array.prototype.findlast "^1.2.5" - array.prototype.flatmap "^1.3.2" + array.prototype.flatmap "^1.3.3" array.prototype.tosorted "^1.1.4" doctrine "^2.1.0" - es-iterator-helpers "^1.0.19" + es-iterator-helpers "^1.2.1" estraverse "^5.3.0" hasown "^2.0.2" jsx-ast-utils "^2.4.1 || ^3.0.0" minimatch "^3.1.2" - object.entries "^1.1.8" + object.entries "^1.1.9" object.fromentries "^2.0.8" - object.values "^1.2.0" + object.values "^1.2.1" prop-types "^15.8.1" resolve "^2.0.0-next.5" semver "^6.3.1" - string.prototype.matchall "^4.0.11" + string.prototype.matchall "^4.0.12" string.prototype.repeat "^1.0.0" eslint-scope@^7.2.2: @@ -2022,6 +2228,13 @@ for-each@^0.3.3: dependencies: is-callable "^1.1.3" +for-each@^0.3.5: + version "0.3.5" + resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.5.tgz#d650688027826920feeb0af747ee7b9421a41d47" + integrity sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg== + dependencies: + is-callable "^1.2.7" + foreground-child@^3.1.0: version "3.3.0" resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-3.3.0.tgz#0ac8644c06e431439f8561db8ecf29a7b5519c77" @@ -2074,6 +2287,18 @@ function.prototype.name@^1.1.6: es-abstract "^1.22.1" functions-have-names "^1.2.3" +function.prototype.name@^1.1.8: + version "1.1.8" + resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.8.tgz#e68e1df7b259a5c949eeef95cdbde53edffabb78" + integrity sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q== + dependencies: + call-bind "^1.0.8" + call-bound "^1.0.3" + define-properties "^1.2.1" + functions-have-names "^1.2.3" + hasown "^2.0.2" + is-callable "^1.2.7" + functions-have-names@^1.2.3: version "1.2.3" resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834" @@ -2100,6 +2325,30 @@ get-intrinsic@^1.1.3, get-intrinsic@^1.2.1, get-intrinsic@^1.2.2, get-intrinsic@ has-symbols "^1.0.3" hasown "^2.0.0" +get-intrinsic@^1.2.5, get-intrinsic@^1.2.6, get-intrinsic@^1.2.7, get-intrinsic@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz#743f0e3b6964a93a5491ed1bffaae054d7f98d01" + integrity sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ== + dependencies: + call-bind-apply-helpers "^1.0.2" + es-define-property "^1.0.1" + es-errors "^1.3.0" + es-object-atoms "^1.1.1" + function-bind "^1.1.2" + get-proto "^1.0.1" + gopd "^1.2.0" + has-symbols "^1.1.0" + hasown "^2.0.2" + math-intrinsics "^1.1.0" + +get-proto@^1.0.0, get-proto@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/get-proto/-/get-proto-1.0.1.tgz#150b3f2743869ef3e851ec0c49d15b1d14d00ee1" + integrity sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g== + dependencies: + dunder-proto "^1.0.1" + es-object-atoms "^1.0.0" + get-symbol-description@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/get-symbol-description/-/get-symbol-description-1.0.2.tgz#533744d5aa20aca4e079c8e5daf7fd44202821f5" @@ -2109,6 +2358,15 @@ get-symbol-description@^1.0.2: es-errors "^1.3.0" get-intrinsic "^1.2.4" +get-symbol-description@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/get-symbol-description/-/get-symbol-description-1.1.0.tgz#7bdd54e0befe8ffc9f3b4e203220d9f1e881b6ee" + integrity sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg== + dependencies: + call-bound "^1.0.3" + es-errors "^1.3.0" + get-intrinsic "^1.2.6" + get-tsconfig@^4.7.5: version "4.8.1" resolved "https://registry.yarnpkg.com/get-tsconfig/-/get-tsconfig-4.8.1.tgz#8995eb391ae6e1638d251118c7b56de7eb425471" @@ -2166,7 +2424,7 @@ globals@^13.19.0: dependencies: type-fest "^0.20.2" -globalthis@^1.0.3: +globalthis@^1.0.3, globalthis@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/globalthis/-/globalthis-1.0.4.tgz#7430ed3a975d97bfb59bcce41f5cabbafa651236" integrity sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ== @@ -2186,6 +2444,11 @@ gopd@^1.0.1: dependencies: get-intrinsic "^1.1.3" +gopd@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1" + integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg== + graceful-fs@^4.2.4: version "4.2.11" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" @@ -2223,11 +2486,23 @@ has-proto@^1.0.1, has-proto@^1.0.3: resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.3.tgz#b31ddfe9b0e6e9914536a6ab286426d0214f77fd" integrity sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q== +has-proto@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.2.0.tgz#5de5a6eabd95fdffd9818b43055e8065e39fe9d5" + integrity sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ== + dependencies: + dunder-proto "^1.0.0" + has-symbols@^1.0.2, has-symbols@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== +has-symbols@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.1.0.tgz#fc9c6a783a084951d0b971fe1018de813707a338" + integrity sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ== + has-tostringtag@^1.0.0, has-tostringtag@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz#2cdc42d40bef2e5b4eeab7c01a73c54ce7ab5abc" @@ -2303,6 +2578,15 @@ internal-slot@^1.0.4, internal-slot@^1.0.7: hasown "^2.0.0" side-channel "^1.0.4" +internal-slot@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.1.0.tgz#1eac91762947d2f7056bc838d93e13b2e9604961" + integrity sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw== + dependencies: + es-errors "^1.3.0" + hasown "^2.0.2" + side-channel "^1.1.0" + invariant@^2.2.4: version "2.2.4" resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6" @@ -2326,6 +2610,15 @@ is-array-buffer@^3.0.2, is-array-buffer@^3.0.4: call-bind "^1.0.2" get-intrinsic "^1.2.1" +is-array-buffer@^3.0.5: + version "3.0.5" + resolved "https://registry.yarnpkg.com/is-array-buffer/-/is-array-buffer-3.0.5.tgz#65742e1e687bd2cc666253068fd8707fe4d44280" + integrity sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A== + dependencies: + call-bind "^1.0.8" + call-bound "^1.0.3" + get-intrinsic "^1.2.6" + is-arrayish@^0.3.1: version "0.3.2" resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.3.2.tgz#4574a2ae56f7ab206896fb431eaeed066fdf8f03" @@ -2345,6 +2638,13 @@ is-bigint@^1.0.1: dependencies: has-bigints "^1.0.1" +is-bigint@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.1.0.tgz#dda7a3445df57a42583db4228682eba7c4170672" + integrity sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ== + dependencies: + has-bigints "^1.0.2" + is-binary-path@~2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" @@ -2360,6 +2660,14 @@ is-boolean-object@^1.1.0: call-bind "^1.0.2" has-tostringtag "^1.0.0" +is-boolean-object@^1.2.1: + version "1.2.2" + resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.2.2.tgz#7067f47709809a393c71ff5bb3e135d8a9215d9e" + integrity sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A== + dependencies: + call-bound "^1.0.3" + has-tostringtag "^1.0.2" + is-bun-module@^1.0.2: version "1.2.1" resolved "https://registry.yarnpkg.com/is-bun-module/-/is-bun-module-1.2.1.tgz#495e706f42e29f086fd5fe1ac3c51f106062b9fc" @@ -2386,6 +2694,15 @@ is-data-view@^1.0.1: dependencies: is-typed-array "^1.1.13" +is-data-view@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-data-view/-/is-data-view-1.0.2.tgz#bae0a41b9688986c2188dda6657e56b8f9e63b8e" + integrity sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw== + dependencies: + call-bound "^1.0.2" + get-intrinsic "^1.2.6" + is-typed-array "^1.1.13" + is-date-object@^1.0.1, is-date-object@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.5.tgz#0841d5536e724c25597bf6ea62e1bd38298df31f" @@ -2393,6 +2710,14 @@ is-date-object@^1.0.1, is-date-object@^1.0.5: dependencies: has-tostringtag "^1.0.0" +is-date-object@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.1.0.tgz#ad85541996fc7aa8b2729701d27b7319f95d82f7" + integrity sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg== + dependencies: + call-bound "^1.0.2" + has-tostringtag "^1.0.2" + is-extglob@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" @@ -2405,6 +2730,13 @@ is-finalizationregistry@^1.0.2: dependencies: call-bind "^1.0.2" +is-finalizationregistry@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz#eefdcdc6c94ddd0674d9c85887bf93f944a97c90" + integrity sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg== + dependencies: + call-bound "^1.0.3" + is-fullwidth-code-point@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" @@ -2441,6 +2773,14 @@ is-number-object@^1.0.4: dependencies: has-tostringtag "^1.0.0" +is-number-object@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.1.1.tgz#144b21e95a1bc148205dcc2814a9134ec41b2541" + integrity sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw== + dependencies: + call-bound "^1.0.3" + has-tostringtag "^1.0.2" + is-number@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" @@ -2459,6 +2799,16 @@ is-regex@^1.1.4: call-bind "^1.0.2" has-tostringtag "^1.0.0" +is-regex@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.2.1.tgz#76d70a3ed10ef9be48eb577887d74205bf0cad22" + integrity sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g== + dependencies: + call-bound "^1.0.2" + gopd "^1.2.0" + has-tostringtag "^1.0.2" + hasown "^2.0.2" + is-set@^2.0.2, is-set@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/is-set/-/is-set-2.0.3.tgz#8ab209ea424608141372ded6e0cb200ef1d9d01d" @@ -2471,6 +2821,13 @@ is-shared-array-buffer@^1.0.2, is-shared-array-buffer@^1.0.3: dependencies: call-bind "^1.0.7" +is-shared-array-buffer@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz#9b67844bd9b7f246ba0708c3a93e34269c774f6f" + integrity sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A== + dependencies: + call-bound "^1.0.3" + is-string@^1.0.5, is-string@^1.0.7: version "1.0.7" resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.7.tgz#0dd12bf2006f255bb58f695110eff7491eebc0fd" @@ -2478,6 +2835,14 @@ is-string@^1.0.5, is-string@^1.0.7: dependencies: has-tostringtag "^1.0.0" +is-string@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.1.1.tgz#92ea3f3d5c5b6e039ca8677e5ac8d07ea773cbb9" + integrity sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA== + dependencies: + call-bound "^1.0.3" + has-tostringtag "^1.0.2" + is-symbol@^1.0.2, is-symbol@^1.0.3: version "1.0.4" resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.4.tgz#a6dac93b635b063ca6872236de88910a57af139c" @@ -2485,6 +2850,15 @@ is-symbol@^1.0.2, is-symbol@^1.0.3: dependencies: has-symbols "^1.0.2" +is-symbol@^1.0.4, is-symbol@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.1.1.tgz#f47761279f532e2b05a7024a7506dbbedacd0634" + integrity sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w== + dependencies: + call-bound "^1.0.2" + has-symbols "^1.1.0" + safe-regex-test "^1.1.0" + is-typed-array@^1.1.13: version "1.1.13" resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.13.tgz#d6c5ca56df62334959322d7d7dd1cca50debe229" @@ -2492,6 +2866,13 @@ is-typed-array@^1.1.13: dependencies: which-typed-array "^1.1.14" +is-typed-array@^1.1.14, is-typed-array@^1.1.15: + version "1.1.15" + resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.15.tgz#4bfb4a45b61cee83a5a46fba778e4e8d59c0ce0b" + integrity sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ== + dependencies: + which-typed-array "^1.1.16" + is-weakmap@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/is-weakmap/-/is-weakmap-2.0.2.tgz#bf72615d649dfe5f699079c54b83e47d1ae19cfd" @@ -2504,6 +2885,13 @@ is-weakref@^1.0.2: dependencies: call-bind "^1.0.2" +is-weakref@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/is-weakref/-/is-weakref-1.1.1.tgz#eea430182be8d64174bd96bffbc46f21bf3f9293" + integrity sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew== + dependencies: + call-bound "^1.0.3" + is-weakset@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/is-weakset/-/is-weakset-2.0.3.tgz#e801519df8c0c43e12ff2834eead84ec9e624007" @@ -2533,6 +2921,18 @@ iterator.prototype@^1.1.2: reflect.getprototypeof "^1.0.4" set-function-name "^2.0.1" +iterator.prototype@^1.1.4: + version "1.1.5" + resolved "https://registry.yarnpkg.com/iterator.prototype/-/iterator.prototype-1.1.5.tgz#12c959a29de32de0aa3bbbb801f4d777066dae39" + integrity sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g== + dependencies: + define-data-property "^1.1.4" + es-object-atoms "^1.0.0" + get-intrinsic "^1.2.6" + get-proto "^1.0.0" + has-symbols "^1.1.0" + set-function-name "^2.0.2" + jackspeak@^3.1.2: version "3.4.3" resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-3.4.3.tgz#8833a9d89ab4acde6188942bd1c53b6390ed5a8a" @@ -2709,6 +3109,11 @@ lz-string@^1.5.0: resolved "https://registry.yarnpkg.com/lz-string/-/lz-string-1.5.0.tgz#c1ab50f77887b712621201ba9fd4e3a6ed099941" integrity sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ== +math-intrinsics@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz#a0dd74be81e2aa5c2f27e65ce283605ee4e2b7f9" + integrity sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g== + merge2@^1.3.0: version "1.4.1" resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" @@ -2794,28 +3199,26 @@ natural-compare@^1.4.0: resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== -next@^15.2.0-canary.64: - version "15.2.0-canary.66" - resolved "https://registry.yarnpkg.com/next/-/next-15.2.0-canary.66.tgz#cb5ee4453c88f247b6e74fe33fd181eca58e7c86" - integrity sha512-S+gsEu8vxxejI7nKqtCLqZlTi9L40xelLRK/Fgtvm/XT8W8ziLp3KMtN4I9Si5nEMU5uv7bllIfd04kVX8+HIw== +next@15.5.2: + version "15.5.2" + resolved "https://registry.yarnpkg.com/next/-/next-15.5.2.tgz#5e50102443fb0328a9dfcac2d82465c7bac93693" + integrity sha512-H8Otr7abj1glFhbGnvUt3gz++0AF1+QoCXEBmd/6aKbfdFwrn0LpA836Ed5+00va/7HQSDD+mOoVhn3tNy3e/Q== dependencies: - "@next/env" "15.2.0-canary.66" - "@swc/counter" "0.1.3" + "@next/env" "15.5.2" "@swc/helpers" "0.5.15" - busboy "1.6.0" caniuse-lite "^1.0.30001579" postcss "8.4.31" styled-jsx "5.1.6" optionalDependencies: - "@next/swc-darwin-arm64" "15.2.0-canary.66" - "@next/swc-darwin-x64" "15.2.0-canary.66" - "@next/swc-linux-arm64-gnu" "15.2.0-canary.66" - "@next/swc-linux-arm64-musl" "15.2.0-canary.66" - "@next/swc-linux-x64-gnu" "15.2.0-canary.66" - "@next/swc-linux-x64-musl" "15.2.0-canary.66" - "@next/swc-win32-arm64-msvc" "15.2.0-canary.66" - "@next/swc-win32-x64-msvc" "15.2.0-canary.66" - sharp "^0.33.5" + "@next/swc-darwin-arm64" "15.5.2" + "@next/swc-darwin-x64" "15.5.2" + "@next/swc-linux-arm64-gnu" "15.5.2" + "@next/swc-linux-arm64-musl" "15.5.2" + "@next/swc-linux-x64-gnu" "15.5.2" + "@next/swc-linux-x64-musl" "15.5.2" + "@next/swc-win32-arm64-msvc" "15.5.2" + "@next/swc-win32-x64-msvc" "15.5.2" + sharp "^0.34.3" node-releases@^2.0.18: version "2.0.18" @@ -2855,6 +3258,11 @@ object-inspect@^1.13.1: resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.2.tgz#dea0088467fb991e67af4058147a24824a3043ff" integrity sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g== +object-inspect@^1.13.3, object-inspect@^1.13.4: + version "1.13.4" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.4.tgz#8375265e21bc20d0fa582c22e1b13485d6e00213" + integrity sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew== + object-is@^1.1.5: version "1.1.6" resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.6.tgz#1a6a53aed2dd8f7e6775ff870bea58545956ab07" @@ -2878,14 +3286,27 @@ object.assign@^4.1.4, object.assign@^4.1.5: has-symbols "^1.0.3" object-keys "^1.1.1" -object.entries@^1.1.8: - version "1.1.8" - resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.8.tgz#bffe6f282e01f4d17807204a24f8edd823599c41" - integrity sha512-cmopxi8VwRIAw/fkijJohSfpef5PdN0pMQJN6VC/ZKvn0LIknWD8KtgY6KlQdEc4tIjcQ3HxSMmnvtzIscdaYQ== +object.assign@^4.1.7: + version "4.1.7" + resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.7.tgz#8c14ca1a424c6a561b0bb2a22f66f5049a945d3d" + integrity sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw== dependencies: - call-bind "^1.0.7" + call-bind "^1.0.8" + call-bound "^1.0.3" define-properties "^1.2.1" es-object-atoms "^1.0.0" + has-symbols "^1.1.0" + object-keys "^1.1.1" + +object.entries@^1.1.9: + version "1.1.9" + resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.9.tgz#e4770a6a1444afb61bd39f984018b5bede25f8b3" + integrity sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw== + dependencies: + call-bind "^1.0.8" + call-bound "^1.0.4" + define-properties "^1.2.1" + es-object-atoms "^1.1.1" object.fromentries@^2.0.8: version "2.0.8" @@ -2915,6 +3336,16 @@ object.values@^1.1.6, object.values@^1.2.0: define-properties "^1.2.1" es-object-atoms "^1.0.0" +object.values@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.2.1.tgz#deed520a50809ff7f75a7cfd4bc64c7a038c6216" + integrity sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA== + dependencies: + call-bind "^1.0.8" + call-bound "^1.0.3" + define-properties "^1.2.1" + es-object-atoms "^1.0.0" + once@^1.3.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" @@ -2934,6 +3365,15 @@ optionator@^0.9.3: type-check "^0.4.0" word-wrap "^1.2.5" +own-keys@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/own-keys/-/own-keys-1.0.1.tgz#e4006910a2bf913585289676eebd6f390cf51358" + integrity sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg== + dependencies: + get-intrinsic "^1.2.6" + object-keys "^1.1.1" + safe-push-apply "^1.0.0" + p-limit@^3.0.2: version "3.1.0" resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" @@ -3137,12 +3577,12 @@ re-resizable@^6.9.16: resolved "https://registry.yarnpkg.com/re-resizable/-/re-resizable-6.10.0.tgz#d684a096ab438f1a93f59ad3a580a206b0ce31ee" integrity sha512-hysSK0xmA5nz24HBVztlk4yCqCLCvS32E6ZpWxVKop9x3tqCa4yAj1++facrmkOf62JsJHjmjABdKxXofYioCw== -react-dom@^19.0.0: - version "19.0.0" - resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-19.0.0.tgz#43446f1f01c65a4cd7f7588083e686a6726cfb57" - integrity sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ== +react-dom@19.1.1: + version "19.1.1" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-19.1.1.tgz#2daa9ff7f3ae384aeb30e76d5ee38c046dc89893" + integrity sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw== dependencies: - scheduler "^0.25.0" + scheduler "^0.26.0" react-is@^16.13.1: version "16.13.1" @@ -3154,10 +3594,10 @@ react-is@^18.0.0: resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.3.1.tgz#e83557dc12eae63a99e003a46388b1dcbb44db7e" integrity sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg== -react@^19.0.0: - version "19.0.0" - resolved "https://registry.yarnpkg.com/react/-/react-19.0.0.tgz#6e1969251b9f108870aa4bff37a0ce9ddfaaabdd" - integrity sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ== +react@19.1.1: + version "19.1.1" + resolved "https://registry.yarnpkg.com/react/-/react-19.1.1.tgz#06d9149ec5e083a67f9a1e39ce97b06a03b644af" + integrity sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ== read-cache@^1.0.0: version "1.0.0" @@ -3186,6 +3626,20 @@ reflect.getprototypeof@^1.0.4: globalthis "^1.0.3" which-builtin-type "^1.1.3" +reflect.getprototypeof@^1.0.6, reflect.getprototypeof@^1.0.9: + version "1.0.10" + resolved "https://registry.yarnpkg.com/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz#c629219e78a3316d8b604c765ef68996964e7bf9" + integrity sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw== + dependencies: + call-bind "^1.0.8" + define-properties "^1.2.1" + es-abstract "^1.23.9" + es-errors "^1.3.0" + es-object-atoms "^1.0.0" + get-intrinsic "^1.2.7" + get-proto "^1.0.1" + which-builtin-type "^1.2.1" + regenerator-runtime@^0.14.0: version "0.14.1" resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz#356ade10263f685dda125100cd862c1db895327f" @@ -3201,6 +3655,18 @@ regexp.prototype.flags@^1.5.1, regexp.prototype.flags@^1.5.2: es-errors "^1.3.0" set-function-name "^2.0.1" +regexp.prototype.flags@^1.5.3, regexp.prototype.flags@^1.5.4: + version "1.5.4" + resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz#1ad6c62d44a259007e55b3970e00f746efbcaa19" + integrity sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA== + dependencies: + call-bind "^1.0.8" + define-properties "^1.2.1" + es-errors "^1.3.0" + get-proto "^1.0.1" + gopd "^1.2.0" + set-function-name "^2.0.2" + require-directory@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" @@ -3270,6 +3736,25 @@ safe-array-concat@^1.1.2: has-symbols "^1.0.3" isarray "^2.0.5" +safe-array-concat@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/safe-array-concat/-/safe-array-concat-1.1.3.tgz#c9e54ec4f603b0bbb8e7e5007a5ee7aecd1538c3" + integrity sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q== + dependencies: + call-bind "^1.0.8" + call-bound "^1.0.2" + get-intrinsic "^1.2.6" + has-symbols "^1.1.0" + isarray "^2.0.5" + +safe-push-apply@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/safe-push-apply/-/safe-push-apply-1.0.0.tgz#01850e981c1602d398c85081f360e4e6d03d27f5" + integrity sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA== + dependencies: + es-errors "^1.3.0" + isarray "^2.0.5" + safe-regex-test@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/safe-regex-test/-/safe-regex-test-1.0.3.tgz#a5b4c0f06e0ab50ea2c395c14d8371232924c377" @@ -3279,10 +3764,19 @@ safe-regex-test@^1.0.3: es-errors "^1.3.0" is-regex "^1.1.4" -scheduler@^0.25.0: - version "0.25.0" - resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.25.0.tgz#336cd9768e8cceebf52d3c80e3dcf5de23e7e015" - integrity sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA== +safe-regex-test@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/safe-regex-test/-/safe-regex-test-1.1.0.tgz#7f87dfb67a3150782eaaf18583ff5d1711ac10c1" + integrity sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw== + dependencies: + call-bound "^1.0.2" + es-errors "^1.3.0" + is-regex "^1.2.1" + +scheduler@^0.26.0: + version "0.26.0" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.26.0.tgz#4ce8a8c2a2095f13ea11bf9a445be50c555d6337" + integrity sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA== semver@^6.3.1: version "6.3.1" @@ -3294,7 +3788,12 @@ semver@^7.6.0, semver@^7.6.3: resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.3.tgz#980f7b5550bc175fb4dc09403085627f9eb33143" integrity sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A== -set-function-length@^1.2.1: +semver@^7.7.2: + version "7.7.2" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.2.tgz#67d99fdcd35cec21e6f8b87a7fd515a33f982b58" + integrity sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA== + +set-function-length@^1.2.1, set-function-length@^1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449" integrity sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg== @@ -3316,34 +3815,46 @@ set-function-name@^2.0.1, set-function-name@^2.0.2: functions-have-names "^1.2.3" has-property-descriptors "^1.0.2" -sharp@^0.33.5: - version "0.33.5" - resolved "https://registry.yarnpkg.com/sharp/-/sharp-0.33.5.tgz#13e0e4130cc309d6a9497596715240b2ec0c594e" - integrity sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw== +set-proto@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/set-proto/-/set-proto-1.0.0.tgz#0760dbcff30b2d7e801fd6e19983e56da337565e" + integrity sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw== + dependencies: + dunder-proto "^1.0.1" + es-errors "^1.3.0" + es-object-atoms "^1.0.0" + +sharp@^0.34.3: + version "0.34.3" + resolved "https://registry.yarnpkg.com/sharp/-/sharp-0.34.3.tgz#10a03bcd15fb72f16355461af0b9245ccb8a5da3" + integrity sha512-eX2IQ6nFohW4DbvHIOLRB3MHFpYqaqvXd3Tp5e/T/dSH83fxaNJQRvDMhASmkNTsNTVF2/OOopzRCt7xokgPfg== dependencies: color "^4.2.3" - detect-libc "^2.0.3" - semver "^7.6.3" + detect-libc "^2.0.4" + semver "^7.7.2" optionalDependencies: - "@img/sharp-darwin-arm64" "0.33.5" - "@img/sharp-darwin-x64" "0.33.5" - "@img/sharp-libvips-darwin-arm64" "1.0.4" - "@img/sharp-libvips-darwin-x64" "1.0.4" - "@img/sharp-libvips-linux-arm" "1.0.5" - "@img/sharp-libvips-linux-arm64" "1.0.4" - "@img/sharp-libvips-linux-s390x" "1.0.4" - "@img/sharp-libvips-linux-x64" "1.0.4" - "@img/sharp-libvips-linuxmusl-arm64" "1.0.4" - "@img/sharp-libvips-linuxmusl-x64" "1.0.4" - "@img/sharp-linux-arm" "0.33.5" - "@img/sharp-linux-arm64" "0.33.5" - "@img/sharp-linux-s390x" "0.33.5" - "@img/sharp-linux-x64" "0.33.5" - "@img/sharp-linuxmusl-arm64" "0.33.5" - "@img/sharp-linuxmusl-x64" "0.33.5" - "@img/sharp-wasm32" "0.33.5" - "@img/sharp-win32-ia32" "0.33.5" - "@img/sharp-win32-x64" "0.33.5" + "@img/sharp-darwin-arm64" "0.34.3" + "@img/sharp-darwin-x64" "0.34.3" + "@img/sharp-libvips-darwin-arm64" "1.2.0" + "@img/sharp-libvips-darwin-x64" "1.2.0" + "@img/sharp-libvips-linux-arm" "1.2.0" + "@img/sharp-libvips-linux-arm64" "1.2.0" + "@img/sharp-libvips-linux-ppc64" "1.2.0" + "@img/sharp-libvips-linux-s390x" "1.2.0" + "@img/sharp-libvips-linux-x64" "1.2.0" + "@img/sharp-libvips-linuxmusl-arm64" "1.2.0" + "@img/sharp-libvips-linuxmusl-x64" "1.2.0" + "@img/sharp-linux-arm" "0.34.3" + "@img/sharp-linux-arm64" "0.34.3" + "@img/sharp-linux-ppc64" "0.34.3" + "@img/sharp-linux-s390x" "0.34.3" + "@img/sharp-linux-x64" "0.34.3" + "@img/sharp-linuxmusl-arm64" "0.34.3" + "@img/sharp-linuxmusl-x64" "0.34.3" + "@img/sharp-wasm32" "0.34.3" + "@img/sharp-win32-arm64" "0.34.3" + "@img/sharp-win32-ia32" "0.34.3" + "@img/sharp-win32-x64" "0.34.3" shebang-command@^2.0.0: version "2.0.0" @@ -3362,7 +3873,36 @@ shell-quote@^1.7.3: resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.8.1.tgz#6dbf4db75515ad5bac63b4f1894c3a154c766680" integrity sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA== -side-channel@^1.0.4, side-channel@^1.0.6: +side-channel-list@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/side-channel-list/-/side-channel-list-1.0.0.tgz#10cb5984263115d3b7a0e336591e290a830af8ad" + integrity sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA== + dependencies: + es-errors "^1.3.0" + object-inspect "^1.13.3" + +side-channel-map@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/side-channel-map/-/side-channel-map-1.0.1.tgz#d6bb6b37902c6fef5174e5f533fab4c732a26f42" + integrity sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA== + dependencies: + call-bound "^1.0.2" + es-errors "^1.3.0" + get-intrinsic "^1.2.5" + object-inspect "^1.13.3" + +side-channel-weakmap@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz#11dda19d5368e40ce9ec2bdc1fb0ecbc0790ecea" + integrity sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A== + dependencies: + call-bound "^1.0.2" + es-errors "^1.3.0" + get-intrinsic "^1.2.5" + object-inspect "^1.13.3" + side-channel-map "^1.0.1" + +side-channel@^1.0.4: version "1.0.6" resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.6.tgz#abd25fb7cd24baf45466406b1096b7831c9215f2" integrity sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA== @@ -3372,6 +3912,17 @@ side-channel@^1.0.4, side-channel@^1.0.6: get-intrinsic "^1.2.4" object-inspect "^1.13.1" +side-channel@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.1.0.tgz#c3fcff9c4da932784873335ec9765fa94ff66bc9" + integrity sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw== + dependencies: + es-errors "^1.3.0" + object-inspect "^1.13.3" + side-channel-list "^1.0.0" + side-channel-map "^1.0.1" + side-channel-weakmap "^1.0.2" + signal-exit@^4.0.1: version "4.1.0" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04" @@ -3406,10 +3957,13 @@ stop-iteration-iterator@^1.0.0: dependencies: internal-slot "^1.0.4" -streamsearch@^1.1.0: +stop-iteration-iterator@^1.1.0: version "1.1.0" - resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-1.1.0.tgz#404dd1e2247ca94af554e841a8ef0eaa238da764" - integrity sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg== + resolved "https://registry.yarnpkg.com/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz#f481ff70a548f6124d0312c3aa14cbfa7aa542ad" + integrity sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ== + dependencies: + es-errors "^1.3.0" + internal-slot "^1.1.0" "string-width-cjs@npm:string-width@^4.2.0": version "4.2.3" @@ -3446,23 +4000,24 @@ string.prototype.includes@^2.0.0: define-properties "^1.1.3" es-abstract "^1.17.5" -string.prototype.matchall@^4.0.11: - version "4.0.11" - resolved "https://registry.yarnpkg.com/string.prototype.matchall/-/string.prototype.matchall-4.0.11.tgz#1092a72c59268d2abaad76582dccc687c0297e0a" - integrity sha512-NUdh0aDavY2og7IbBPenWqR9exH+E26Sv8e0/eTe1tltDGZL+GtBkDAnnyBtmekfK6/Dq3MkcGtzXFEd1LQrtg== +string.prototype.matchall@^4.0.12: + version "4.0.12" + resolved "https://registry.yarnpkg.com/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz#6c88740e49ad4956b1332a911e949583a275d4c0" + integrity sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA== dependencies: - call-bind "^1.0.7" + call-bind "^1.0.8" + call-bound "^1.0.3" define-properties "^1.2.1" - es-abstract "^1.23.2" + es-abstract "^1.23.6" es-errors "^1.3.0" es-object-atoms "^1.0.0" - get-intrinsic "^1.2.4" - gopd "^1.0.1" - has-symbols "^1.0.3" - internal-slot "^1.0.7" - regexp.prototype.flags "^1.5.2" + get-intrinsic "^1.2.6" + gopd "^1.2.0" + has-symbols "^1.1.0" + internal-slot "^1.1.0" + regexp.prototype.flags "^1.5.3" set-function-name "^2.0.2" - side-channel "^1.0.6" + side-channel "^1.1.0" string.prototype.repeat@^1.0.0: version "1.0.0" @@ -3472,6 +4027,19 @@ string.prototype.repeat@^1.0.0: define-properties "^1.1.3" es-abstract "^1.17.5" +string.prototype.trim@^1.2.10: + version "1.2.10" + resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz#40b2dd5ee94c959b4dcfb1d65ce72e90da480c81" + integrity sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA== + dependencies: + call-bind "^1.0.8" + call-bound "^1.0.2" + define-data-property "^1.1.4" + define-properties "^1.2.1" + es-abstract "^1.23.5" + es-object-atoms "^1.0.0" + has-property-descriptors "^1.0.2" + string.prototype.trim@^1.2.9: version "1.2.9" resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz#b6fa326d72d2c78b6df02f7759c73f8f6274faa4" @@ -3491,6 +4059,16 @@ string.prototype.trimend@^1.0.8: define-properties "^1.2.1" es-object-atoms "^1.0.0" +string.prototype.trimend@^1.0.9: + version "1.0.9" + resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz#62e2731272cd285041b36596054e9f66569b6942" + integrity sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ== + dependencies: + call-bind "^1.0.8" + call-bound "^1.0.2" + define-properties "^1.2.1" + es-object-atoms "^1.0.0" + string.prototype.trimstart@^1.0.8: version "1.0.8" resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz#7ee834dda8c7c17eff3118472bb35bfedaa34dde" @@ -3692,6 +4270,15 @@ typed-array-buffer@^1.0.2: es-errors "^1.3.0" is-typed-array "^1.1.13" +typed-array-buffer@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz#a72395450a4869ec033fd549371b47af3a2ee536" + integrity sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw== + dependencies: + call-bound "^1.0.3" + es-errors "^1.3.0" + is-typed-array "^1.1.14" + typed-array-byte-length@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/typed-array-byte-length/-/typed-array-byte-length-1.0.1.tgz#d92972d3cff99a3fa2e765a28fcdc0f1d89dec67" @@ -3703,6 +4290,17 @@ typed-array-byte-length@^1.0.1: has-proto "^1.0.3" is-typed-array "^1.1.13" +typed-array-byte-length@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz#8407a04f7d78684f3d252aa1a143d2b77b4160ce" + integrity sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg== + dependencies: + call-bind "^1.0.8" + for-each "^0.3.3" + gopd "^1.2.0" + has-proto "^1.2.0" + is-typed-array "^1.1.14" + typed-array-byte-offset@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/typed-array-byte-offset/-/typed-array-byte-offset-1.0.2.tgz#f9ec1acb9259f395093e4567eb3c28a580d02063" @@ -3715,6 +4313,19 @@ typed-array-byte-offset@^1.0.2: has-proto "^1.0.3" is-typed-array "^1.1.13" +typed-array-byte-offset@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz#ae3698b8ec91a8ab945016108aef00d5bff12355" + integrity sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ== + dependencies: + available-typed-arrays "^1.0.7" + call-bind "^1.0.8" + for-each "^0.3.3" + gopd "^1.2.0" + has-proto "^1.2.0" + is-typed-array "^1.1.15" + reflect.getprototypeof "^1.0.9" + typed-array-length@^1.0.6: version "1.0.6" resolved "https://registry.yarnpkg.com/typed-array-length/-/typed-array-length-1.0.6.tgz#57155207c76e64a3457482dfdc1c9d1d3c4c73a3" @@ -3727,6 +4338,18 @@ typed-array-length@^1.0.6: is-typed-array "^1.1.13" possible-typed-array-names "^1.0.0" +typed-array-length@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/typed-array-length/-/typed-array-length-1.0.7.tgz#ee4deff984b64be1e118b0de8c9c877d5ce73d3d" + integrity sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg== + dependencies: + call-bind "^1.0.7" + for-each "^0.3.3" + gopd "^1.0.1" + is-typed-array "^1.1.13" + possible-typed-array-names "^1.0.0" + reflect.getprototypeof "^1.0.6" + unbox-primitive@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.2.tgz#29032021057d5e6cdbd08c5129c226dff8ed6f9e" @@ -3737,6 +4360,16 @@ unbox-primitive@^1.0.2: has-symbols "^1.0.3" which-boxed-primitive "^1.0.2" +unbox-primitive@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.1.0.tgz#8d9d2c9edeea8460c7f35033a88867944934d1e2" + integrity sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw== + dependencies: + call-bound "^1.0.3" + has-bigints "^1.0.2" + has-symbols "^1.1.0" + which-boxed-primitive "^1.1.1" + update-browserslist-db@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz#7ca61c0d8650766090728046e416a8cde682859e" @@ -3779,6 +4412,17 @@ which-boxed-primitive@^1.0.2: is-string "^1.0.5" is-symbol "^1.0.3" +which-boxed-primitive@^1.1.0, which-boxed-primitive@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz#d76ec27df7fa165f18d5808374a5fe23c29b176e" + integrity sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA== + dependencies: + is-bigint "^1.1.0" + is-boolean-object "^1.2.1" + is-number-object "^1.1.1" + is-string "^1.1.1" + is-symbol "^1.1.1" + which-builtin-type@^1.1.3: version "1.1.4" resolved "https://registry.yarnpkg.com/which-builtin-type/-/which-builtin-type-1.1.4.tgz#592796260602fc3514a1b5ee7fa29319b72380c3" @@ -3797,6 +4441,25 @@ which-builtin-type@^1.1.3: which-collection "^1.0.2" which-typed-array "^1.1.15" +which-builtin-type@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/which-builtin-type/-/which-builtin-type-1.2.1.tgz#89183da1b4907ab089a6b02029cc5d8d6574270e" + integrity sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q== + dependencies: + call-bound "^1.0.2" + function.prototype.name "^1.1.6" + has-tostringtag "^1.0.2" + is-async-function "^2.0.0" + is-date-object "^1.1.0" + is-finalizationregistry "^1.1.0" + is-generator-function "^1.0.10" + is-regex "^1.2.1" + is-weakref "^1.0.2" + isarray "^2.0.5" + which-boxed-primitive "^1.1.0" + which-collection "^1.0.2" + which-typed-array "^1.1.16" + which-collection@^1.0.1, which-collection@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/which-collection/-/which-collection-1.0.2.tgz#627ef76243920a107e7ce8e96191debe4b16c2a0" @@ -3818,6 +4481,19 @@ which-typed-array@^1.1.13, which-typed-array@^1.1.14, which-typed-array@^1.1.15: gopd "^1.0.1" has-tostringtag "^1.0.2" +which-typed-array@^1.1.16, which-typed-array@^1.1.19: + version "1.1.19" + resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.19.tgz#df03842e870b6b88e117524a4b364b6fc689f956" + integrity sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw== + dependencies: + available-typed-arrays "^1.0.7" + call-bind "^1.0.8" + call-bound "^1.0.4" + for-each "^0.3.5" + get-proto "^1.0.1" + gopd "^1.2.0" + has-tostringtag "^1.0.2" + which@^2.0.1: version "2.0.2" resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" diff --git a/compiler/package.json b/compiler/package.json index e3c3ee8c7f341..4492b70210803 100644 --- a/compiler/package.json +++ b/compiler/package.json @@ -19,7 +19,8 @@ "test": "yarn workspaces run test", "snap": "yarn workspace babel-plugin-react-compiler run snap", "snap:build": "yarn workspace snap run build", - "npm:publish": "node scripts/release/publish" + "npm:publish": "node scripts/release/publish", + "eslint-docs": "yarn workspace babel-plugin-react-compiler build && node scripts/build-eslint-docs.js" }, "dependencies": { "fs-extra": "^4.0.2", diff --git a/compiler/packages/babel-plugin-react-compiler/src/Babel/BabelPlugin.ts b/compiler/packages/babel-plugin-react-compiler/src/Babel/BabelPlugin.ts index 58167194249e5..ed74f4664953b 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Babel/BabelPlugin.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Babel/BabelPlugin.ts @@ -12,6 +12,7 @@ import { pipelineUsesReanimatedPlugin, } from '../Entrypoint/Reanimated'; import validateNoUntransformedReferences from '../Entrypoint/ValidateNoUntransformedReferences'; +import {CompilerError} from '..'; const ENABLE_REACT_COMPILER_TIMINGS = process.env['ENABLE_REACT_COMPILER_TIMINGS'] === '1'; @@ -34,51 +35,58 @@ export default function BabelPluginReactCompiler( */ Program: { enter(prog, pass): void { - const filename = pass.filename ?? 'unknown'; - if (ENABLE_REACT_COMPILER_TIMINGS === true) { - performance.mark(`${filename}:start`, { - detail: 'BabelPlugin:Program:start', - }); - } - let opts = parsePluginOptions(pass.opts); - const isDev = - (typeof __DEV__ !== 'undefined' && __DEV__ === true) || - process.env['NODE_ENV'] === 'development'; - if ( - opts.enableReanimatedCheck === true && - pipelineUsesReanimatedPlugin(pass.file.opts.plugins) - ) { - opts = injectReanimatedFlag(opts); - } - if ( - opts.environment.enableResetCacheOnSourceFileChanges !== false && - isDev - ) { - opts = { - ...opts, - environment: { - ...opts.environment, - enableResetCacheOnSourceFileChanges: true, - }, - }; - } - const result = compileProgram(prog, { - opts, - filename: pass.filename ?? null, - comments: pass.file.ast.comments ?? [], - code: pass.file.code, - }); - validateNoUntransformedReferences( - prog, - pass.filename ?? null, - opts.logger, - opts.environment, - result, - ); - if (ENABLE_REACT_COMPILER_TIMINGS === true) { - performance.mark(`${filename}:end`, { - detail: 'BabelPlugin:Program:end', + try { + const filename = pass.filename ?? 'unknown'; + if (ENABLE_REACT_COMPILER_TIMINGS === true) { + performance.mark(`${filename}:start`, { + detail: 'BabelPlugin:Program:start', + }); + } + let opts = parsePluginOptions(pass.opts); + const isDev = + (typeof __DEV__ !== 'undefined' && __DEV__ === true) || + process.env['NODE_ENV'] === 'development'; + if ( + opts.enableReanimatedCheck === true && + pipelineUsesReanimatedPlugin(pass.file.opts.plugins) + ) { + opts = injectReanimatedFlag(opts); + } + if ( + opts.environment.enableResetCacheOnSourceFileChanges !== false && + isDev + ) { + opts = { + ...opts, + environment: { + ...opts.environment, + enableResetCacheOnSourceFileChanges: true, + }, + }; + } + const result = compileProgram(prog, { + opts, + filename: pass.filename ?? null, + comments: pass.file.ast.comments ?? [], + code: pass.file.code, }); + validateNoUntransformedReferences( + prog, + pass.filename ?? null, + opts.logger, + opts.environment, + result, + ); + if (ENABLE_REACT_COMPILER_TIMINGS === true) { + performance.mark(`${filename}:end`, { + detail: 'BabelPlugin:Program:end', + }); + } + } catch (e) { + if (e instanceof CompilerError) { + throw e.withPrintedMessage(pass.file.code, {eslint: false}); + } + throw e; } }, exit(_, pass): void { diff --git a/compiler/packages/babel-plugin-react-compiler/src/CompilerError.ts b/compiler/packages/babel-plugin-react-compiler/src/CompilerError.ts index 7285140de0a62..e12530a8db56b 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/CompilerError.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/CompilerError.ts @@ -5,9 +5,12 @@ * LICENSE file in the root directory of this source tree. */ -import type {SourceLocation} from './HIR'; +import * as t from '@babel/types'; +import {codeFrameColumns} from '@babel/code-frame'; +import {type SourceLocation} from './HIR'; import {Err, Ok, Result} from './Utils/Result'; import {assertExhaustive} from './Utils/utils'; +import invariant from 'invariant'; export enum ErrorSeverity { /** @@ -15,6 +18,11 @@ export enum ErrorSeverity { * misunderstanding on the user’s part. */ InvalidJS = 'InvalidJS', + /** + * JS syntax that is not supported and which we do not plan to support. Developers should + * rewrite to use supported forms. + */ + UnsupportedJS = 'UnsupportedJS', /** * Code that breaks the rules of React. */ @@ -28,6 +36,14 @@ export enum ErrorSeverity { * memoization. */ CannotPreserveMemoization = 'CannotPreserveMemoization', + /** + * An API that is known to be incompatible with the compiler. Generally as a result of + * the library using "interior mutability", ie having a value whose referential identity + * stays the same but which provides access to values that can change. For example a + * function that doesn't change but returns different results, or an object that doesn't + * change identity but whose properties change. + */ + IncompatibleLibrary = 'IncompatibleLibrary', /** * Unhandled syntax that we don't support yet. */ @@ -39,6 +55,29 @@ export enum ErrorSeverity { Invariant = 'Invariant', } +export type CompilerDiagnosticOptions = { + category: ErrorCategory; + severity: ErrorSeverity; + reason: string; + description: string; + details: Array; + suggestions?: Array | null | undefined; +}; + +export type CompilerDiagnosticDetail = + /** + * A/the source of the error + */ + | { + kind: 'error'; + loc: SourceLocation | null; + message: string; + } + | { + kind: 'hint'; + message: string; + }; + export enum CompilerSuggestionOperation { InsertBefore, InsertAfter, @@ -62,13 +101,124 @@ export type CompilerSuggestion = }; export type CompilerErrorDetailOptions = { + category: ErrorCategory; + severity: ErrorSeverity; reason: string; description?: string | null | undefined; - severity: ErrorSeverity; loc: SourceLocation | null; suggestions?: Array | null | undefined; }; +export type PrintErrorMessageOptions = { + /** + * ESLint uses 1-indexed columns and prints one error at a time + * So it doesn't require the "Found # error(s)" text + */ + eslint: boolean; +}; + +export class CompilerDiagnostic { + options: CompilerDiagnosticOptions; + + constructor(options: CompilerDiagnosticOptions) { + this.options = options; + } + + static create( + options: Omit, + ): CompilerDiagnostic { + return new CompilerDiagnostic({...options, details: []}); + } + + get reason(): CompilerDiagnosticOptions['reason'] { + return this.options.reason; + } + get description(): CompilerDiagnosticOptions['description'] { + return this.options.description; + } + get severity(): CompilerDiagnosticOptions['severity'] { + return this.options.severity; + } + get suggestions(): CompilerDiagnosticOptions['suggestions'] { + return this.options.suggestions; + } + get category(): ErrorCategory { + return this.options.category; + } + + withDetail(detail: CompilerDiagnosticDetail): CompilerDiagnostic { + this.options.details.push(detail); + return this; + } + + primaryLocation(): SourceLocation | null { + const firstErrorDetail = this.options.details.filter( + d => d.kind === 'error', + )[0]; + return firstErrorDetail != null && firstErrorDetail.kind === 'error' + ? firstErrorDetail.loc + : null; + } + + printErrorMessage(source: string, options: PrintErrorMessageOptions): string { + const buffer = [ + printErrorSummary(this.severity, this.reason), + '\n\n', + this.description, + ]; + for (const detail of this.options.details) { + switch (detail.kind) { + case 'error': { + const loc = detail.loc; + if (loc == null || typeof loc === 'symbol') { + continue; + } + let codeFrame: string; + try { + codeFrame = printCodeFrame(source, loc, detail.message); + } catch (e) { + codeFrame = detail.message; + } + buffer.push('\n\n'); + if (loc.filename != null) { + const line = loc.start.line; + const column = options.eslint + ? loc.start.column + 1 + : loc.start.column; + buffer.push(`${loc.filename}:${line}:${column}\n`); + } + buffer.push(codeFrame); + break; + } + case 'hint': { + buffer.push('\n\n'); + buffer.push(detail.message); + break; + } + default: { + assertExhaustive( + detail, + `Unexpected detail kind ${(detail as any).kind}`, + ); + } + } + } + return buffer.join(''); + } + + toString(): string { + const buffer = [printErrorSummary(this.severity, this.reason)]; + if (this.description != null) { + buffer.push(`. ${this.description}.`); + } + const loc = this.primaryLocation(); + if (loc != null && typeof loc !== 'symbol') { + buffer.push(` (${loc.start.line}:${loc.start.column})`); + } + return buffer.join(''); + } +} + /* * Each bailout or invariant in HIR lowering creates an {@link CompilerErrorDetail}, which is then * aggregated into a single {@link CompilerError} later. @@ -95,35 +245,66 @@ export class CompilerErrorDetail { get suggestions(): CompilerErrorDetailOptions['suggestions'] { return this.options.suggestions; } + get category(): ErrorCategory { + return this.options.category; + } + + primaryLocation(): SourceLocation | null { + return this.loc; + } - printErrorMessage(): string { - const buffer = [`${this.severity}: ${this.reason}`]; + printErrorMessage(source: string, options: PrintErrorMessageOptions): string { + const buffer = [printErrorSummary(this.severity, this.reason)]; if (this.description != null) { - buffer.push(`. ${this.description}`); + buffer.push(`\n\n${this.description}.`); } - if (this.loc != null && typeof this.loc !== 'symbol') { - buffer.push(` (${this.loc.start.line}:${this.loc.end.line})`); + const loc = this.loc; + if (loc != null && typeof loc !== 'symbol') { + let codeFrame: string; + try { + codeFrame = printCodeFrame(source, loc, this.reason); + } catch (e) { + codeFrame = ''; + } + buffer.push(`\n\n`); + if (loc.filename != null) { + const line = loc.start.line; + const column = options.eslint ? loc.start.column + 1 : loc.start.column; + buffer.push(`${loc.filename}:${line}:${column}\n`); + } + buffer.push(codeFrame); + buffer.push('\n\n'); } return buffer.join(''); } toString(): string { - return this.printErrorMessage(); + const buffer = [printErrorSummary(this.severity, this.reason)]; + if (this.description != null) { + buffer.push(`. ${this.description}.`); + } + const loc = this.loc; + if (loc != null && typeof loc !== 'symbol') { + buffer.push(` (${loc.start.line}:${loc.start.column})`); + } + return buffer.join(''); } } export class CompilerError extends Error { - details: Array = []; + details: Array = []; + printedMessage: string | null = null; static invariant( condition: unknown, - options: Omit, + options: Omit, ): asserts condition { if (!condition) { const errors = new CompilerError(); errors.pushErrorDetail( new CompilerErrorDetail({ ...options, + category: ErrorCategory.Invariant, severity: ErrorSeverity.Invariant, }), ); @@ -131,24 +312,35 @@ export class CompilerError extends Error { } } + static throwDiagnostic(options: CompilerDiagnosticOptions): never { + const errors = new CompilerError(); + errors.pushDiagnostic(new CompilerDiagnostic(options)); + throw errors; + } + static throwTodo( - options: Omit, + options: Omit, ): never { const errors = new CompilerError(); errors.pushErrorDetail( - new CompilerErrorDetail({...options, severity: ErrorSeverity.Todo}), + new CompilerErrorDetail({ + ...options, + severity: ErrorSeverity.Todo, + category: ErrorCategory.Todo, + }), ); throw errors; } static throwInvalidJS( - options: Omit, + options: Omit, ): never { const errors = new CompilerError(); errors.pushErrorDetail( new CompilerErrorDetail({ ...options, severity: ErrorSeverity.InvalidJS, + category: ErrorCategory.Syntax, }), ); throw errors; @@ -168,13 +360,14 @@ export class CompilerError extends Error { } static throwInvalidConfig( - options: Omit, + options: Omit, ): never { const errors = new CompilerError(); errors.pushErrorDetail( new CompilerErrorDetail({ ...options, severity: ErrorSeverity.InvalidConfig, + category: ErrorCategory.Config, }), ); throw errors; @@ -193,20 +386,52 @@ export class CompilerError extends Error { } override get message(): string { - return this.toString(); + return this.printedMessage ?? this.toString(); } override set message(_message: string) {} override toString(): string { + if (this.printedMessage) { + return this.printedMessage; + } if (Array.isArray(this.details)) { return this.details.map(detail => detail.toString()).join('\n\n'); } return this.name; } + withPrintedMessage( + source: string, + options: PrintErrorMessageOptions, + ): CompilerError { + this.printedMessage = this.printErrorMessage(source, options); + return this; + } + + printErrorMessage(source: string, options: PrintErrorMessageOptions): string { + if (options.eslint && this.details.length === 1) { + return this.details[0].printErrorMessage(source, options); + } + return ( + `Found ${this.details.length} error${this.details.length === 1 ? '' : 's'}:\n\n` + + this.details + .map(detail => detail.printErrorMessage(source, options).trim()) + .join('\n\n') + ); + } + + merge(other: CompilerError): void { + this.details.push(...other.details); + } + + pushDiagnostic(diagnostic: CompilerDiagnostic): void { + this.details.push(diagnostic); + } + push(options: CompilerErrorDetailOptions): CompilerErrorDetail { const detail = new CompilerErrorDetail({ + category: options.category, reason: options.reason, description: options.description ?? null, severity: options.severity, @@ -241,13 +466,438 @@ export class CompilerError extends Error { case ErrorSeverity.InvalidJS: case ErrorSeverity.InvalidReact: case ErrorSeverity.InvalidConfig: + case ErrorSeverity.UnsupportedJS: + case ErrorSeverity.IncompatibleLibrary: { return true; + } case ErrorSeverity.CannotPreserveMemoization: - case ErrorSeverity.Todo: + case ErrorSeverity.Todo: { return false; - default: + } + default: { assertExhaustive(detail.severity, 'Unhandled error severity'); + } } }); } } + +function printCodeFrame( + source: string, + loc: t.SourceLocation, + message: string, +): string { + return codeFrameColumns( + source, + { + start: { + line: loc.start.line, + column: loc.start.column + 1, + }, + end: { + line: loc.end.line, + column: loc.end.column + 1, + }, + }, + { + message, + }, + ); +} + +function printErrorSummary(severity: ErrorSeverity, message: string): string { + let severityCategory: string; + switch (severity) { + case ErrorSeverity.InvalidConfig: + case ErrorSeverity.InvalidJS: + case ErrorSeverity.InvalidReact: + case ErrorSeverity.UnsupportedJS: { + severityCategory = 'Error'; + break; + } + case ErrorSeverity.IncompatibleLibrary: + case ErrorSeverity.CannotPreserveMemoization: { + severityCategory = 'Compilation Skipped'; + break; + } + case ErrorSeverity.Invariant: { + severityCategory = 'Invariant'; + break; + } + case ErrorSeverity.Todo: { + severityCategory = 'Todo'; + break; + } + default: { + assertExhaustive(severity, `Unexpected severity '${severity}'`); + } + } + return `${severityCategory}: ${message}`; +} + +/** + * See getRuleForCategory() for how these map to ESLint rules + */ +export enum ErrorCategory { + // Checking for valid hooks usage (non conditional, non-first class, non reactive, etc) + Hooks = 'Hooks', + + // Checking for no capitalized calls (not definitively an error, hence separating) + CapitalizedCalls = 'CapitalizedCalls', + + // Checking for static components + StaticComponents = 'StaticComponents', + + // Checking for valid usage of manual memoization + UseMemo = 'UseMemo', + + // Checking for higher order functions acting as factories for components/hooks + Factories = 'Factories', + + // Checks that manual memoization is preserved + PreserveManualMemo = 'PreserveManualMemo', + + // Checks for known incompatible libraries + IncompatibleLibrary = 'IncompatibleLibrary', + + // Checking for no mutations of props, hook arguments, hook return values + Immutability = 'Immutability', + + // Checking for assignments to globals + Globals = 'Globals', + + // Checking for valid usage of refs, ie no access during render + Refs = 'Refs', + + // Checks for memoized effect deps + EffectDependencies = 'EffectDependencies', + + // Checks for no setState in effect bodies + EffectSetState = 'EffectSetState', + + EffectDerivationsOfState = 'EffectDerivationsOfState', + + // Validates against try/catch in place of error boundaries + ErrorBoundaries = 'ErrorBoundaries', + + // Checking for pure functions + Purity = 'Purity', + + // Validates against setState in render + RenderSetState = 'RenderSetState', + + // Internal invariants + Invariant = 'Invariant', + + // Todos + Todo = 'Todo', + + // Syntax errors + Syntax = 'Syntax', + + // Checks for use of unsupported syntax + UnsupportedSyntax = 'UnsupportedSyntax', + + // Config errors + Config = 'Config', + + // Gating error + Gating = 'Gating', + + // Suppressions + Suppression = 'Suppression', + + // Issues with auto deps + AutomaticEffectDependencies = 'AutomaticEffectDependencies', + + // Issues with `fire` + Fire = 'Fire', + + // fbt-specific issues + FBT = 'FBT', +} + +export type LintRule = { + // Stores the category the rule corresponds to, used to filter errors when reporting + category: ErrorCategory; + + /** + * The "name" of the rule as it will be used by developers to enable/disable, eg + * "eslint-disable-nest line " + */ + name: string; + + /** + * A description of the rule that appears somewhere in ESLint. This does not affect + * how error messages are formatted + */ + description: string; + + /** + * If true, this rule will automatically appear in the default, "recommended" ESLint + * rule set. Otherwise it will be part of an `allRules` export that developers can + * use to opt-in to showing output of all possible rules. + * + * NOTE: not all validations are enabled by default! Setting this flag only affects + * whether a given rule is part of the recommended set. The corresponding validation + * also should be enabled by default if you want the error to actually show up! + */ + recommended: boolean; +}; + +const RULE_NAME_PATTERN = /^[a-z]+(-[a-z]+)*$/; + +export function getRuleForCategory(category: ErrorCategory): LintRule { + const rule = getRuleForCategoryImpl(category); + invariant( + RULE_NAME_PATTERN.test(rule.name), + `Invalid rule name, got '${rule.name}' but rules must match ${RULE_NAME_PATTERN.toString()}`, + ); + return rule; +} + +function getRuleForCategoryImpl(category: ErrorCategory): LintRule { + switch (category) { + case ErrorCategory.AutomaticEffectDependencies: { + return { + category, + name: 'automatic-effect-dependencies', + description: + 'Verifies that automatic effect dependencies are compiled if opted-in', + recommended: false, + }; + } + case ErrorCategory.CapitalizedCalls: { + return { + category, + name: 'capitalized-calls', + description: + 'Validates against calling capitalized functions/methods instead of using JSX', + recommended: false, + }; + } + case ErrorCategory.Config: { + return { + category, + name: 'config', + description: 'Validates the compiler configuration options', + recommended: true, + }; + } + case ErrorCategory.EffectDependencies: { + return { + category, + name: 'memoized-effect-dependencies', + description: 'Validates that effect dependencies are memoized', + recommended: false, + }; + } + case ErrorCategory.EffectDerivationsOfState: { + return { + category, + name: 'no-deriving-state-in-effects', + description: + 'Validates against deriving values from state in an effect', + recommended: false, + }; + } + case ErrorCategory.EffectSetState: { + return { + category, + name: 'set-state-in-effect', + description: + 'Validates against calling setState synchronously in an effect, which can lead to re-renders that degrade performance', + recommended: true, + }; + } + case ErrorCategory.ErrorBoundaries: { + return { + category, + name: 'error-boundaries', + description: + 'Validates usage of error boundaries instead of try/catch for errors in child components', + recommended: true, + }; + } + case ErrorCategory.Factories: { + return { + category, + name: 'component-hook-factories', + description: + 'Validates against higher order functions defining nested components or hooks. ' + + 'Components and hooks should be defined at the module level', + recommended: true, + }; + } + case ErrorCategory.FBT: { + return { + category, + name: 'fbt', + description: 'Validates usage of fbt', + recommended: false, + }; + } + case ErrorCategory.Fire: { + return { + category, + name: 'fire', + description: 'Validates usage of `fire`', + recommended: false, + }; + } + case ErrorCategory.Gating: { + return { + category, + name: 'gating', + description: + 'Validates configuration of [gating mode](https://react.dev/reference/react-compiler/gating)', + recommended: true, + }; + } + case ErrorCategory.Globals: { + return { + category, + name: 'globals', + description: + 'Validates against assignment/mutation of globals during render, part of ensuring that ' + + '[side effects must render outside of render](https://react.dev/reference/rules/components-and-hooks-must-be-pure#side-effects-must-run-outside-of-render)', + recommended: true, + }; + } + case ErrorCategory.Hooks: { + return { + category, + name: 'hooks', + description: 'Validates the rules of hooks', + /** + * TODO: the "Hooks" rule largely reimplements the "rules-of-hooks" non-compiler rule. + * We need to dedeupe these (moving the remaining bits into the compiler) and then enable + * this rule. + */ + recommended: false, + }; + } + case ErrorCategory.Immutability: { + return { + category, + name: 'immutability', + description: + 'Validates against mutating props, state, and other values that [are immutable](https://react.dev/reference/rules/components-and-hooks-must-be-pure#props-and-state-are-immutable)', + recommended: true, + }; + } + case ErrorCategory.Invariant: { + return { + category, + name: 'invariant', + description: 'Internal invariants', + recommended: false, + }; + } + case ErrorCategory.PreserveManualMemo: { + return { + category, + name: 'preserve-manual-memoization', + description: + 'Validates that existing manual memoized is preserved by the compiler. ' + + 'React Compiler will only compile components and hooks if its inference ' + + '[matches or exceeds the existing manual memoization](https://react.dev/learn/react-compiler/introduction#what-should-i-do-about-usememo-usecallback-and-reactmemo)', + recommended: true, + }; + } + case ErrorCategory.Purity: { + return { + category, + name: 'purity', + description: + 'Validates that [components/hooks are pure](https://react.dev/reference/rules/components-and-hooks-must-be-pure) by checking that they do not call known-impure functions', + recommended: true, + }; + } + case ErrorCategory.Refs: { + return { + category, + name: 'refs', + description: + 'Validates correct usage of refs, not reading/writing during render. See the "pitfalls" section in [`useRef()` usage](https://react.dev/reference/react/useRef#usage)', + recommended: true, + }; + } + case ErrorCategory.RenderSetState: { + return { + category, + name: 'set-state-in-render', + description: + 'Validates against setting state during render, which can trigger additional renders and potential infinite render loops', + recommended: true, + }; + } + case ErrorCategory.StaticComponents: { + return { + category, + name: 'static-components', + description: + 'Validates that components are static, not recreated every render. Components that are recreated dynamically can reset state and trigger excessive re-rendering', + recommended: true, + }; + } + case ErrorCategory.Suppression: { + return { + category, + name: 'rule-suppression', + description: 'Validates against suppression of other rules', + recommended: false, + }; + } + case ErrorCategory.Syntax: { + return { + category, + name: 'syntax', + description: 'Validates against invalid syntax', + recommended: false, + }; + } + case ErrorCategory.Todo: { + return { + category, + name: 'todo', + description: 'Unimplemented features', + recommended: false, + }; + } + case ErrorCategory.UnsupportedSyntax: { + return { + category, + name: 'unsupported-syntax', + description: + 'Validates against syntax that we do not plan to support in React Compiler', + recommended: true, + }; + } + case ErrorCategory.UseMemo: { + return { + category, + name: 'use-memo', + description: + 'Validates usage of the useMemo() hook against common mistakes. See [`useMemo()` docs](https://react.dev/reference/react/useMemo) for more information.', + recommended: true, + }; + } + case ErrorCategory.IncompatibleLibrary: { + return { + category, + name: 'incompatible-library', + description: + 'Validates against usage of libraries which are incompatible with memoization (manual or automatic)', + recommended: true, + }; + } + default: { + assertExhaustive(category, `Unsupported category ${category}`); + } + } +} + +export const LintRules: Array = Object.keys(ErrorCategory).map( + category => getRuleForCategory(category as any), +); diff --git a/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Imports.ts b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Imports.ts index 24ce37cf72c29..9653c49576a64 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Imports.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Imports.ts @@ -9,7 +9,7 @@ import {NodePath} from '@babel/core'; import * as t from '@babel/types'; import {Scope as BabelScope} from '@babel/traverse'; -import {CompilerError, ErrorSeverity} from '../CompilerError'; +import {CompilerError, ErrorCategory, ErrorSeverity} from '../CompilerError'; import { EnvironmentConfig, GeneratedSource, @@ -38,6 +38,7 @@ export function validateRestrictedImports( ImportDeclaration(importDeclPath) { if (restrictedImports.has(importDeclPath.node.source.value)) { error.push({ + category: ErrorCategory.Todo, severity: ErrorSeverity.Todo, reason: 'Bailing out due to blocklisted import', description: `Import from module ${importDeclPath.node.source.value}`, @@ -205,6 +206,7 @@ export class ProgramContext { } const error = new CompilerError(); error.push({ + category: ErrorCategory.Todo, severity: ErrorSeverity.Todo, reason: 'Encountered conflicting global in generated program', description: `Conflict from local binding ${name}`, diff --git a/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Options.ts b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Options.ts index c732e164101d4..c13940ed10a21 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Options.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Options.ts @@ -7,7 +7,12 @@ import * as t from '@babel/types'; import {z} from 'zod'; -import {CompilerError, CompilerErrorDetailOptions} from '../CompilerError'; +import { + CompilerDiagnostic, + CompilerError, + CompilerErrorDetail, + CompilerErrorDetailOptions, +} from '../CompilerError'; import { EnvironmentConfig, ExternalFunction, @@ -37,6 +42,14 @@ const PanicThresholdOptionsSchema = z.enum([ ]); export type PanicThresholdOptions = z.infer; +const DynamicGatingOptionsSchema = z.object({ + source: z.string(), +}); +export type DynamicGatingOptions = z.infer; +const CustomOptOutDirectiveSchema = z + .nullable(z.array(z.string())) + .default(null); +type CustomOptOutDirective = z.infer; export type PluginOptions = { environment: EnvironmentConfig; @@ -65,6 +78,28 @@ export type PluginOptions = { */ gating: ExternalFunction | null; + /** + * If specified, this enables dynamic gating which matches `use memo if(...)` + * directives. + * + * Example usage: + * ```js + * // @dynamicGating:{"source":"myModule"} + * export function MyComponent() { + * 'use memo if(isEnabled)'; + * return
...
; + * } + * ``` + * This will emit: + * ```js + * import {isEnabled} from 'myModule'; + * export const MyComponent = isEnabled() + * ? + * : ; + * ``` + */ + dynamicGating: DynamicGatingOptions | null; + panicThreshold: PanicThresholdOptions; /* @@ -106,6 +141,11 @@ export type PluginOptions = { */ ignoreUseNoForget: boolean; + /** + * Unstable / do not use + */ + customOptOutDirectives: CustomOptOutDirective; + sources: Array | ((filename: string) => boolean) | null; /** @@ -189,7 +229,7 @@ export type LoggerEvent = export type CompileErrorEvent = { kind: 'CompileError'; fnLoc: t.SourceLocation | null; - detail: CompilerErrorDetailOptions; + detail: CompilerErrorDetail | CompilerDiagnostic; }; export type CompileDiagnosticEvent = { kind: 'CompileDiagnostic'; @@ -244,6 +284,7 @@ export const defaultOptions: PluginOptions = { logger: null, gating: null, noEmit: false, + dynamicGating: null, eslintSuppressionRules: null, flowSuppressions: true, ignoreUseNoForget: false, @@ -251,6 +292,7 @@ export const defaultOptions: PluginOptions = { return filename.indexOf('node_modules') === -1; }, enableReanimatedCheck: true, + customOptOutDirectives: null, target: '19', } as const; @@ -292,6 +334,40 @@ export function parsePluginOptions(obj: unknown): PluginOptions { } break; } + case 'dynamicGating': { + if (value == null) { + parsedOptions[key] = null; + } else { + const result = DynamicGatingOptionsSchema.safeParse(value); + if (result.success) { + parsedOptions[key] = result.data; + } else { + CompilerError.throwInvalidConfig({ + reason: + 'Could not parse dynamic gating. Update React Compiler config to fix the error', + description: `${fromZodError(result.error)}`, + loc: null, + suggestions: null, + }); + } + } + break; + } + case 'customOptOutDirectives': { + const result = CustomOptOutDirectiveSchema.safeParse(value); + if (result.success) { + parsedOptions[key] = result.data; + } else { + CompilerError.throwInvalidConfig({ + reason: + 'Could not parse custom opt out directives. Update React Compiler config to fix the error', + description: `${fromZodError(result.error)}`, + loc: null, + suggestions: null, + }); + } + break; + } default: { parsedOptions[key] = value; } 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 831d1ca38054e..e5005d02c4ef7 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Pipeline.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Pipeline.ts @@ -33,9 +33,7 @@ import {findContextIdentifiers} from '../HIR/FindContextIdentifiers'; import { analyseFunctions, dropManualMemoization, - inferMutableRanges, inferReactivePlaces, - inferReferenceEffects, inlineImmediatelyInvokedFunctionExpressions, inferEffectDependencies, } from '../Inference'; @@ -92,18 +90,19 @@ import { } from '../Validation'; import {validateLocalsNotReassignedAfterRender} from '../Validation/ValidateLocalsNotReassignedAfterRender'; import {outlineFunctions} from '../Optimization/OutlineFunctions'; -import {propagatePhiTypes} from '../TypeInference/PropagatePhiTypes'; import {lowerContextAccess} from '../Optimization/LowerContextAccess'; -import {validateNoSetStateInPassiveEffects} from '../Validation/ValidateNoSetStateInPassiveEffects'; +import {validateNoSetStateInEffects} from '../Validation/ValidateNoSetStateInEffects'; import {validateNoJSXInTryStatement} from '../Validation/ValidateNoJSXInTryStatement'; import {propagateScopeDependenciesHIR} from '../HIR/PropagateScopeDependenciesHIR'; import {outlineJSX} from '../Optimization/OutlineJsx'; import {optimizePropsMethodCalls} from '../Optimization/OptimizePropsMethodCalls'; import {transformFire} from '../Transform'; import {validateNoImpureFunctionsInRender} from '../Validation/ValidateNoImpureFunctionsInRender'; -import {CompilerError} from '..'; import {validateStaticComponents} from '../Validation/ValidateStaticComponents'; import {validateNoFreezingKnownMutableFunctions} from '../Validation/ValidateNoFreezingKnownMutableFunctions'; +import {inferMutationAliasingEffects} from '../Inference/InferMutationAliasingEffects'; +import {inferMutationAliasingRanges} from '../Inference/InferMutationAliasingRanges'; +import {validateNoDerivedComputationsInEffects} from '../Validation/ValidateNoDerivedComputationsInEffects'; export type CompilerPipelineValue = | {kind: 'ast'; name: string; value: CodegenFunction} @@ -130,6 +129,7 @@ function run( mode, config, contextIdentifiers, + func, logger, filename, code, @@ -171,7 +171,7 @@ function runWithEnvironment( !env.config.disableMemoizationForDebugging && !env.config.enableChangeDetectionForDebugging ) { - dropManualMemoization(hir); + dropManualMemoization(hir).unwrap(); log({kind: 'hir', name: 'DropManualMemoization', value: hir}); } @@ -226,15 +226,13 @@ function runWithEnvironment( analyseFunctions(hir); log({kind: 'hir', name: 'AnalyseFunctions', value: hir}); - const fnEffectErrors = inferReferenceEffects(hir); + const mutabilityAliasingErrors = inferMutationAliasingEffects(hir); + log({kind: 'hir', name: 'InferMutationAliasingEffects', value: hir}); if (env.isInferredMemoEnabled) { - if (fnEffectErrors.length > 0) { - CompilerError.throw(fnEffectErrors[0]); + if (mutabilityAliasingErrors.isErr()) { + throw mutabilityAliasingErrors.unwrapErr(); } } - log({kind: 'hir', name: 'InferReferenceEffects', value: hir}); - - validateLocalsNotReassignedAfterRender(hir); // Note: Has to come after infer reference effects because "dead" code may still affect inference deadCodeElimination(hir); @@ -248,8 +246,16 @@ function runWithEnvironment( pruneMaybeThrows(hir); log({kind: 'hir', name: 'PruneMaybeThrows', value: hir}); - inferMutableRanges(hir); - log({kind: 'hir', name: 'InferMutableRanges', value: hir}); + const mutabilityAliasingRangeErrors = inferMutationAliasingRanges(hir, { + isFunctionExpression: false, + }); + log({kind: 'hir', name: 'InferMutationAliasingRanges', value: hir}); + if (env.isInferredMemoEnabled) { + if (mutabilityAliasingRangeErrors.isErr()) { + throw mutabilityAliasingRangeErrors.unwrapErr(); + } + validateLocalsNotReassignedAfterRender(hir); + } if (env.isInferredMemoEnabled) { if (env.config.assertValidMutableRanges) { @@ -264,8 +270,12 @@ function runWithEnvironment( validateNoSetStateInRender(hir).unwrap(); } - if (env.config.validateNoSetStateInPassiveEffects) { - env.logErrors(validateNoSetStateInPassiveEffects(hir)); + if (env.config.validateNoDerivedComputationsInEffects) { + validateNoDerivedComputationsInEffects(hir); + } + + if (env.config.validateNoSetStateInEffects) { + env.logErrors(validateNoSetStateInEffects(hir)); } if (env.config.validateNoJSXInTryStatements) { @@ -276,9 +286,7 @@ function runWithEnvironment( validateNoImpureFunctionsInRender(hir).unwrap(); } - if (env.config.validateNoFreezingKnownMutableFunctions) { - validateNoFreezingKnownMutableFunctions(hir).unwrap(); - } + validateNoFreezingKnownMutableFunctions(hir).unwrap(); } inferReactivePlaces(hir); @@ -291,13 +299,6 @@ function runWithEnvironment( value: hir, }); - propagatePhiTypes(hir); - log({ - kind: 'hir', - name: 'PropagatePhiTypes', - value: hir, - }); - if (env.isInferredMemoEnabled) { if (env.config.validateStaticComponents) { env.logErrors(validateStaticComponents(hir)); 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 64abc110ea12f..5a9ef9495fa1d 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Program.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Program.ts @@ -10,9 +10,10 @@ import * as t from '@babel/types'; import { CompilerError, CompilerErrorDetail, + ErrorCategory, ErrorSeverity, } from '../CompilerError'; -import {ReactFunctionType} from '../HIR/Environment'; +import {ExternalFunction, ReactFunctionType} from '../HIR/Environment'; import {CodegenFunction} from '../ReactiveScopes'; import {isComponentDeclaration} from '../Utils/ComponentDeclaration'; import {isHookDeclaration} from '../Utils/HookDeclaration'; @@ -31,6 +32,7 @@ import { suppressionsToCompilerError, } from './Suppression'; import {GeneratedSource} from '../HIR'; +import {Err, Ok, Result} from '../Utils/Result'; export type CompilerPass = { opts: PluginOptions; @@ -40,26 +42,104 @@ export type CompilerPass = { }; export const OPT_IN_DIRECTIVES = new Set(['use forget', 'use memo']); export const OPT_OUT_DIRECTIVES = new Set(['use no forget', 'use no memo']); +const DYNAMIC_GATING_DIRECTIVE = new RegExp('^use memo if\\(([^\\)]*)\\)$'); -export function findDirectiveEnablingMemoization( +export function tryFindDirectiveEnablingMemoization( directives: Array, -): t.Directive | null { - return ( - directives.find(directive => - OPT_IN_DIRECTIVES.has(directive.value.value), - ) ?? null + opts: PluginOptions, +): Result { + const optIn = directives.find(directive => + OPT_IN_DIRECTIVES.has(directive.value.value), ); + if (optIn != null) { + return Ok(optIn); + } + const dynamicGating = findDirectivesDynamicGating(directives, opts); + if (dynamicGating.isOk()) { + return Ok(dynamicGating.unwrap()?.directive ?? null); + } else { + return Err(dynamicGating.unwrapErr()); + } } export function findDirectiveDisablingMemoization( directives: Array, + {customOptOutDirectives}: PluginOptions, ): t.Directive | null { + if (customOptOutDirectives != null) { + return ( + directives.find( + directive => + customOptOutDirectives.indexOf(directive.value.value) !== -1, + ) ?? null + ); + } return ( directives.find(directive => OPT_OUT_DIRECTIVES.has(directive.value.value), ) ?? null ); } +function findDirectivesDynamicGating( + directives: Array, + opts: PluginOptions, +): Result< + { + gating: ExternalFunction; + directive: t.Directive; + } | null, + CompilerError +> { + if (opts.dynamicGating === null) { + return Ok(null); + } + const errors = new CompilerError(); + const result: Array<{directive: t.Directive; match: string}> = []; + + for (const directive of directives) { + const maybeMatch = DYNAMIC_GATING_DIRECTIVE.exec(directive.value.value); + if (maybeMatch != null && maybeMatch[1] != null) { + if (t.isValidIdentifier(maybeMatch[1])) { + result.push({directive, match: maybeMatch[1]}); + } else { + errors.push({ + reason: `Dynamic gating directive is not a valid JavaScript identifier`, + description: `Found '${directive.value.value}'`, + severity: ErrorSeverity.InvalidReact, + category: ErrorCategory.Gating, + loc: directive.loc ?? null, + suggestions: null, + }); + } + } + } + if (errors.hasErrors()) { + return Err(errors); + } else if (result.length > 1) { + const error = new CompilerError(); + error.push({ + reason: `Multiple dynamic gating directives found`, + description: `Expected a single directive but found [${result + .map(r => r.directive.value.value) + .join(', ')}]`, + severity: ErrorSeverity.InvalidReact, + category: ErrorCategory.Gating, + loc: result[0].directive.loc ?? null, + suggestions: null, + }); + return Err(error); + } else if (result.length === 1) { + return Ok({ + gating: { + source: opts.dynamicGating.source, + importSpecifierName: result[0].match, + }, + directive: result[0].directive, + }); + } else { + return Ok(null); + } +} function isCriticalError(err: unknown): boolean { return !(err instanceof CompilerError) || err.isCritical(); @@ -104,7 +184,7 @@ function logError( context.opts.logger.logEvent(context.filename, { kind: 'CompileError', fnLoc, - detail: detail.options, + detail, }); } } else { @@ -326,7 +406,8 @@ export function compileProgram( code: pass.code, suppressions, hasModuleScopeOptOut: - findDirectiveDisablingMemoization(program.node.directives) != null, + findDirectiveDisablingMemoization(program.node.directives, pass.opts) != + null, }); const queue: Array = findFunctionsToCompile( @@ -378,6 +459,7 @@ export function compileProgram( reason: 'Unexpected compiled functions when module scope opt-out is present', severity: ErrorSeverity.Invariant, + category: ErrorCategory.Invariant, loc: null, }), ); @@ -412,7 +494,20 @@ function findFunctionsToCompile( ): Array { const queue: Array = []; const traverseFunction = (fn: BabelFn, pass: CompilerPass): void => { + // In 'all' mode, compile only top level functions + if ( + pass.opts.compilationMode === 'all' && + fn.scope.getProgramParent() !== fn.scope.parent + ) { + return; + } + const fnType = getReactFunctionType(fn, pass); + + if (pass.opts.environment.validateNoDynamicallyCreatedComponentsOrHooks) { + validateNoDynamicallyCreatedComponentsOrHooks(fn, pass, programContext); + } + if (fnType === null || programContext.alreadyCompiled.has(fn.node)) { return; } @@ -477,13 +572,36 @@ function processFn( fnType: ReactFunctionType, programContext: ProgramContext, ): null | CodegenFunction { - let directives; + let directives: { + optIn: t.Directive | null; + optOut: t.Directive | null; + }; if (fn.node.body.type !== 'BlockStatement') { - directives = {optIn: null, optOut: null}; + directives = { + optIn: null, + optOut: null, + }; } else { + const optIn = tryFindDirectiveEnablingMemoization( + fn.node.body.directives, + programContext.opts, + ); + if (optIn.isErr()) { + /** + * If parsing opt-in directive fails, it's most likely that React Compiler + * was not tested or rolled out on this function. In that case, we handle + * the error and fall back to the safest option which is to not optimize + * the function. + */ + handleError(optIn.unwrapErr(), programContext, fn.node.loc ?? null); + return null; + } directives = { - optIn: findDirectiveEnablingMemoization(fn.node.body.directives), - optOut: findDirectiveDisablingMemoization(fn.node.body.directives), + optIn: optIn.unwrapOr(null), + optOut: findDirectiveDisablingMemoization( + fn.node.body.directives, + programContext.opts, + ), }; } @@ -659,25 +777,31 @@ function applyCompiledFunctions( pass: CompilerPass, programContext: ProgramContext, ): void { - const referencedBeforeDeclared = - pass.opts.gating != null - ? getFunctionReferencedBeforeDeclarationAtTopLevel(program, compiledFns) - : null; + let referencedBeforeDeclared = null; for (const result of compiledFns) { const {kind, originalFn, compiledFn} = result; const transformedFn = createNewFunctionNode(originalFn, compiledFn); programContext.alreadyCompiled.add(transformedFn); - if (referencedBeforeDeclared != null && kind === 'original') { - CompilerError.invariant(pass.opts.gating != null, { - reason: "Expected 'gating' import to be present", - loc: null, - }); + let dynamicGating: ExternalFunction | null = null; + if (originalFn.node.body.type === 'BlockStatement') { + const result = findDirectivesDynamicGating( + originalFn.node.body.directives, + pass.opts, + ); + if (result.isOk()) { + dynamicGating = result.unwrap()?.gating ?? null; + } + } + const functionGating = dynamicGating ?? pass.opts.gating; + if (kind === 'original' && functionGating != null) { + referencedBeforeDeclared ??= + getFunctionReferencedBeforeDeclarationAtTopLevel(program, compiledFns); insertGatedFunctionDeclaration( originalFn, transformedFn, programContext, - pass.opts.gating, + functionGating, referencedBeforeDeclared.has(result), ); } else { @@ -704,6 +828,7 @@ function shouldSkipCompilation( description: "When the 'sources' config options is specified, the React compiler will only compile files with a name", severity: ErrorSeverity.InvalidConfig, + category: ErrorCategory.Config, loc: null, }), ); @@ -727,14 +852,86 @@ function shouldSkipCompilation( return false; } +/** + * Validates that Components/Hooks are always defined at module level. This prevents scope reference + * errors that occur when the compiler attempts to optimize the nested component/hook while its + * parent function remains uncompiled. + */ +function validateNoDynamicallyCreatedComponentsOrHooks( + fn: BabelFn, + pass: CompilerPass, + programContext: ProgramContext, +): void { + const parentNameExpr = getFunctionName(fn); + const parentName = + parentNameExpr !== null && parentNameExpr.isIdentifier() + ? parentNameExpr.node.name + : ''; + + const validateNestedFunction = ( + nestedFn: NodePath< + t.FunctionDeclaration | t.FunctionExpression | t.ArrowFunctionExpression + >, + ): void => { + if ( + nestedFn.node === fn.node || + programContext.alreadyCompiled.has(nestedFn.node) + ) { + return; + } + + if (nestedFn.scope.getProgramParent() !== nestedFn.scope.parent) { + const nestedFnType = getReactFunctionType(nestedFn as BabelFn, pass); + const nestedFnNameExpr = getFunctionName(nestedFn as BabelFn); + const nestedName = + nestedFnNameExpr !== null && nestedFnNameExpr.isIdentifier() + ? nestedFnNameExpr.node.name + : ''; + if (nestedFnType === 'Component' || nestedFnType === 'Hook') { + CompilerError.throwDiagnostic({ + category: ErrorCategory.Factories, + severity: ErrorSeverity.InvalidReact, + reason: `Components and hooks cannot be created dynamically`, + description: `The function \`${nestedName}\` appears to be a React ${nestedFnType.toLowerCase()}, but it's defined inside \`${parentName}\`. Components and Hooks should always be declared at module scope`, + details: [ + { + kind: 'error', + message: 'this function dynamically created a component/hook', + loc: parentNameExpr?.node.loc ?? fn.node.loc ?? null, + }, + { + kind: 'error', + message: 'the component is created here', + loc: nestedFnNameExpr?.node.loc ?? nestedFn.node.loc ?? null, + }, + ], + }); + } + } + + nestedFn.skip(); + }; + + fn.traverse({ + FunctionDeclaration: validateNestedFunction, + FunctionExpression: validateNestedFunction, + ArrowFunctionExpression: validateNestedFunction, + }); +} + function getReactFunctionType( fn: BabelFn, pass: CompilerPass, ): ReactFunctionType | null { const hookPattern = pass.opts.environment.hookPattern; if (fn.node.body.type === 'BlockStatement') { - if (findDirectiveEnablingMemoization(fn.node.body.directives) != null) + const optInDirectives = tryFindDirectiveEnablingMemoization( + fn.node.body.directives, + pass.opts, + ); + if (optInDirectives.unwrapOr(null) != null) { return getComponentOrHookLike(fn, hookPattern) ?? 'Other'; + } } // Component and hook declarations are known components/hooks @@ -760,11 +957,6 @@ function getReactFunctionType( return componentSyntaxType; } case 'all': { - // Compile only top level functions - if (fn.scope.getProgramParent() !== fn.scope.parent) { - return null; - } - return getComponentOrHookLike(fn, hookPattern) ?? 'Other'; } default: { diff --git a/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Suppression.ts b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Suppression.ts index 4d0369f5210ca..a0d06f96f0e52 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Suppression.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Suppression.ts @@ -8,9 +8,10 @@ import {NodePath} from '@babel/core'; import * as t from '@babel/types'; import { + CompilerDiagnostic, CompilerError, - CompilerErrorDetail, CompilerSuggestionOperation, + ErrorCategory, ErrorSeverity, } from '../CompilerError'; import {assertExhaustive} from '../Utils/utils'; @@ -181,12 +182,12 @@ export function suppressionsToCompilerError( 'Unhandled suppression source', ); } - error.pushErrorDetail( - new CompilerErrorDetail({ - reason: `${reason}. React Compiler only works when your components follow all the rules of React, disabling them may result in unexpected or incorrect behavior`, - description: suppressionRange.disableComment.value.trim(), + error.pushDiagnostic( + CompilerDiagnostic.create({ + reason: reason, + description: `React Compiler only works when your components follow all the rules of React, disabling them may result in unexpected or incorrect behavior. Found suppression \`${suppressionRange.disableComment.value.trim()}\``, severity: ErrorSeverity.InvalidReact, - loc: suppressionRange.disableComment.loc ?? null, + category: ErrorCategory.Suppression, suggestions: [ { description: suggestion, @@ -197,6 +198,10 @@ export function suppressionsToCompilerError( op: CompilerSuggestionOperation.Remove, }, ], + }).withDetail({ + kind: 'error', + loc: suppressionRange.disableComment.loc ?? null, + message: 'Found React rule suppression', }), ); } diff --git a/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/ValidateNoUntransformedReferences.ts b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/ValidateNoUntransformedReferences.ts index e288c227ad25c..beaaff0f79a33 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/ValidateNoUntransformedReferences.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/ValidateNoUntransformedReferences.ts @@ -8,35 +8,67 @@ import {NodePath} from '@babel/core'; import * as t from '@babel/types'; -import { - CompilerError, - CompilerErrorDetailOptions, - EnvironmentConfig, - ErrorSeverity, - Logger, -} from '..'; +import {CompilerError, EnvironmentConfig, ErrorSeverity, Logger} from '..'; import {getOrInsertWith} from '../Utils/utils'; -import {Environment} from '../HIR'; +import {Environment, GeneratedSource} from '../HIR'; import {DEFAULT_EXPORT} from '../HIR/Environment'; import {CompileProgramMetadata} from './Program'; +import { + CompilerDiagnostic, + CompilerDiagnosticOptions, + ErrorCategory, +} from '../CompilerError'; function throwInvalidReact( - options: Omit, + options: Omit, {logger, filename}: TraversalState, ): never { - const detail: CompilerErrorDetailOptions = { - ...options, + const detail: CompilerDiagnosticOptions = { severity: ErrorSeverity.InvalidReact, + ...options, }; logger?.logEvent(filename, { kind: 'CompileError', fnLoc: null, - detail, + detail: new CompilerDiagnostic(detail), }); - CompilerError.throw(detail); + CompilerError.throwDiagnostic(detail); +} + +function isAutodepsSigil( + arg: NodePath, +): boolean { + // Check for AUTODEPS identifier imported from React + if (arg.isIdentifier() && arg.node.name === 'AUTODEPS') { + const binding = arg.scope.getBinding(arg.node.name); + if (binding && binding.path.isImportSpecifier()) { + const importSpecifier = binding.path.node as t.ImportSpecifier; + if (importSpecifier.imported.type === 'Identifier') { + return (importSpecifier.imported as t.Identifier).name === 'AUTODEPS'; + } + } + return false; + } + + // Check for React.AUTODEPS member expression + if (arg.isMemberExpression() && !arg.node.computed) { + const object = arg.get('object'); + const property = arg.get('property'); + + if ( + object.isIdentifier() && + object.node.name === 'React' && + property.isIdentifier() && + property.node.name === 'AUTODEPS' + ) { + return true; + } + } + + return false; } function assertValidEffectImportReference( - numArgs: number, + autodepsIndex: number, paths: Array>, context: TraversalState, ): void { @@ -49,11 +81,10 @@ function assertValidEffectImportReference( maybeCalleeLoc != null && context.inferredEffectLocations.has(maybeCalleeLoc); /** - * Only error on untransformed references of the form `useMyEffect(...)` - * or `moduleNamespace.useMyEffect(...)`, with matching argument counts. - * TODO: do we also want a mode to also hard error on non-call references? + * Error on effect calls that still have AUTODEPS in their args */ - if (args.length === numArgs && !hasInferredEffect) { + const hasAutodepsArg = args.some(isAutodepsSigil); + if (hasAutodepsArg && !hasInferredEffect) { const maybeErrorDiagnostic = matchCompilerDiagnostic( path, context.transformErrors, @@ -65,14 +96,19 @@ function assertValidEffectImportReference( */ throwInvalidReact( { + category: ErrorCategory.AutomaticEffectDependencies, reason: - '[InferEffectDependencies] React Compiler is unable to infer dependencies of this effect. ' + - 'This will break your build! ' + - 'To resolve, either pass your own dependency array or fix reported compiler bailout diagnostics.', - description: maybeErrorDiagnostic - ? `(Bailout reason: ${maybeErrorDiagnostic})` - : null, - loc: parent.node.loc ?? null, + 'Cannot infer dependencies of this effect. This will break your build!', + description: + 'To resolve, either pass a dependency array or fix reported compiler bailout diagnostics.' + + (maybeErrorDiagnostic ? ` ${maybeErrorDiagnostic}` : ''), + details: [ + { + kind: 'error', + message: 'Cannot infer dependencies', + loc: parent.node.loc ?? GeneratedSource, + }, + ], }, context, ); @@ -92,13 +128,20 @@ function assertValidFireImportReference( ); throwInvalidReact( { - reason: - '[Fire] Untransformed reference to compiler-required feature. ' + - 'Either remove this `fire` call or ensure it is successfully transformed by the compiler', - description: maybeErrorDiagnostic - ? `(Bailout reason: ${maybeErrorDiagnostic})` - : null, - loc: paths[0].node.loc ?? null, + category: ErrorCategory.Fire, + reason: '[Fire] Untransformed reference to compiler-required feature.', + description: + 'Either remove this `fire` call or ensure it is successfully transformed by the compiler' + + maybeErrorDiagnostic + ? ` ${maybeErrorDiagnostic}` + : '', + details: [ + { + kind: 'error', + message: 'Untransformed `fire` call', + loc: paths[0].node.loc ?? GeneratedSource, + }, + ], }, context, ); @@ -128,12 +171,12 @@ export default function validateNoUntransformedReferences( if (env.inferEffectDependencies) { for (const { function: {source, importSpecifierName}, - numRequiredArgs, + autodepsIndex, } of env.inferEffectDependencies) { const module = getOrInsertWith(moduleLoadChecks, source, () => new Map()); module.set( importSpecifierName, - assertValidEffectImportReference.bind(null, numRequiredArgs), + assertValidEffectImportReference.bind(null, autodepsIndex), ); } } diff --git a/compiler/packages/babel-plugin-react-compiler/src/Flood/FlowTypes.ts b/compiler/packages/babel-plugin-react-compiler/src/Flood/FlowTypes.ts new file mode 100644 index 0000000000000..c63feb830feeb --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/Flood/FlowTypes.ts @@ -0,0 +1,752 @@ +/** + * TypeScript definitions for Flow type JSON representations + * Based on the output of /data/sandcastle/boxes/fbsource/fbcode/flow/src/typing/convertTypes.ml + */ + +// Base type for all Flow types with a kind field +export interface BaseFlowType { + kind: string; +} + +// Type for representing polarity +export type Polarity = 'positive' | 'negative' | 'neutral'; + +// Type for representing a name that might be null +export type OptionalName = string | null; + +// Open type +export interface OpenType extends BaseFlowType { + kind: 'Open'; +} + +// Def type +export interface DefType extends BaseFlowType { + kind: 'Def'; + def: DefT; +} + +// Eval type +export interface EvalType extends BaseFlowType { + kind: 'Eval'; + type: FlowType; + destructor: Destructor; +} + +// Generic type +export interface GenericType extends BaseFlowType { + kind: 'Generic'; + name: string; + bound: FlowType; + no_infer: boolean; +} + +// ThisInstance type +export interface ThisInstanceType extends BaseFlowType { + kind: 'ThisInstance'; + instance: InstanceT; + is_this: boolean; + name: string; +} + +// ThisTypeApp type +export interface ThisTypeAppType extends BaseFlowType { + kind: 'ThisTypeApp'; + t1: FlowType; + t2: FlowType; + t_list?: Array; +} + +// TypeApp type +export interface TypeAppType extends BaseFlowType { + kind: 'TypeApp'; + type: FlowType; + targs: Array; + from_value: boolean; + use_desc: boolean; +} + +// FunProto type +export interface FunProtoType extends BaseFlowType { + kind: 'FunProto'; +} + +// ObjProto type +export interface ObjProtoType extends BaseFlowType { + kind: 'ObjProto'; +} + +// NullProto type +export interface NullProtoType extends BaseFlowType { + kind: 'NullProto'; +} + +// FunProtoBind type +export interface FunProtoBindType extends BaseFlowType { + kind: 'FunProtoBind'; +} + +// Intersection type +export interface IntersectionType extends BaseFlowType { + kind: 'Intersection'; + members: Array; +} + +// Union type +export interface UnionType extends BaseFlowType { + kind: 'Union'; + members: Array; +} + +// Maybe type +export interface MaybeType extends BaseFlowType { + kind: 'Maybe'; + type: FlowType; +} + +// Optional type +export interface OptionalType extends BaseFlowType { + kind: 'Optional'; + type: FlowType; + use_desc: boolean; +} + +// Keys type +export interface KeysType extends BaseFlowType { + kind: 'Keys'; + type: FlowType; +} + +// Annot type +export interface AnnotType extends BaseFlowType { + kind: 'Annot'; + type: FlowType; + use_desc: boolean; +} + +// Opaque type +export interface OpaqueType extends BaseFlowType { + kind: 'Opaque'; + opaquetype: { + opaque_id: string; + underlying_t: FlowType | null; + super_t: FlowType | null; + opaque_type_args: Array<{ + name: string; + type: FlowType; + polarity: Polarity; + }>; + opaque_name: string; + }; +} + +// Namespace type +export interface NamespaceType extends BaseFlowType { + kind: 'Namespace'; + namespace_symbol: { + symbol: string; + }; + values_type: FlowType; + types_tmap: PropertyMap; +} + +// Any type +export interface AnyType extends BaseFlowType { + kind: 'Any'; +} + +// StrUtil type +export interface StrUtilType extends BaseFlowType { + kind: 'StrUtil'; + op: 'StrPrefix' | 'StrSuffix'; + prefix?: string; + suffix?: string; + remainder?: FlowType; +} + +// TypeParam definition +export interface TypeParam { + name: string; + bound: FlowType; + polarity: Polarity; + default: FlowType | null; +} + +// EnumInfo types +export type EnumInfo = ConcreteEnum | AbstractEnum; + +export interface ConcreteEnum { + kind: 'ConcreteEnum'; + enum_name: string; + enum_id: string; + members: Array; + representation_t: FlowType; + has_unknown_members: boolean; +} + +export interface AbstractEnum { + kind: 'AbstractEnum'; + representation_t: FlowType; +} + +// CanonicalRendersForm types +export type CanonicalRendersForm = + | InstrinsicRenders + | NominalRenders + | StructuralRenders + | DefaultRenders; + +export interface InstrinsicRenders { + kind: 'InstrinsicRenders'; + name: string; +} + +export interface NominalRenders { + kind: 'NominalRenders'; + renders_id: string; + renders_name: string; + renders_super: FlowType; +} + +export interface StructuralRenders { + kind: 'StructuralRenders'; + renders_variant: 'RendersNormal' | 'RendersMaybe' | 'RendersStar'; + renders_structural_type: FlowType; +} + +export interface DefaultRenders { + kind: 'DefaultRenders'; +} + +// InstanceT definition +export interface InstanceT { + inst: InstType; + static: FlowType; + super: FlowType; + implements: Array; +} + +// InstType definition +export interface InstType { + class_name: string | null; + class_id: string; + type_args: Array<{ + name: string; + type: FlowType; + polarity: Polarity; + }>; + own_props: PropertyMap; + proto_props: PropertyMap; + call_t: null | { + id: number; + call: FlowType; + }; +} + +// DefT types +export type DefT = + | NumGeneralType + | StrGeneralType + | BoolGeneralType + | BigIntGeneralType + | EmptyType + | MixedType + | NullType + | VoidType + | SymbolType + | FunType + | ObjType + | ArrType + | ClassType + | InstanceType + | SingletonStrType + | NumericStrKeyType + | SingletonNumType + | SingletonBoolType + | SingletonBigIntType + | TypeType + | PolyType + | ReactAbstractComponentType + | RendersType + | EnumValueType + | EnumObjectType; + +export interface NumGeneralType extends BaseFlowType { + kind: 'NumGeneral'; +} + +export interface StrGeneralType extends BaseFlowType { + kind: 'StrGeneral'; +} + +export interface BoolGeneralType extends BaseFlowType { + kind: 'BoolGeneral'; +} + +export interface BigIntGeneralType extends BaseFlowType { + kind: 'BigIntGeneral'; +} + +export interface EmptyType extends BaseFlowType { + kind: 'Empty'; +} + +export interface MixedType extends BaseFlowType { + kind: 'Mixed'; +} + +export interface NullType extends BaseFlowType { + kind: 'Null'; +} + +export interface VoidType extends BaseFlowType { + kind: 'Void'; +} + +export interface SymbolType extends BaseFlowType { + kind: 'Symbol'; +} + +export interface FunType extends BaseFlowType { + kind: 'Fun'; + static: FlowType; + funtype: FunTypeObj; +} + +export interface ObjType extends BaseFlowType { + kind: 'Obj'; + objtype: ObjTypeObj; +} + +export interface ArrType extends BaseFlowType { + kind: 'Arr'; + arrtype: ArrTypeObj; +} + +export interface ClassType extends BaseFlowType { + kind: 'Class'; + type: FlowType; +} + +export interface InstanceType extends BaseFlowType { + kind: 'Instance'; + instance: InstanceT; +} + +export interface SingletonStrType extends BaseFlowType { + kind: 'SingletonStr'; + from_annot: boolean; + value: string; +} + +export interface NumericStrKeyType extends BaseFlowType { + kind: 'NumericStrKey'; + number: string; + string: string; +} + +export interface SingletonNumType extends BaseFlowType { + kind: 'SingletonNum'; + from_annot: boolean; + number: string; + string: string; +} + +export interface SingletonBoolType extends BaseFlowType { + kind: 'SingletonBool'; + from_annot: boolean; + value: boolean; +} + +export interface SingletonBigIntType extends BaseFlowType { + kind: 'SingletonBigInt'; + from_annot: boolean; + value: string; +} + +export interface TypeType extends BaseFlowType { + kind: 'Type'; + type_kind: TypeTKind; + type: FlowType; +} + +export type TypeTKind = + | 'TypeAliasKind' + | 'TypeParamKind' + | 'OpaqueKind' + | 'ImportTypeofKind' + | 'ImportClassKind' + | 'ImportEnumKind' + | 'InstanceKind' + | 'RenderTypeKind'; + +export interface PolyType extends BaseFlowType { + kind: 'Poly'; + tparams: Array; + t_out: FlowType; + id: string; +} + +export interface ReactAbstractComponentType extends BaseFlowType { + kind: 'ReactAbstractComponent'; + config: FlowType; + renders: FlowType; + instance: ComponentInstance; + component_kind: ComponentKind; +} + +export type ComponentInstance = + | {kind: 'RefSetterProp'; type: FlowType} + | {kind: 'Omitted'}; + +export type ComponentKind = + | {kind: 'Structural'} + | {kind: 'Nominal'; id: string; name: string; types: Array | null}; + +export interface RendersType extends BaseFlowType { + kind: 'Renders'; + form: CanonicalRendersForm; +} + +export interface EnumValueType extends BaseFlowType { + kind: 'EnumValue'; + enum_info: EnumInfo; +} + +export interface EnumObjectType extends BaseFlowType { + kind: 'EnumObject'; + enum_value_t: FlowType; + enum_info: EnumInfo; +} + +// ObjKind types +export type ObjKind = + | {kind: 'Exact'} + | {kind: 'Inexact'} + | {kind: 'Indexed'; dicttype: DictType}; + +// DictType definition +export interface DictType { + dict_name: string | null; + key: FlowType; + value: FlowType; + dict_polarity: Polarity; +} + +// ArrType types +export type ArrTypeObj = ArrayAT | TupleAT | ROArrayAT; + +export interface ArrayAT { + kind: 'ArrayAT'; + elem_t: FlowType; +} + +export interface TupleAT { + kind: 'TupleAT'; + elem_t: FlowType; + elements: Array; + min_arity: number; + max_arity: number; + inexact: boolean; +} + +export interface ROArrayAT { + kind: 'ROArrayAT'; + elem_t: FlowType; +} + +// TupleElement definition +export interface TupleElement { + name: string | null; + t: FlowType; + polarity: Polarity; + optional: boolean; +} + +// Flags definition +export interface Flags { + obj_kind: ObjKind; +} + +// Property types +export type Property = + | FieldProperty + | GetProperty + | SetProperty + | GetSetProperty + | MethodProperty; + +export interface FieldProperty { + kind: 'Field'; + type: FlowType; + polarity: Polarity; +} + +export interface GetProperty { + kind: 'Get'; + type: FlowType; +} + +export interface SetProperty { + kind: 'Set'; + type: FlowType; +} + +export interface GetSetProperty { + kind: 'GetSet'; + get_type: FlowType; + set_type: FlowType; +} + +export interface MethodProperty { + kind: 'Method'; + type: FlowType; +} + +// PropertyMap definition +export interface PropertyMap { + [key: string]: Property; // For other properties in the map +} + +// ObjType definition +export interface ObjTypeObj { + flags: Flags; + props: PropertyMap; + proto_t: FlowType; + call_t: number | null; +} + +// FunType definition +export interface FunTypeObj { + this_t: { + type: FlowType; + status: ThisStatus; + }; + params: Array<{ + name: string | null; + type: FlowType; + }>; + rest_param: null | { + name: string | null; + type: FlowType; + }; + return_t: FlowType; + type_guard: null | { + inferred: boolean; + param_name: string; + type_guard: FlowType; + one_sided: boolean; + }; + effect: Effect; +} + +// ThisStatus types +export type ThisStatus = + | {kind: 'This_Method'; unbound: boolean} + | {kind: 'This_Function'}; + +// Effect types +export type Effect = + | {kind: 'HookDecl'; id: string} + | {kind: 'HookAnnot'} + | {kind: 'ArbitraryEffect'} + | {kind: 'AnyEffect'}; + +// Destructor types +export type Destructor = + | NonMaybeTypeDestructor + | PropertyTypeDestructor + | ElementTypeDestructor + | OptionalIndexedAccessNonMaybeTypeDestructor + | OptionalIndexedAccessResultTypeDestructor + | ExactTypeDestructor + | ReadOnlyTypeDestructor + | PartialTypeDestructor + | RequiredTypeDestructor + | SpreadTypeDestructor + | SpreadTupleTypeDestructor + | RestTypeDestructor + | ValuesTypeDestructor + | ConditionalTypeDestructor + | TypeMapDestructor + | ReactElementPropsTypeDestructor + | ReactElementConfigTypeDestructor + | ReactCheckComponentConfigDestructor + | ReactDRODestructor + | MakeHooklikeDestructor + | MappedTypeDestructor + | EnumTypeDestructor; + +export interface NonMaybeTypeDestructor { + kind: 'NonMaybeType'; +} + +export interface PropertyTypeDestructor { + kind: 'PropertyType'; + name: string; +} + +export interface ElementTypeDestructor { + kind: 'ElementType'; + index_type: FlowType; +} + +export interface OptionalIndexedAccessNonMaybeTypeDestructor { + kind: 'OptionalIndexedAccessNonMaybeType'; + index: OptionalIndexedAccessIndex; +} + +export type OptionalIndexedAccessIndex = + | {kind: 'StrLitIndex'; name: string} + | {kind: 'TypeIndex'; type: FlowType}; + +export interface OptionalIndexedAccessResultTypeDestructor { + kind: 'OptionalIndexedAccessResultType'; +} + +export interface ExactTypeDestructor { + kind: 'ExactType'; +} + +export interface ReadOnlyTypeDestructor { + kind: 'ReadOnlyType'; +} + +export interface PartialTypeDestructor { + kind: 'PartialType'; +} + +export interface RequiredTypeDestructor { + kind: 'RequiredType'; +} + +export interface SpreadTypeDestructor { + kind: 'SpreadType'; + target: SpreadTarget; + operands: Array; + operand_slice: Slice | null; +} + +export type SpreadTarget = + | {kind: 'Value'; make_seal: 'Sealed' | 'Frozen' | 'As_Const'} + | {kind: 'Annot'; make_exact: boolean}; + +export type SpreadOperand = {kind: 'Type'; type: FlowType} | Slice; + +export interface Slice { + kind: 'Slice'; + prop_map: PropertyMap; + generics: Array; + dict: DictType | null; + reachable_targs: Array<{ + type: FlowType; + polarity: Polarity; + }>; +} + +export interface SpreadTupleTypeDestructor { + kind: 'SpreadTupleType'; + inexact: boolean; + resolved_rev: string; + unresolved: string; +} + +export interface RestTypeDestructor { + kind: 'RestType'; + merge_mode: RestMergeMode; + type: FlowType; +} + +export type RestMergeMode = + | {kind: 'SpreadReversal'} + | {kind: 'ReactConfigMerge'; polarity: Polarity} + | {kind: 'Omit'}; + +export interface ValuesTypeDestructor { + kind: 'ValuesType'; +} + +export interface ConditionalTypeDestructor { + kind: 'ConditionalType'; + distributive_tparam_name: string | null; + infer_tparams: string; + extends_t: FlowType; + true_t: FlowType; + false_t: FlowType; +} + +export interface TypeMapDestructor { + kind: 'ObjectKeyMirror'; +} + +export interface ReactElementPropsTypeDestructor { + kind: 'ReactElementPropsType'; +} + +export interface ReactElementConfigTypeDestructor { + kind: 'ReactElementConfigType'; +} + +export interface ReactCheckComponentConfigDestructor { + kind: 'ReactCheckComponentConfig'; + props: { + [key: string]: Property; + }; +} + +export interface ReactDRODestructor { + kind: 'ReactDRO'; + dro_type: + | 'HookReturn' + | 'HookArg' + | 'Props' + | 'ImmutableAnnot' + | 'DebugAnnot'; +} + +export interface MakeHooklikeDestructor { + kind: 'MakeHooklike'; +} + +export interface MappedTypeDestructor { + kind: 'MappedType'; + homomorphic: Homomorphic; + distributive_tparam_name: string | null; + property_type: FlowType; + mapped_type_flags: { + variance: Polarity; + optional: 'MakeOptional' | 'RemoveOptional' | 'KeepOptionality'; + }; +} + +export type Homomorphic = + | {kind: 'Homomorphic'} + | {kind: 'Unspecialized'} + | {kind: 'SemiHomomorphic'; type: FlowType}; + +export interface EnumTypeDestructor { + kind: 'EnumType'; +} + +// Union of all possible Flow types +export type FlowType = + | OpenType + | DefType + | EvalType + | GenericType + | ThisInstanceType + | ThisTypeAppType + | TypeAppType + | FunProtoType + | ObjProtoType + | NullProtoType + | FunProtoBindType + | IntersectionType + | UnionType + | MaybeType + | OptionalType + | KeysType + | AnnotType + | OpaqueType + | NamespaceType + | AnyType + | StrUtilType; diff --git a/compiler/packages/babel-plugin-react-compiler/src/Flood/TypeErrors.ts b/compiler/packages/babel-plugin-react-compiler/src/Flood/TypeErrors.ts new file mode 100644 index 0000000000000..fa3f551ff5fc3 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/Flood/TypeErrors.ts @@ -0,0 +1,131 @@ +import {CompilerError, SourceLocation} from '..'; +import { + ConcreteType, + printConcrete, + printType, + StructuralValue, + Type, + VariableId, +} from './Types'; + +export function unsupportedLanguageFeature( + desc: string, + loc: SourceLocation, +): never { + CompilerError.throwInvalidJS({ + reason: `Typedchecker does not currently support language feature: ${desc}`, + loc, + }); +} + +export type UnificationError = + | { + kind: 'TypeUnification'; + left: ConcreteType; + right: ConcreteType; + } + | { + kind: 'StructuralUnification'; + left: StructuralValue; + right: ConcreteType; + }; + +function printUnificationError(err: UnificationError): string { + if (err.kind === 'TypeUnification') { + return `${printConcrete(err.left, printType)} is incompatible with ${printConcrete(err.right, printType)}`; + } else { + return `structural ${err.left.kind} is incompatible with ${printConcrete(err.right, printType)}`; + } +} + +export function raiseUnificationErrors( + errs: null | Array, + loc: SourceLocation, +): void { + if (errs != null) { + if (errs.length === 0) { + CompilerError.invariant(false, { + reason: 'Should not have array of zero errors', + loc, + }); + } else if (errs.length === 1) { + CompilerError.throwInvalidJS({ + reason: `Unable to unify types because ${printUnificationError(errs[0])}`, + loc, + }); + } else { + const messages = errs + .map(err => `\t* ${printUnificationError(err)}`) + .join('\n'); + CompilerError.throwInvalidJS({ + reason: `Unable to unify types because:\n${messages}`, + loc, + }); + } + } +} + +export function unresolvableTypeVariable( + id: VariableId, + loc: SourceLocation, +): never { + CompilerError.throwInvalidJS({ + reason: `Unable to resolve free variable ${id} to a concrete type`, + loc, + }); +} + +export function cannotAddVoid(explicit: boolean, loc: SourceLocation): never { + if (explicit) { + CompilerError.throwInvalidJS({ + reason: `Undefined is not a valid operand of \`+\``, + loc, + }); + } else { + CompilerError.throwInvalidJS({ + reason: `Value may be undefined, which is not a valid operand of \`+\``, + loc, + }); + } +} + +export function unsupportedTypeAnnotation( + desc: string, + loc: SourceLocation, +): never { + CompilerError.throwInvalidJS({ + reason: `Typedchecker does not currently support type annotation: ${desc}`, + loc, + }); +} + +export function checkTypeArgumentArity( + desc: string, + expected: number, + actual: number, + loc: SourceLocation, +): void { + if (expected !== actual) { + CompilerError.throwInvalidJS({ + reason: `Expected ${desc} to have ${expected} type parameters, got ${actual}`, + loc, + }); + } +} + +export function notAFunction(desc: string, loc: SourceLocation): void { + CompilerError.throwInvalidJS({ + reason: `Cannot call ${desc} because it is not a function`, + loc, + }); +} + +export function notAPolymorphicFunction( + desc: string, + loc: SourceLocation, +): void { + CompilerError.throwInvalidJS({ + reason: `Cannot call ${desc} with type arguments because it is not a polymorphic function`, + loc, + }); +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/Flood/TypeUtils.ts b/compiler/packages/babel-plugin-react-compiler/src/Flood/TypeUtils.ts new file mode 100644 index 0000000000000..0a514f090d2a2 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/Flood/TypeUtils.ts @@ -0,0 +1,312 @@ +import {GeneratedSource} from '../HIR'; +import {assertExhaustive} from '../Utils/utils'; +import {unsupportedLanguageFeature} from './TypeErrors'; +import { + ConcreteType, + ResolvedType, + TypeParameter, + TypeParameterId, + DEBUG, + printConcrete, + printType, +} from './Types'; + +export function substitute( + type: ConcreteType, + typeParameters: Array>, + typeArguments: Array, +): ResolvedType { + const substMap = new Map(); + for (let i = 0; i < typeParameters.length; i++) { + // TODO: Length checks to make sure type params match up with args + const typeParameter = typeParameters[i]; + const typeArgument = typeArguments[i]; + substMap.set(typeParameter.id, typeArgument); + } + const substitutionFunction = (t: ResolvedType): ResolvedType => { + // TODO: We really want a stateful mapper or visitor here so that we can model nested polymorphic types + if (t.type.kind === 'Generic' && substMap.has(t.type.id)) { + const substitutedType = substMap.get(t.type.id)!; + return substitutedType; + } + + return { + kind: 'Concrete', + type: mapType(substitutionFunction, t.type), + platform: t.platform, + }; + }; + + const substituted = mapType(substitutionFunction, type); + + if (DEBUG) { + let substs = ''; + for (let i = 0; i < typeParameters.length; i++) { + const typeParameter = typeParameters[i]; + const typeArgument = typeArguments[i]; + substs += `[${typeParameter.name}${typeParameter.id} := ${printType(typeArgument)}]`; + } + console.log( + `${printConcrete(type, printType)}${substs} = ${printConcrete(substituted, printType)}`, + ); + } + + return {kind: 'Concrete', type: substituted, platform: /* TODO */ 'shared'}; +} + +export function mapType( + f: (t: T) => U, + type: ConcreteType, +): ConcreteType { + switch (type.kind) { + case 'Mixed': + case 'Number': + case 'String': + case 'Boolean': + case 'Void': + return type; + + case 'Nullable': + return { + kind: 'Nullable', + type: f(type.type), + }; + + case 'Array': + return { + kind: 'Array', + element: f(type.element), + }; + + case 'Set': + return { + kind: 'Set', + element: f(type.element), + }; + + case 'Map': + return { + kind: 'Map', + key: f(type.key), + value: f(type.value), + }; + + case 'Function': + return { + kind: 'Function', + typeParameters: + type.typeParameters?.map(param => ({ + id: param.id, + name: param.name, + bound: f(param.bound), + })) ?? null, + params: type.params.map(f), + returnType: f(type.returnType), + }; + + case 'Component': { + return { + kind: 'Component', + children: type.children != null ? f(type.children) : null, + props: new Map([...type.props.entries()].map(([k, v]) => [k, f(v)])), + }; + } + + case 'Generic': + return { + kind: 'Generic', + id: type.id, + bound: f(type.bound), + }; + + case 'Object': + return type; + + case 'Tuple': + return { + kind: 'Tuple', + id: type.id, + members: type.members.map(f), + }; + + case 'Structural': + return type; + + case 'Enum': + case 'Union': + case 'Instance': + unsupportedLanguageFeature(type.kind, GeneratedSource); + + default: + assertExhaustive(type, 'Unknown type kind'); + } +} + +export function diff( + a: ConcreteType, + b: ConcreteType, + onChild: (a: T, b: T) => R, + onChildMismatch: (child: R, cur: R) => R, + onMismatch: (a: ConcreteType, b: ConcreteType, cur: R) => R, + init: R, +): R { + let errors = init; + + // Check if kinds match + if (a.kind !== b.kind) { + errors = onMismatch(a, b, errors); + return errors; + } + + // Based on kind, check other properties + switch (a.kind) { + case 'Mixed': + case 'Number': + case 'String': + case 'Boolean': + case 'Void': + // Simple types, no further checks needed + break; + + case 'Nullable': + // Check the nested type + errors = onChildMismatch(onChild(a.type, (b as typeof a).type), errors); + break; + + case 'Array': + case 'Set': + // Check the element type + errors = onChildMismatch( + onChild(a.element, (b as typeof a).element), + errors, + ); + break; + + case 'Map': + // Check both key and value types + errors = onChildMismatch(onChild(a.key, (b as typeof a).key), errors); + errors = onChildMismatch(onChild(a.value, (b as typeof a).value), errors); + break; + + case 'Function': { + const bFunc = b as typeof a; + + // Check type parameters + if ((a.typeParameters == null) !== (bFunc.typeParameters == null)) { + errors = onMismatch(a, b, errors); + } + + if (a.typeParameters != null && bFunc.typeParameters != null) { + if (a.typeParameters.length !== bFunc.typeParameters.length) { + errors = onMismatch(a, b, errors); + } + + // Type parameters are just numbers, so we can compare them directly + for (let i = 0; i < a.typeParameters.length; i++) { + if (a.typeParameters[i] !== bFunc.typeParameters[i]) { + errors = onMismatch(a, b, errors); + } + } + } + + // Check parameters + if (a.params.length !== bFunc.params.length) { + errors = onMismatch(a, b, errors); + } + + for (let i = 0; i < a.params.length; i++) { + errors = onChildMismatch(onChild(a.params[i], bFunc.params[i]), errors); + } + + // Check return type + errors = onChildMismatch(onChild(a.returnType, bFunc.returnType), errors); + break; + } + + case 'Component': { + const bComp = b as typeof a; + + // Check children + if (a.children !== bComp.children) { + errors = onMismatch(a, b, errors); + } + + // Check props + if (a.props.size !== bComp.props.size) { + errors = onMismatch(a, b, errors); + } + + for (const [k, v] of a.props) { + const bProp = bComp.props.get(k); + if (bProp == null) { + errors = onMismatch(a, b, errors); + } else { + errors = onChildMismatch(onChild(v, bProp), errors); + } + } + + break; + } + + case 'Generic': { + // Check that the type parameter IDs match + if (a.id !== (b as typeof a).id) { + errors = onMismatch(a, b, errors); + } + break; + } + case 'Structural': { + const bStruct = b as typeof a; + + // Check that the structural IDs match + if (a.id !== bStruct.id) { + errors = onMismatch(a, b, errors); + } + break; + } + case 'Object': { + const bNom = b as typeof a; + + // Check that the nominal IDs match + if (a.id !== bNom.id) { + errors = onMismatch(a, b, errors); + } + break; + } + + case 'Tuple': { + const bTuple = b as typeof a; + + // Check that the tuple IDs match + if (a.id !== bTuple.id) { + errors = onMismatch(a, b, errors); + } + for (let i = 0; i < a.members.length; i++) { + errors = onChildMismatch( + onChild(a.members[i], bTuple.members[i]), + errors, + ); + } + + break; + } + + case 'Enum': + case 'Instance': + case 'Union': { + unsupportedLanguageFeature(a.kind, GeneratedSource); + } + + default: + assertExhaustive(a, 'Unknown type kind'); + } + + return errors; +} + +export function filterOptional(t: ResolvedType): ResolvedType { + if (t.kind === 'Concrete' && t.type.kind === 'Nullable') { + return t.type.type; + } + return t; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/Flood/Types.ts b/compiler/packages/babel-plugin-react-compiler/src/Flood/Types.ts new file mode 100644 index 0000000000000..21391b197b798 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/Flood/Types.ts @@ -0,0 +1,1000 @@ +import {CompilerError, SourceLocation} from '..'; +import { + Environment, + GeneratedSource, + HIRFunction, + Identifier, + IdentifierId, +} from '../HIR'; +import * as t from '@babel/types'; +import * as TypeErrors from './TypeErrors'; +import {assertExhaustive} from '../Utils/utils'; +import {FlowType} from './FlowTypes'; + +export const DEBUG = false; + +export type Type = + | {kind: 'Concrete'; type: ConcreteType; platform: Platform} + | {kind: 'Variable'; id: VariableId}; + +export type ResolvedType = { + kind: 'Concrete'; + type: ConcreteType; + platform: Platform; +}; + +export type ComponentType = { + kind: 'Component'; + props: Map; + children: null | T; +}; +export type ConcreteType = + | {kind: 'Enum'} + | {kind: 'Mixed'} + | {kind: 'Number'} + | {kind: 'String'} + | {kind: 'Boolean'} + | {kind: 'Void'} + | {kind: 'Nullable'; type: T} + | {kind: 'Array'; element: T} + | {kind: 'Set'; element: T} + | {kind: 'Map'; key: T; value: T} + | { + kind: 'Function'; + typeParameters: null | Array>; + params: Array; + returnType: T; + } + | ComponentType + | {kind: 'Generic'; id: TypeParameterId; bound: T} + | { + kind: 'Object'; + id: NominalId; + members: Map; + } + | { + kind: 'Tuple'; + id: NominalId; + members: Array; + } + | {kind: 'Structural'; id: LinearId} + | {kind: 'Union'; members: Array} + | {kind: 'Instance'; name: string; members: Map}; + +export type StructuralValue = + | { + kind: 'Function'; + fn: HIRFunction; + } + | { + kind: 'Object'; + members: Map; + } + | { + kind: 'Array'; + elementType: ResolvedType; + }; + +export type Structural = { + type: StructuralValue; + consumed: boolean; +}; +// TODO: create a kind: "Alias" + +// type T = { foo: X} + +/** + * + * function apply(x: A, f: A => B): B { } + * + * apply(42, x => String(x)); + * + * f({foo: 42}) + * + * f([HOLE]) -----> {foo: 42} with context NominalType + * + * $0 = Object {foo: 42} + * $1 = LoadLocal "f" + * $2 = Call $1, [$0] + * + * ContextMap: + * $2 => ?? + * $1 => [HOLE]($0) + * $0 => $1([HOLE]) + */ + +/* + *const g = {foo: 42} as NominalType // ok + * + * + *function f(x: NominalType) { ... } + *f() + * + *const y: NominalType = {foo: 42} + * + * + */ + +/** + * // Mike: maybe this could be the ideal? + *type X = nominal('registryNameX', { + *value: number, + *}); + * + * // For now: + *opaque type X = { // creates a new nominal type + *value: number, + *}; + * + *type Y = X; // creates a type alias + * + *type Z = number; // creates a type alias + * + * + * // (todo: disallowed) + *type X' = { + *value: number, + *} + */ + +export type TypeParameter = { + name: string; + id: TypeParameterId; + bound: T; +}; + +const opaqueLinearId = Symbol(); +export type LinearId = number & { + [opaqueLinearId]: 'LinearId'; +}; + +export function makeLinearId(id: number): LinearId { + CompilerError.invariant(id >= 0 && Number.isInteger(id), { + reason: 'Expected LinearId id to be a non-negative integer', + description: null, + loc: null, + suggestions: null, + }); + return id as LinearId; +} + +const opaqueTypeParameterId = Symbol(); +export type TypeParameterId = number & { + [opaqueTypeParameterId]: 'TypeParameterId'; +}; + +export function makeTypeParameterId(id: number): TypeParameterId { + CompilerError.invariant(id >= 0 && Number.isInteger(id), { + reason: 'Expected TypeParameterId to be a non-negative integer', + description: null, + loc: null, + suggestions: null, + }); + return id as TypeParameterId; +} + +const opaqueNominalId = Symbol(); +export type NominalId = number & { + [opaqueNominalId]: 'NominalId'; +}; + +export function makeNominalId(id: number): NominalId { + return id as NominalId; +} + +const opaqueVariableId = Symbol(); +export type VariableId = number & { + [opaqueVariableId]: 'VariableId'; +}; + +export function makeVariableId(id: number): VariableId { + CompilerError.invariant(id >= 0 && Number.isInteger(id), { + reason: 'Expected VariableId id to be a non-negative integer', + description: null, + loc: null, + suggestions: null, + }); + return id as VariableId; +} + +export function printConcrete( + type: ConcreteType, + printType: (_: T) => string, +): string { + switch (type.kind) { + case 'Mixed': + return 'mixed'; + case 'Number': + return 'number'; + case 'String': + return 'string'; + case 'Boolean': + return 'boolean'; + case 'Void': + return 'void'; + case 'Nullable': + return `${printType(type.type)} | void`; + case 'Array': + return `Array<${printType(type.element)}>`; + case 'Set': + return `Set<${printType(type.element)}>`; + case 'Map': + return `Map<${printType(type.key)}, ${printType(type.value)}>`; + case 'Function': { + const typeParams = type.typeParameters + ? `<${type.typeParameters.map(tp => `T${tp}`).join(', ')}>` + : ''; + const params = type.params.map(printType).join(', '); + const returnType = printType(type.returnType); + return `${typeParams}(${params}) => ${returnType}`; + } + case 'Component': { + const params = [...type.props.entries()] + .map(([k, v]) => `${k}: ${printType(v)}`) + .join(', '); + const comma = type.children != null && type.props.size > 0 ? ', ' : ''; + const children = + type.children != null ? `children: ${printType(type.children)}` : ''; + return `component (${params}${comma}${children})`; + } + case 'Generic': + return `T${type.id}`; + case 'Object': { + const name = `Object [${[...type.members.keys()].map(key => JSON.stringify(key)).join(', ')}]`; + return `${name}`; + } + case 'Tuple': { + const name = `Tuple ${type.members}`; + return `${name}`; + } + case 'Structural': { + const name = `Structural ${type.id}`; + return `${name}`; + } + case 'Enum': { + return 'TODO enum printing'; + } + case 'Union': { + return type.members.map(printType).join(' | '); + } + case 'Instance': { + return type.name; + } + default: + assertExhaustive(type, `Unknown type: ${JSON.stringify(type)}`); + } +} + +export function printType(type: Type): string { + switch (type.kind) { + case 'Concrete': + return printConcrete(type.type, printType); + case 'Variable': + return `$${type.id}`; + default: + assertExhaustive(type, `Unknown type: ${JSON.stringify(type)}`); + } +} + +export function printResolved(type: ResolvedType): string { + return printConcrete(type.type, printResolved); +} + +type Platform = 'client' | 'server' | 'shared'; + +const DUMMY_NOMINAL = makeNominalId(0); + +function convertFlowType(flowType: FlowType, loc: string): ResolvedType { + let nextGenericId = 0; + function convertFlowTypeImpl( + flowType: FlowType, + loc: string, + genericEnv: Map, + platform: Platform, + poly: null | Array> = null, + ): ResolvedType { + switch (flowType.kind) { + case 'TypeApp': { + if ( + flowType.type.kind === 'Def' && + flowType.type.def.kind === 'Poly' && + flowType.type.def.t_out.kind === 'Def' && + flowType.type.def.t_out.def.kind === 'Type' && + flowType.type.def.t_out.def.type.kind === 'Opaque' && + flowType.type.def.t_out.def.type.opaquetype.opaque_name === + 'Client' && + flowType.targs.length === 1 + ) { + return convertFlowTypeImpl( + flowType.targs[0], + loc, + genericEnv, + 'client', + ); + } else if ( + flowType.type.kind === 'Def' && + flowType.type.def.kind === 'Poly' && + flowType.type.def.t_out.kind === 'Def' && + flowType.type.def.t_out.def.kind === 'Type' && + flowType.type.def.t_out.def.type.kind === 'Opaque' && + flowType.type.def.t_out.def.type.opaquetype.opaque_name === + 'Server' && + flowType.targs.length === 1 + ) { + return convertFlowTypeImpl( + flowType.targs[0], + loc, + genericEnv, + 'server', + ); + } + return Resolved.todo(platform); + } + case 'Open': + return Resolved.mixed(platform); + case 'Any': + return Resolved.todo(platform); + case 'Annot': + return convertFlowTypeImpl( + flowType.type, + loc, + genericEnv, + platform, + poly, + ); + case 'Opaque': { + if ( + flowType.opaquetype.opaque_name === 'Client' && + flowType.opaquetype.super_t != null + ) { + return convertFlowTypeImpl( + flowType.opaquetype.super_t, + loc, + genericEnv, + 'client', + ); + } + if ( + flowType.opaquetype.opaque_name === 'Server' && + flowType.opaquetype.super_t != null + ) { + return convertFlowTypeImpl( + flowType.opaquetype.super_t, + loc, + genericEnv, + 'server', + ); + } + const t = + flowType.opaquetype.underlying_t ?? flowType.opaquetype.super_t; + if (t != null) { + return convertFlowTypeImpl(t, loc, genericEnv, platform, poly); + } else { + return Resolved.todo(platform); + } + } + case 'Def': { + switch (flowType.def.kind) { + case 'EnumValue': + return convertFlowTypeImpl( + flowType.def.enum_info.representation_t, + loc, + genericEnv, + platform, + poly, + ); + case 'EnumObject': + return Resolved.enum(platform); + case 'Empty': + return Resolved.todo(platform); + case 'Instance': { + const members = new Map(); + for (const key in flowType.def.instance.inst.own_props) { + const prop = flowType.def.instance.inst.own_props[key]; + if (prop.kind === 'Field') { + members.set( + key, + convertFlowTypeImpl(prop.type, loc, genericEnv, platform), + ); + } else { + CompilerError.invariant(false, { + reason: `Unsupported property kind ${prop.kind}`, + loc: GeneratedSource, + }); + } + } + return Resolved.class( + flowType.def.instance.inst.class_name ?? '[anonymous class]', + members, + platform, + ); + } + case 'Type': + return convertFlowTypeImpl( + flowType.def.type, + loc, + genericEnv, + platform, + poly, + ); + case 'NumGeneral': + case 'SingletonNum': + return Resolved.number(platform); + case 'StrGeneral': + case 'SingletonStr': + return Resolved.string(platform); + case 'BoolGeneral': + case 'SingletonBool': + return Resolved.boolean(platform); + case 'Void': + return Resolved.void(platform); + case 'Null': + return Resolved.void(platform); + case 'Mixed': + return Resolved.mixed(platform); + case 'Arr': { + if ( + flowType.def.arrtype.kind === 'ArrayAT' || + flowType.def.arrtype.kind === 'ROArrayAT' + ) { + return Resolved.array( + convertFlowTypeImpl( + flowType.def.arrtype.elem_t, + loc, + genericEnv, + platform, + ), + platform, + ); + } else { + return Resolved.tuple( + DUMMY_NOMINAL, + flowType.def.arrtype.elements.map(t => + convertFlowTypeImpl(t.t, loc, genericEnv, platform), + ), + platform, + ); + } + } + case 'Obj': { + const members = new Map(); + for (const key in flowType.def.objtype.props) { + const prop = flowType.def.objtype.props[key]; + if (prop.kind === 'Field') { + members.set( + key, + convertFlowTypeImpl(prop.type, loc, genericEnv, platform), + ); + } else { + CompilerError.invariant(false, { + reason: `Unsupported property kind ${prop.kind}`, + loc: GeneratedSource, + }); + } + } + return Resolved.object(DUMMY_NOMINAL, members, platform); + } + case 'Class': { + if (flowType.def.type.kind === 'ThisInstance') { + const members = new Map(); + for (const key in flowType.def.type.instance.inst.own_props) { + const prop = flowType.def.type.instance.inst.own_props[key]; + if (prop.kind === 'Field') { + members.set( + key, + convertFlowTypeImpl(prop.type, loc, genericEnv, platform), + ); + } else { + CompilerError.invariant(false, { + reason: `Unsupported property kind ${prop.kind}`, + loc: GeneratedSource, + }); + } + } + return Resolved.class( + flowType.def.type.instance.inst.class_name ?? + '[anonymous class]', + members, + platform, + ); + } + CompilerError.invariant(false, { + reason: `Unsupported class instance type ${flowType.def.type.kind}`, + loc: GeneratedSource, + }); + } + case 'Fun': + return Resolved.function( + poly, + flowType.def.funtype.params.map(p => + convertFlowTypeImpl(p.type, loc, genericEnv, platform), + ), + convertFlowTypeImpl( + flowType.def.funtype.return_t, + loc, + genericEnv, + platform, + ), + platform, + ); + case 'Poly': { + let newEnv = genericEnv; + const poly = flowType.def.tparams.map(p => { + const id = makeTypeParameterId(nextGenericId++); + const bound = convertFlowTypeImpl(p.bound, loc, newEnv, platform); + newEnv = new Map(newEnv); + newEnv.set(p.name, id); + return { + name: p.name, + id, + bound, + }; + }); + return convertFlowTypeImpl( + flowType.def.t_out, + loc, + newEnv, + platform, + poly, + ); + } + case 'ReactAbstractComponent': { + const props = new Map(); + let children: ResolvedType | null = null; + const propsType = convertFlowTypeImpl( + flowType.def.config, + loc, + genericEnv, + platform, + ); + + if (propsType.type.kind === 'Object') { + propsType.type.members.forEach((v, k) => { + if (k === 'children') { + children = v; + } else { + props.set(k, v); + } + }); + } else { + CompilerError.invariant(false, { + reason: `Unsupported component props type ${propsType.type.kind}`, + loc: GeneratedSource, + }); + } + + return Resolved.component(props, children, platform); + } + case 'Renders': + return Resolved.todo(platform); + default: + TypeErrors.unsupportedTypeAnnotation('Renders', GeneratedSource); + } + } + case 'Generic': { + const id = genericEnv.get(flowType.name); + if (id == null) { + TypeErrors.unsupportedTypeAnnotation(flowType.name, GeneratedSource); + } + return Resolved.generic( + id, + platform, + convertFlowTypeImpl(flowType.bound, loc, genericEnv, platform), + ); + } + case 'Union': { + const members = flowType.members.map(t => + convertFlowTypeImpl(t, loc, genericEnv, platform), + ); + if (members.length === 1) { + return members[0]; + } + if ( + members[0].type.kind === 'Number' || + members[0].type.kind === 'String' || + members[0].type.kind === 'Boolean' + ) { + const dupes = members.filter( + t => t.type.kind === members[0].type.kind, + ); + if (dupes.length === members.length) { + return members[0]; + } + } + if ( + members[0].type.kind === 'Array' && + (members[0].type.element.type.kind === 'Number' || + members[0].type.element.type.kind === 'String' || + members[0].type.element.type.kind === 'Boolean') + ) { + const first = members[0].type.element; + const dupes = members.filter( + t => + t.type.kind === 'Array' && + t.type.element.type.kind === first.type.kind, + ); + if (dupes.length === members.length) { + return members[0]; + } + } + return Resolved.union(members, platform); + } + case 'Eval': { + if ( + flowType.destructor.kind === 'ReactDRO' || + flowType.destructor.kind === 'ReactCheckComponentConfig' + ) { + return convertFlowTypeImpl( + flowType.type, + loc, + genericEnv, + platform, + poly, + ); + } + TypeErrors.unsupportedTypeAnnotation( + `EvalT(${flowType.destructor.kind})`, + GeneratedSource, + ); + } + case 'Optional': { + return Resolved.union( + [ + convertFlowTypeImpl(flowType.type, loc, genericEnv, platform), + Resolved.void(platform), + ], + platform, + ); + } + default: + TypeErrors.unsupportedTypeAnnotation(flowType.kind, GeneratedSource); + } + } + return convertFlowTypeImpl(flowType, loc, new Map(), 'shared'); +} + +export interface ITypeEnv { + popGeneric(name: string): void; + getGeneric(name: string): null | TypeParameter; + pushGeneric( + name: string, + binding: {name: string; id: TypeParameterId; bound: ResolvedType}, + ): void; + getType(id: Identifier): ResolvedType; + getTypeOrNull(id: Identifier): ResolvedType | null; + setType(id: Identifier, type: ResolvedType): void; + nextNominalId(): NominalId; + nextTypeParameterId(): TypeParameterId; + moduleEnv: Map; + addBinding(bindingIdentifier: t.Identifier, type: ResolvedType): void; + resolveBinding(bindingIdentifier: t.Identifier): ResolvedType | null; +} + +function serializeLoc(location: t.SourceLocation): string { + return `${location.start.line}:${location.start.column}-${location.end.line}:${location.end.column}`; +} + +function buildTypeEnvironment( + flowOutput: Array<{loc: t.SourceLocation; type: string}>, +): Map { + const result: Map = new Map(); + for (const item of flowOutput) { + const loc: t.SourceLocation = { + start: { + line: item.loc.start.line, + column: item.loc.start.column - 1, + index: item.loc.start.index, + }, + end: item.loc.end, + filename: item.loc.filename, + identifierName: item.loc.identifierName, + }; + + result.set(serializeLoc(loc), item.type); + } + return result; +} + +let lastFlowSource: string | null = null; +let lastFlowResult: any = null; + +export class FlowTypeEnv implements ITypeEnv { + moduleEnv: Map = new Map(); + #nextNominalId: number = 0; + #nextTypeParameterId: number = 0; + + #types: Map = new Map(); + #bindings: Map = new Map(); + #generics: Array<[string, TypeParameter]> = []; + #flowTypes: Map = new Map(); + + init(env: Environment, source: string): void { + // TODO: use flow-js only for web environments (e.g. playground) + CompilerError.invariant(env.config.flowTypeProvider != null, { + reason: 'Expected flowDumpTypes to be defined in environment config', + loc: GeneratedSource, + }); + let stdout: any; + if (source === lastFlowSource) { + stdout = lastFlowResult; + } else { + lastFlowSource = source; + lastFlowResult = env.config.flowTypeProvider(source); + stdout = lastFlowResult; + } + const flowTypes = buildTypeEnvironment(stdout); + const resolvedFlowTypes = new Map(); + for (const [loc, type] of flowTypes) { + if (typeof loc === 'symbol') continue; + resolvedFlowTypes.set(loc, convertFlowType(JSON.parse(type), loc)); + } + // =console.log(resolvedFlowTypes); + this.#flowTypes = resolvedFlowTypes; + } + + setType(identifier: Identifier, type: ResolvedType): void { + if ( + typeof identifier.loc !== 'symbol' && + this.#flowTypes.has(serializeLoc(identifier.loc)) + ) { + return; + } + this.#types.set(identifier.id, type); + } + + getType(identifier: Identifier): ResolvedType { + const result = this.getTypeOrNull(identifier); + if (result == null) { + throw new Error( + `Type not found for ${identifier.id}, ${typeof identifier.loc === 'symbol' ? 'generated loc' : serializeLoc(identifier.loc)}`, + ); + } + return result; + } + + getTypeOrNull(identifier: Identifier): ResolvedType | null { + const result = this.#types.get(identifier.id) ?? null; + if (result == null && typeof identifier.loc !== 'symbol') { + const flowType = this.#flowTypes.get(serializeLoc(identifier.loc)); + return flowType ?? null; + } + return result; + } + + getTypeByLoc(loc: SourceLocation): ResolvedType | null { + if (typeof loc === 'symbol') { + return null; + } + const flowType = this.#flowTypes.get(serializeLoc(loc)); + return flowType ?? null; + } + + nextNominalId(): NominalId { + return makeNominalId(this.#nextNominalId++); + } + + nextTypeParameterId(): TypeParameterId { + return makeTypeParameterId(this.#nextTypeParameterId++); + } + + addBinding(bindingIdentifier: t.Identifier, type: ResolvedType): void { + this.#bindings.set(bindingIdentifier, type); + } + + resolveBinding(bindingIdentifier: t.Identifier): ResolvedType | null { + return this.#bindings.get(bindingIdentifier) ?? null; + } + + pushGeneric(name: string, generic: TypeParameter): void { + this.#generics.unshift([name, generic]); + } + + popGeneric(name: string): void { + for (let i = 0; i < this.#generics.length; i++) { + if (this.#generics[i][0] === name) { + this.#generics.splice(i, 1); + return; + } + } + } + + /** + * Look up bound polymorphic types + * @param name + * @returns + */ + getGeneric(name: string): null | TypeParameter { + for (const [eltName, param] of this.#generics) { + if (name === eltName) { + return param; + } + } + return null; + } +} +const Primitives = { + number(platform: Platform): Type & ResolvedType { + return {kind: 'Concrete', type: {kind: 'Number'}, platform}; + }, + string(platform: Platform): Type & ResolvedType { + return {kind: 'Concrete', type: {kind: 'String'}, platform}; + }, + boolean(platform: Platform): Type & ResolvedType { + return {kind: 'Concrete', type: {kind: 'Boolean'}, platform}; + }, + void(platform: Platform): Type & ResolvedType { + return {kind: 'Concrete', type: {kind: 'Void'}, platform}; + }, + mixed(platform: Platform): Type & ResolvedType { + return {kind: 'Concrete', type: {kind: 'Mixed'}, platform}; + }, + enum(platform: Platform): Type & ResolvedType { + return {kind: 'Concrete', type: {kind: 'Enum'}, platform}; + }, + todo(platform: Platform): Type & ResolvedType { + return {kind: 'Concrete', type: {kind: 'Mixed'}, platform}; + }, +}; + +export const Resolved = { + ...Primitives, + nullable(type: ResolvedType, platform: Platform): ResolvedType { + return {kind: 'Concrete', type: {kind: 'Nullable', type}, platform}; + }, + array(element: ResolvedType, platform: Platform): ResolvedType { + return {kind: 'Concrete', type: {kind: 'Array', element}, platform}; + }, + set(element: ResolvedType, platform: Platform): ResolvedType { + return {kind: 'Concrete', type: {kind: 'Set', element}, platform}; + }, + map( + key: ResolvedType, + value: ResolvedType, + platform: Platform, + ): ResolvedType { + return {kind: 'Concrete', type: {kind: 'Map', key, value}, platform}; + }, + function( + typeParameters: null | Array>, + params: Array, + returnType: ResolvedType, + platform: Platform, + ): ResolvedType { + return { + kind: 'Concrete', + type: {kind: 'Function', typeParameters, params, returnType}, + platform, + }; + }, + component( + props: Map, + children: ResolvedType | null, + platform: Platform, + ): ResolvedType { + return { + kind: 'Concrete', + type: {kind: 'Component', props, children}, + platform, + }; + }, + object( + id: NominalId, + members: Map, + platform: Platform, + ): ResolvedType { + return { + kind: 'Concrete', + type: { + kind: 'Object', + id, + members, + }, + platform, + }; + }, + class( + name: string, + members: Map, + platform: Platform, + ): ResolvedType { + return { + kind: 'Concrete', + type: { + kind: 'Instance', + name, + members, + }, + platform, + }; + }, + tuple( + id: NominalId, + members: Array, + platform: Platform, + ): ResolvedType { + return { + kind: 'Concrete', + type: { + kind: 'Tuple', + id, + members, + }, + platform, + }; + }, + generic( + id: TypeParameterId, + platform: Platform, + bound = Primitives.mixed(platform), + ): ResolvedType { + return { + kind: 'Concrete', + type: { + kind: 'Generic', + id, + bound, + }, + platform, + }; + }, + union(members: Array, platform: Platform): ResolvedType { + return { + kind: 'Concrete', + type: { + kind: 'Union', + members, + }, + platform, + }; + }, +}; + +/* + * export const Types = { + * ...Primitives, + * variable(env: TypeEnv): Type { + * return env.nextTypeVariable(); + * }, + * nullable(type: Type): Type { + * return {kind: 'Concrete', type: {kind: 'Nullable', type}}; + * }, + * array(element: Type): Type { + * return {kind: 'Concrete', type: {kind: 'Array', element}}; + * }, + * set(element: Type): Type { + * return {kind: 'Concrete', type: {kind: 'Set', element}}; + * }, + * map(key: Type, value: Type): Type { + * return {kind: 'Concrete', type: {kind: 'Map', key, value}}; + * }, + * function( + * typeParameters: null | Array>, + * params: Array, + * returnType: Type, + * ): Type { + * return { + * kind: 'Concrete', + * type: {kind: 'Function', typeParameters, params, returnType}, + * }; + * }, + * component( + * props: Map, + * children: Type | null, + * ): Type { + * return { + * kind: 'Concrete', + * type: {kind: 'Component', props, children}, + * }; + * }, + * object(id: NominalId, members: Map): Type { + * return { + * kind: 'Concrete', + * type: { + * kind: 'Object', + * id, + * members, + * }, + * }; + * }, + * }; + */ diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/AssertValidMutableRanges.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/AssertValidMutableRanges.ts index d44f6108eaa57..773986a1b5e77 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/AssertValidMutableRanges.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/AssertValidMutableRanges.ts @@ -5,13 +5,14 @@ * LICENSE file in the root directory of this source tree. */ -import invariant from 'invariant'; -import {HIRFunction, Identifier, MutableRange} from './HIR'; +import {HIRFunction, MutableRange, Place} from './HIR'; import { eachInstructionLValue, eachInstructionOperand, eachTerminalOperand, } from './visitors'; +import {CompilerError} from '..'; +import {printPlace} from './PrintHIR'; /* * Checks that all mutable ranges in the function are well-formed, with @@ -20,38 +21,43 @@ import { export function assertValidMutableRanges(fn: HIRFunction): void { for (const [, block] of fn.body.blocks) { for (const phi of block.phis) { - visitIdentifier(phi.place.identifier); - for (const [, operand] of phi.operands) { - visitIdentifier(operand.identifier); + visit(phi.place, `phi for block bb${block.id}`); + for (const [pred, operand] of phi.operands) { + visit(operand, `phi predecessor bb${pred} for block bb${block.id}`); } } for (const instr of block.instructions) { for (const operand of eachInstructionLValue(instr)) { - visitIdentifier(operand.identifier); + visit(operand, `instruction [${instr.id}]`); } for (const operand of eachInstructionOperand(instr)) { - visitIdentifier(operand.identifier); + visit(operand, `instruction [${instr.id}]`); } } for (const operand of eachTerminalOperand(block.terminal)) { - visitIdentifier(operand.identifier); + visit(operand, `terminal [${block.terminal.id}]`); } } } -function visitIdentifier(identifier: Identifier): void { - validateMutableRange(identifier.mutableRange); - if (identifier.scope !== null) { - validateMutableRange(identifier.scope.range); +function visit(place: Place, description: string): void { + validateMutableRange(place, place.identifier.mutableRange, description); + if (place.identifier.scope !== null) { + validateMutableRange(place, place.identifier.scope.range, description); } } -function validateMutableRange(mutableRange: MutableRange): void { - invariant( - (mutableRange.start === 0 && mutableRange.end === 0) || - mutableRange.end > mutableRange.start, - 'Identifier scope mutableRange was invalid: [%s:%s]', - mutableRange.start, - mutableRange.end, +function validateMutableRange( + place: Place, + range: MutableRange, + description: string, +): void { + CompilerError.invariant( + (range.start === 0 && range.end === 0) || range.end > range.start, + { + reason: `Invalid mutable range: [${range.start}:${range.end}]`, + description: `${printPlace(place)} in ${description}`, + loc: place.loc, + }, ); } 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 b9f82eea18e9f..77f2a04e7cf67 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/BuildHIR.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/BuildHIR.ts @@ -9,8 +9,10 @@ import {NodePath, Scope} from '@babel/traverse'; import * as t from '@babel/types'; import invariant from 'invariant'; import { + CompilerDiagnostic, CompilerError, CompilerSuggestionOperation, + ErrorCategory, ErrorSeverity, } from '../CompilerError'; import {Err, Ok, Result} from '../Utils/Result'; @@ -47,7 +49,7 @@ import { makeType, promoteTemporary, } from './HIR'; -import HIRBuilder, {Bindings} from './HIRBuilder'; +import HIRBuilder, {Bindings, createTemporaryPlace} from './HIRBuilder'; import {BuiltInArrayId} from './ObjectShape'; /* @@ -70,21 +72,23 @@ import {BuiltInArrayId} from './ObjectShape'; export function lower( func: NodePath, env: Environment, + // Bindings captured from the outer function, in case lower() is called recursively (for lambdas) bindings: Bindings | null = null, - capturedRefs: Array = [], - // the outermost function being compiled, in case lower() is called recursively (for lambdas) - parent: NodePath | null = null, + capturedRefs: Map = new Map(), ): Result { - const builder = new HIRBuilder(env, parent ?? func, bindings, capturedRefs); + const builder = new HIRBuilder(env, { + bindings, + context: capturedRefs, + }); const context: HIRFunction['context'] = []; - for (const ref of capturedRefs ?? []) { + for (const [ref, loc] of capturedRefs ?? []) { context.push({ kind: 'Identifier', identifier: builder.resolveBinding(ref), effect: Effect.Unknown, reactive: false, - loc: ref.loc ?? GeneratedSource, + loc, }); } @@ -102,12 +106,18 @@ export function lower( if (param.isIdentifier()) { const binding = builder.resolveIdentifier(param); if (binding.kind !== 'Identifier') { - builder.errors.push({ - reason: `(BuildHIR::lower) Could not find binding for param \`${param.node.name}\``, - severity: ErrorSeverity.Invariant, - loc: param.node.loc ?? null, - suggestions: null, - }); + builder.errors.pushDiagnostic( + CompilerDiagnostic.create({ + severity: ErrorSeverity.Invariant, + category: ErrorCategory.Invariant, + reason: 'Could not find binding', + description: `[BuildHIR] Could not find binding for param \`${param.node.name}\`.`, + }).withDetail({ + kind: 'error', + loc: param.node.loc ?? null, + message: 'Could not find binding', + }), + ); return; } const place: Place = { @@ -161,12 +171,18 @@ export function lower( 'Assignment', ); } else { - builder.errors.push({ - reason: `(BuildHIR::lower) Handle ${param.node.type} params`, - severity: ErrorSeverity.Todo, - loc: param.node.loc ?? null, - suggestions: null, - }); + builder.errors.pushDiagnostic( + CompilerDiagnostic.create({ + severity: ErrorSeverity.Todo, + category: ErrorCategory.Todo, + reason: `Handle ${param.node.type} parameters`, + description: `[BuildHIR] Add support for ${param.node.type} parameters.`, + }).withDetail({ + kind: 'error', + loc: param.node.loc ?? null, + message: 'Unsupported parameter type', + }), + ); } }); @@ -176,22 +192,29 @@ export function lower( const fallthrough = builder.reserve('block'); const terminal: ReturnTerminal = { kind: 'return', + returnVariant: 'Implicit', loc: GeneratedSource, value: lowerExpressionToTemporary(builder, body), id: makeInstructionId(0), + effects: null, }; builder.terminateWithContinuation(terminal, fallthrough); } else if (body.isBlockStatement()) { lowerStatement(builder, body); directives = body.get('directives').map(d => d.node.value.value); } else { - builder.errors.push({ - severity: ErrorSeverity.InvalidJS, - reason: `Unexpected function body kind`, - description: `Expected function body to be an expression or a block statement, got \`${body.type}\``, - loc: body.node.loc ?? null, - suggestions: null, - }); + builder.errors.pushDiagnostic( + CompilerDiagnostic.create({ + severity: ErrorSeverity.InvalidJS, + category: ErrorCategory.Syntax, + reason: `Unexpected function body kind`, + description: `Expected function body to be an expression or a block statement, got \`${body.type}\`.`, + }).withDetail({ + kind: 'error', + loc: body.node.loc ?? null, + message: 'Expected a block statement or expression', + }), + ); } if (builder.errors.hasErrors()) { @@ -201,6 +224,7 @@ export function lower( builder.terminate( { kind: 'return', + returnVariant: 'Void', loc: GeneratedSource, value: lowerValueToTemporary(builder, { kind: 'Primitive', @@ -208,6 +232,7 @@ export function lower( loc: GeneratedSource, }), id: makeInstructionId(0), + effects: null, }, null, ); @@ -215,9 +240,9 @@ export function lower( return Ok({ id, params, - fnType: parent == null ? env.fnType : 'Other', + fnType: bindings == null ? env.fnType : 'Other', returnTypeAnnotation: null, // TODO: extract the actual return type node if present - returnType: makeType(), + returns: createTemporaryPlace(env, func.node.loc ?? GeneratedSource), body: builder.build(), context, generator: func.node.generator === true, @@ -225,6 +250,7 @@ export function lower( loc: func.node.loc ?? GeneratedSource, env, effects: null, + aliasingEffects: null, directives, }); } @@ -251,6 +277,7 @@ function lowerStatement( reason: '(BuildHIR::lowerStatement) Support ThrowStatement inside of try/catch', severity: ErrorSeverity.Todo, + category: ErrorCategory.Todo, loc: stmt.node.loc ?? null, suggestions: null, }); @@ -282,9 +309,11 @@ function lowerStatement( } const terminal: ReturnTerminal = { kind: 'return', + returnVariant: 'Explicit', loc: stmt.node.loc ?? GeneratedSource, value, id: makeInstructionId(0), + effects: null, }; builder.terminate(terminal, 'block'); return; @@ -436,6 +465,7 @@ function lowerStatement( } else if (!binding.path.isVariableDeclarator()) { builder.errors.push({ severity: ErrorSeverity.Todo, + category: ErrorCategory.Todo, reason: 'Unsupported declaration type for hoisting', description: `variable "${binding.identifier.name}" declared with ${binding.path.type}`, suggestions: null, @@ -445,6 +475,7 @@ function lowerStatement( } else { builder.errors.push({ severity: ErrorSeverity.Todo, + category: ErrorCategory.Todo, reason: 'Handle non-const declarations for hoisting', description: `variable "${binding.identifier.name}" declared with ${binding.kind}`, suggestions: null, @@ -525,6 +556,7 @@ function lowerStatement( reason: '(BuildHIR::lowerStatement) Handle non-variable initialization in ForStatement', severity: ErrorSeverity.Todo, + category: ErrorCategory.Todo, loc: stmt.node.loc ?? null, suggestions: null, }); @@ -597,6 +629,7 @@ function lowerStatement( builder.errors.push({ reason: `(BuildHIR::lowerStatement) Handle empty test in ForStatement`, severity: ErrorSeverity.Todo, + category: ErrorCategory.Todo, loc: stmt.node.loc ?? null, suggestions: null, }); @@ -748,6 +781,7 @@ function lowerStatement( builder.errors.push({ reason: `Expected at most one \`default\` branch in a switch statement, this code should have failed to parse`, severity: ErrorSeverity.InvalidJS, + category: ErrorCategory.Syntax, loc: case_.node.loc ?? null, suggestions: null, }); @@ -820,6 +854,7 @@ function lowerStatement( builder.errors.push({ reason: `(BuildHIR::lowerStatement) Handle ${nodeKind} kinds in VariableDeclaration`, severity: ErrorSeverity.Todo, + category: ErrorCategory.Todo, loc: stmt.node.loc ?? null, suggestions: null, }); @@ -848,6 +883,7 @@ function lowerStatement( builder.errors.push({ reason: `(BuildHIR::lowerAssignment) Could not find binding for declaration.`, severity: ErrorSeverity.Invariant, + category: ErrorCategory.Invariant, loc: id.node.loc ?? null, suggestions: null, }); @@ -865,6 +901,7 @@ function lowerStatement( builder.errors.push({ reason: `Expect \`const\` declaration not to be reassigned`, severity: ErrorSeverity.InvalidJS, + category: ErrorCategory.Syntax, loc: id.node.loc ?? null, suggestions: [ { @@ -912,6 +949,7 @@ function lowerStatement( reason: `Expected variable declaration to be an identifier if no initializer was provided`, description: `Got a \`${id.type}\``, severity: ErrorSeverity.InvalidJS, + category: ErrorCategory.Syntax, loc: stmt.node.loc ?? null, suggestions: null, }); @@ -1020,6 +1058,7 @@ function lowerStatement( builder.errors.push({ reason: `(BuildHIR::lowerStatement) Handle for-await loops`, severity: ErrorSeverity.Todo, + category: ErrorCategory.Todo, loc: stmt.node.loc ?? null, suggestions: null, }); @@ -1235,6 +1274,7 @@ function lowerStatement( kind: 'Debugger', loc, }, + effects: null, loc, }); return; @@ -1251,6 +1291,7 @@ function lowerStatement( builder.errors.push({ reason: `(BuildHIR::lowerStatement) Handle TryStatement without a catch clause`, severity: ErrorSeverity.Todo, + category: ErrorCategory.Todo, loc: stmt.node.loc ?? null, suggestions: null, }); @@ -1260,6 +1301,7 @@ function lowerStatement( builder.errors.push({ reason: `(BuildHIR::lowerStatement) Handle TryStatement with a finalizer ('finally') clause`, severity: ErrorSeverity.Todo, + category: ErrorCategory.Todo, loc: stmt.node.loc ?? null, suggestions: null, }); @@ -1348,40 +1390,63 @@ function lowerStatement( return; } - case 'TypeAlias': - case 'TSInterfaceDeclaration': - case 'TSTypeAliasDeclaration': { - // We do not preserve type annotations/syntax through transformation + case 'WithStatement': { + builder.errors.push({ + reason: `JavaScript 'with' syntax is not supported`, + description: `'with' syntax is considered deprecated and removed from JavaScript standards, consider alternatives`, + severity: ErrorSeverity.UnsupportedJS, + category: ErrorCategory.UnsupportedSyntax, + loc: stmtPath.node.loc ?? null, + suggestions: null, + }); + lowerValueToTemporary(builder, { + kind: 'UnsupportedNode', + loc: stmtPath.node.loc ?? GeneratedSource, + node: stmtPath.node, + }); + return; + } + case 'ClassDeclaration': { + /** + * In theory we could support inline class declarations, but this is rare enough in practice + * and complex enough to support that we don't anticipate supporting anytime soon. Developers + * are encouraged to lift classes out of component/hook declarations. + */ + builder.errors.push({ + reason: 'Inline `class` declarations are not supported', + description: `Move class declarations outside of components/hooks`, + severity: ErrorSeverity.UnsupportedJS, + category: ErrorCategory.UnsupportedSyntax, + loc: stmtPath.node.loc ?? null, + suggestions: null, + }); + lowerValueToTemporary(builder, { + kind: 'UnsupportedNode', + loc: stmtPath.node.loc ?? GeneratedSource, + node: stmtPath.node, + }); return; } - case 'ClassDeclaration': - case 'DeclareClass': - case 'DeclareExportAllDeclaration': - case 'DeclareExportDeclaration': - case 'DeclareFunction': - case 'DeclareInterface': - case 'DeclareModule': - case 'DeclareModuleExports': - case 'DeclareOpaqueType': - case 'DeclareTypeAlias': - case 'DeclareVariable': case 'EnumDeclaration': + case 'TSEnumDeclaration': { + lowerValueToTemporary(builder, { + kind: 'UnsupportedNode', + loc: stmtPath.node.loc ?? GeneratedSource, + node: stmtPath.node, + }); + return; + } case 'ExportAllDeclaration': case 'ExportDefaultDeclaration': case 'ExportNamedDeclaration': case 'ImportDeclaration': - case 'InterfaceDeclaration': - case 'OpaqueType': - case 'TSDeclareFunction': - case 'TSEnumDeclaration': case 'TSExportAssignment': - case 'TSImportEqualsDeclaration': - case 'TSModuleDeclaration': - case 'TSNamespaceExportDeclaration': - case 'WithStatement': { + case 'TSImportEqualsDeclaration': { builder.errors.push({ - reason: `(BuildHIR::lowerStatement) Handle ${stmtPath.type} statements`, - severity: ErrorSeverity.Todo, + reason: + 'JavaScript `import` and `export` statements may only appear at the top level of a module', + severity: ErrorSeverity.InvalidJS, + category: ErrorCategory.Syntax, loc: stmtPath.node.loc ?? null, suggestions: null, }); @@ -1392,6 +1457,42 @@ function lowerStatement( }); return; } + case 'TSNamespaceExportDeclaration': { + builder.errors.push({ + reason: + 'TypeScript `namespace` statements may only appear at the top level of a module', + severity: ErrorSeverity.InvalidJS, + category: ErrorCategory.Syntax, + loc: stmtPath.node.loc ?? null, + suggestions: null, + }); + lowerValueToTemporary(builder, { + kind: 'UnsupportedNode', + loc: stmtPath.node.loc ?? GeneratedSource, + node: stmtPath.node, + }); + return; + } + case 'DeclareClass': + case 'DeclareExportAllDeclaration': + case 'DeclareExportDeclaration': + case 'DeclareFunction': + case 'DeclareInterface': + case 'DeclareModule': + case 'DeclareModuleExports': + case 'DeclareOpaqueType': + case 'DeclareTypeAlias': + case 'DeclareVariable': + case 'InterfaceDeclaration': + case 'OpaqueType': + case 'TSDeclareFunction': + case 'TSInterfaceDeclaration': + case 'TSModuleDeclaration': + case 'TSTypeAliasDeclaration': + case 'TypeAlias': { + // We do not preserve type annotations/syntax through transformation + return; + } default: { return assertExhaustive( stmtNode, @@ -1440,6 +1541,7 @@ function lowerObjectPropertyKey( builder.errors.push({ reason: `(BuildHIR::lowerExpression) Expected Identifier, got ${key.type} key in ObjectExpression`, severity: ErrorSeverity.Todo, + category: ErrorCategory.Todo, loc: key.node.loc ?? null, suggestions: null, }); @@ -1465,6 +1567,7 @@ function lowerObjectPropertyKey( builder.errors.push({ reason: `(BuildHIR::lowerExpression) Expected Identifier, got ${key.type} key in ObjectExpression`, severity: ErrorSeverity.Todo, + category: ErrorCategory.Todo, loc: key.node.loc ?? null, suggestions: null, }); @@ -1522,6 +1625,7 @@ function lowerExpression( builder.errors.push({ reason: `(BuildHIR::lowerExpression) Handle ${valuePath.type} values in ObjectExpression`, severity: ErrorSeverity.Todo, + category: ErrorCategory.Todo, loc: valuePath.node.loc ?? null, suggestions: null, }); @@ -1548,6 +1652,7 @@ function lowerExpression( builder.errors.push({ reason: `(BuildHIR::lowerExpression) Handle ${propertyPath.node.kind} functions in ObjectExpression`, severity: ErrorSeverity.Todo, + category: ErrorCategory.Todo, loc: propertyPath.node.loc ?? null, suggestions: null, }); @@ -1569,6 +1674,7 @@ function lowerExpression( builder.errors.push({ reason: `(BuildHIR::lowerExpression) Handle ${propertyPath.type} properties in ObjectExpression`, severity: ErrorSeverity.Todo, + category: ErrorCategory.Todo, loc: propertyPath.node.loc ?? null, suggestions: null, }); @@ -1602,6 +1708,7 @@ function lowerExpression( builder.errors.push({ reason: `(BuildHIR::lowerExpression) Handle ${element.type} elements in ArrayExpression`, severity: ErrorSeverity.Todo, + category: ErrorCategory.Todo, loc: element.node.loc ?? null, suggestions: null, }); @@ -1622,6 +1729,7 @@ function lowerExpression( reason: `Expected an expression as the \`new\` expression receiver (v8 intrinsics are not supported)`, description: `Got a \`${calleePath.node.type}\``, severity: ErrorSeverity.InvalidJS, + category: ErrorCategory.Syntax, loc: calleePath.node.loc ?? null, suggestions: null, }); @@ -1648,6 +1756,7 @@ function lowerExpression( builder.errors.push({ reason: `Expected Expression, got ${calleePath.type} in CallExpression (v8 intrinsics not supported). This error is likely caused by a bug in React Compiler. Please file an issue`, severity: ErrorSeverity.Todo, + category: ErrorCategory.Todo, loc: calleePath.node.loc ?? null, suggestions: null, }); @@ -1682,6 +1791,7 @@ function lowerExpression( builder.errors.push({ reason: `(BuildHIR::lowerExpression) Expected Expression, got ${leftPath.type} lval in BinaryExpression`, severity: ErrorSeverity.Todo, + category: ErrorCategory.Todo, loc: leftPath.node.loc ?? null, suggestions: null, }); @@ -1694,6 +1804,7 @@ function lowerExpression( builder.errors.push({ reason: `(BuildHIR::lowerExpression) Pipe operator not supported`, severity: ErrorSeverity.Todo, + category: ErrorCategory.Todo, loc: leftPath.node.loc ?? null, suggestions: null, }); @@ -1723,6 +1834,7 @@ function lowerExpression( builder.errors.push({ reason: `Expected sequence expression to have at least one expression`, severity: ErrorSeverity.InvalidJS, + category: ErrorCategory.Syntax, loc: expr.node.loc ?? null, suggestions: null, }); @@ -1892,6 +2004,7 @@ function lowerExpression( place: leftValue, loc: exprLoc, }, + effects: null, loc: exprLoc, }); builder.terminateWithContinuation( @@ -1934,6 +2047,7 @@ function lowerExpression( reason: `(BuildHIR::lowerExpression) Unsupported syntax on the left side of an AssignmentExpression`, description: `Expected an LVal, got: ${left.type}`, severity: ErrorSeverity.Todo, + category: ErrorCategory.Todo, loc: left.node.loc ?? null, suggestions: null, }); @@ -1962,6 +2076,7 @@ function lowerExpression( builder.errors.push({ reason: `(BuildHIR::lowerExpression) Handle ${operator} operators in AssignmentExpression`, severity: ErrorSeverity.Todo, + category: ErrorCategory.Todo, loc: expr.node.loc ?? null, suggestions: null, }); @@ -2061,6 +2176,7 @@ function lowerExpression( builder.errors.push({ reason: `(BuildHIR::lowerExpression) Expected Identifier or MemberExpression, got ${expr.type} lval in AssignmentExpression`, severity: ErrorSeverity.Todo, + category: ErrorCategory.Todo, loc: expr.node.loc ?? null, suggestions: null, }); @@ -2100,6 +2216,7 @@ function lowerExpression( builder.errors.push({ reason: `(BuildHIR::lowerExpression) Handle ${attribute.type} attributes in JSXElement`, severity: ErrorSeverity.Todo, + category: ErrorCategory.Todo, loc: attribute.node.loc ?? null, suggestions: null, }); @@ -2113,6 +2230,7 @@ function lowerExpression( builder.errors.push({ reason: `(BuildHIR::lowerExpression) Unexpected colon in attribute name \`${propName}\``, severity: ErrorSeverity.Todo, + category: ErrorCategory.Todo, loc: namePath.node.loc ?? null, suggestions: null, }); @@ -2143,6 +2261,7 @@ function lowerExpression( builder.errors.push({ reason: `(BuildHIR::lowerExpression) Handle ${valueExpr.type} attribute values in JSXElement`, severity: ErrorSeverity.Todo, + category: ErrorCategory.Todo, loc: valueExpr.node?.loc ?? null, suggestions: null, }); @@ -2153,6 +2272,7 @@ function lowerExpression( builder.errors.push({ reason: `(BuildHIR::lowerExpression) Handle ${expression.type} expressions in JSXExpressionContainer within JSXElement`, severity: ErrorSeverity.Todo, + category: ErrorCategory.Todo, loc: valueExpr.node.loc ?? null, suggestions: null, }); @@ -2208,11 +2328,18 @@ function lowerExpression( }); for (const [name, locations] of Object.entries(fbtLocations)) { if (locations.length > 1) { - CompilerError.throwTodo({ - reason: `Support <${tagName}> tags with multiple <${tagName}:${name}> values`, - loc: locations.at(-1) ?? GeneratedSource, - description: null, - suggestions: null, + CompilerError.throwDiagnostic({ + severity: ErrorSeverity.Todo, + category: ErrorCategory.FBT, + reason: 'Support duplicate fbt tags', + description: `Support \`<${tagName}>\` tags with multiple \`<${tagName}:${name}>\` values`, + details: locations.map(loc => { + return { + kind: 'error', + message: `Multiple \`<${tagName}:${name}>\` tags found`, + loc, + }; + }), }); } } @@ -2265,6 +2392,7 @@ function lowerExpression( reason: '(BuildHIR::lowerExpression) Handle tagged template with interpolations', severity: ErrorSeverity.Todo, + category: ErrorCategory.Todo, loc: exprPath.node.loc ?? null, suggestions: null, }); @@ -2283,6 +2411,7 @@ function lowerExpression( reason: '(BuildHIR::lowerExpression) Handle tagged template where cooked value is different from raw value', severity: ErrorSeverity.Todo, + category: ErrorCategory.Todo, loc: exprPath.node.loc ?? null, suggestions: null, }); @@ -2305,6 +2434,7 @@ function lowerExpression( builder.errors.push({ reason: `Unexpected quasi and subexpression lengths in template literal`, severity: ErrorSeverity.InvalidJS, + category: ErrorCategory.Syntax, loc: exprPath.node.loc ?? null, suggestions: null, }); @@ -2315,6 +2445,7 @@ function lowerExpression( builder.errors.push({ reason: `(BuildHIR::lowerAssignment) Handle TSType in TemplateLiteral.`, severity: ErrorSeverity.Todo, + category: ErrorCategory.Todo, loc: exprPath.node.loc ?? null, suggestions: null, }); @@ -2357,6 +2488,7 @@ function lowerExpression( builder.errors.push({ reason: `Only object properties can be deleted`, severity: ErrorSeverity.InvalidJS, + category: ErrorCategory.Syntax, loc: expr.node.loc ?? null, suggestions: [ { @@ -2372,6 +2504,7 @@ function lowerExpression( builder.errors.push({ reason: `Throw expressions are not supported`, severity: ErrorSeverity.InvalidJS, + category: ErrorCategory.Syntax, loc: expr.node.loc ?? null, suggestions: [ { @@ -2493,6 +2626,7 @@ function lowerExpression( builder.errors.push({ reason: `(BuildHIR::lowerExpression) Handle UpdateExpression with ${argument.type} argument`, severity: ErrorSeverity.Todo, + category: ErrorCategory.Todo, loc: exprPath.node.loc ?? null, suggestions: null, }); @@ -2501,6 +2635,7 @@ function lowerExpression( builder.errors.push({ reason: `(BuildHIR::lowerExpression) Handle UpdateExpression to variables captured within lambdas.`, severity: ErrorSeverity.Todo, + category: ErrorCategory.Todo, loc: exprPath.node.loc ?? null, suggestions: null, }); @@ -2521,6 +2656,7 @@ function lowerExpression( builder.errors.push({ reason: `(BuildHIR::lowerExpression) Found an invalid UpdateExpression without a previously reported error`, severity: ErrorSeverity.Invariant, + category: ErrorCategory.Invariant, loc: exprLoc, suggestions: null, }); @@ -2530,6 +2666,7 @@ function lowerExpression( builder.errors.push({ reason: `(BuildHIR::lowerExpression) Support UpdateExpression where argument is a global`, severity: ErrorSeverity.Todo, + category: ErrorCategory.Todo, loc: exprLoc, suggestions: null, }); @@ -2585,6 +2722,7 @@ function lowerExpression( builder.errors.push({ reason: `(BuildHIR::lowerExpression) Handle MetaProperty expressions other than import.meta`, severity: ErrorSeverity.Todo, + category: ErrorCategory.Todo, loc: exprPath.node.loc ?? null, suggestions: null, }); @@ -2594,6 +2732,7 @@ function lowerExpression( builder.errors.push({ reason: `(BuildHIR::lowerExpression) Handle ${exprPath.type} expressions`, severity: ErrorSeverity.Todo, + category: ErrorCategory.Todo, loc: exprPath.node.loc ?? null, suggestions: null, }); @@ -2827,6 +2966,7 @@ function lowerOptionalCallExpression( args, loc, }, + effects: null, loc, }); } else { @@ -2840,6 +2980,7 @@ function lowerOptionalCallExpression( args, loc, }, + effects: null, loc, }); } @@ -2889,6 +3030,7 @@ function lowerReorderableExpression( builder.errors.push({ reason: `(BuildHIR::node.lowerReorderableExpression) Expression type \`${expr.type}\` cannot be safely reordered`, severity: ErrorSeverity.Todo, + category: ErrorCategory.Todo, loc: expr.node.loc ?? null, suggestions: null, }); @@ -2936,6 +3078,8 @@ function isReorderableExpression( } } } + case 'TSAsExpression': + case 'TSNonNullExpression': case 'TypeCastExpression': { return isReorderableExpression( builder, @@ -3083,6 +3227,7 @@ function lowerArguments( builder.errors.push({ reason: `(BuildHIR::lowerExpression) Handle ${argPath.type} arguments in CallExpression`, severity: ErrorSeverity.Todo, + category: ErrorCategory.Todo, loc: argPath.node.loc ?? null, suggestions: null, }); @@ -3118,6 +3263,7 @@ function lowerMemberExpression( builder.errors.push({ reason: `(BuildHIR::lowerMemberExpression) Handle ${propertyNode.type} property`, severity: ErrorSeverity.Todo, + category: ErrorCategory.Todo, loc: propertyNode.node.loc ?? null, suggestions: null, }); @@ -3139,6 +3285,7 @@ function lowerMemberExpression( builder.errors.push({ reason: `(BuildHIR::lowerMemberExpression) Expected Expression, got ${propertyNode.type} property`, severity: ErrorSeverity.Todo, + category: ErrorCategory.Todo, loc: propertyNode.node.loc ?? null, suggestions: null, }); @@ -3198,6 +3345,7 @@ function lowerJsxElementName( reason: `Expected JSXNamespacedName to have no colons in the namespace or name`, description: `Got \`${namespace}\` : \`${name}\``, severity: ErrorSeverity.InvalidJS, + category: ErrorCategory.Syntax, loc: exprPath.node.loc ?? null, suggestions: null, }); @@ -3212,6 +3360,7 @@ function lowerJsxElementName( builder.errors.push({ reason: `(BuildHIR::lowerJsxElementName) Handle ${exprPath.type} tags`, severity: ErrorSeverity.Todo, + category: ErrorCategory.Todo, loc: exprPath.node.loc ?? null, suggestions: null, }); @@ -3310,6 +3459,7 @@ function lowerJsxElement( builder.errors.push({ reason: `(BuildHIR::lowerJsxElement) Unhandled JsxElement, got: ${exprPath.type}`, severity: ErrorSeverity.Todo, + category: ErrorCategory.Todo, loc: exprPath.node.loc ?? null, suggestions: null, }); @@ -3417,7 +3567,7 @@ function lowerFunction( | t.ObjectMethod >, ): LoweredFunction | null { - const componentScope: Scope = builder.parentFunction.scope; + const componentScope: Scope = builder.environment.parentFunction.scope; const capturedContext = gatherCapturedContext(expr, componentScope); /* @@ -3432,14 +3582,12 @@ function lowerFunction( expr, builder.environment, builder.bindings, - [...builder.context, ...capturedContext], - builder.parentFunction, + new Map([...builder.context, ...capturedContext]), ); let loweredFunc: HIRFunction; if (lowering.isErr()) { - lowering - .unwrapErr() - .details.forEach(detail => builder.errors.pushErrorDetail(detail)); + const functionErrors = lowering.unwrapErr(); + builder.errors.merge(functionErrors); return null; } loweredFunc = lowering.unwrap(); @@ -3456,7 +3604,7 @@ function lowerExpressionToTemporary( return lowerValueToTemporary(builder, value); } -function lowerValueToTemporary( +export function lowerValueToTemporary( builder: HIRBuilder, value: InstructionValue, ): Place { @@ -3466,9 +3614,10 @@ function lowerValueToTemporary( const place: Place = buildTemporaryPlace(builder, value.loc); builder.push({ id: makeInstructionId(0), + lvalue: {...place}, value: value, + effects: null, loc: value.loc, - lvalue: {...place}, }); return place; } @@ -3492,6 +3641,17 @@ function lowerIdentifier( return place; } default: { + if (binding.kind === 'Global' && binding.name === 'eval') { + builder.errors.push({ + reason: `The 'eval' function is not supported`, + description: + 'Eval is an anti-pattern in JavaScript, and the code executed cannot be evaluated by React Compiler', + severity: ErrorSeverity.UnsupportedJS, + category: ErrorCategory.UnsupportedSyntax, + loc: exprPath.node.loc ?? null, + suggestions: null, + }); + } return lowerValueToTemporary(builder, { kind: 'LoadGlobal', binding, @@ -3544,6 +3704,7 @@ function lowerIdentifierForAssignment( builder.errors.push({ reason: `(BuildHIR::lowerAssignment) Could not find binding for declaration.`, severity: ErrorSeverity.Invariant, + category: ErrorCategory.Invariant, loc: path.node.loc ?? null, suggestions: null, }); @@ -3556,6 +3717,7 @@ function lowerIdentifierForAssignment( builder.errors.push({ reason: `Cannot reassign a \`const\` variable`, severity: ErrorSeverity.InvalidJS, + category: ErrorCategory.Syntax, loc: path.node.loc ?? null, description: binding.identifier.name != null @@ -3613,6 +3775,7 @@ function lowerAssignment( builder.errors.push({ reason: `Expected \`const\` declaration not to be reassigned`, severity: ErrorSeverity.InvalidJS, + category: ErrorCategory.Syntax, loc: lvalue.node.loc ?? null, suggestions: null, }); @@ -3627,6 +3790,7 @@ function lowerAssignment( builder.errors.push({ reason: `Unexpected context variable kind`, severity: ErrorSeverity.InvalidJS, + category: ErrorCategory.Syntax, loc: lvalue.node.loc ?? null, suggestions: null, }); @@ -3698,6 +3862,7 @@ function lowerAssignment( builder.errors.push({ reason: `(BuildHIR::lowerAssignment) Handle ${property.type} properties in MemberExpression`, severity: ErrorSeverity.Todo, + category: ErrorCategory.Todo, loc: property.node.loc ?? null, suggestions: null, }); @@ -3710,6 +3875,7 @@ function lowerAssignment( reason: '(BuildHIR::lowerAssignment) Expected private name to appear as a non-computed property', severity: ErrorSeverity.Todo, + category: ErrorCategory.Todo, loc: property.node.loc ?? null, suggestions: null, }); @@ -3775,6 +3941,7 @@ function lowerAssignment( } else if (identifier.kind === 'Global') { builder.errors.push({ severity: ErrorSeverity.Todo, + category: ErrorCategory.Todo, reason: 'Expected reassignment of globals to enable forceTemporaries', loc: element.node.loc ?? GeneratedSource, @@ -3814,6 +3981,7 @@ function lowerAssignment( } else if (identifier.kind === 'Global') { builder.errors.push({ severity: ErrorSeverity.Todo, + category: ErrorCategory.Todo, reason: 'Expected reassignment of globals to enable forceTemporaries', loc: element.node.loc ?? GeneratedSource, @@ -3887,6 +4055,7 @@ function lowerAssignment( builder.errors.push({ reason: `(BuildHIR::lowerAssignment) Handle ${argument.node.type} rest element in ObjectPattern`, severity: ErrorSeverity.Todo, + category: ErrorCategory.Todo, loc: argument.node.loc ?? null, suggestions: null, }); @@ -3918,6 +4087,7 @@ function lowerAssignment( } else if (identifier.kind === 'Global') { builder.errors.push({ severity: ErrorSeverity.Todo, + category: ErrorCategory.Todo, reason: 'Expected reassignment of globals to enable forceTemporaries', loc: property.node.loc ?? GeneratedSource, @@ -3935,6 +4105,7 @@ function lowerAssignment( builder.errors.push({ reason: `(BuildHIR::lowerAssignment) Handle ${property.type} properties in ObjectPattern`, severity: ErrorSeverity.Todo, + category: ErrorCategory.Todo, loc: property.node.loc ?? null, suggestions: null, }); @@ -3944,6 +4115,7 @@ function lowerAssignment( builder.errors.push({ reason: `(BuildHIR::lowerAssignment) Handle computed properties in ObjectPattern`, severity: ErrorSeverity.Todo, + category: ErrorCategory.Todo, loc: property.node.loc ?? null, suggestions: null, }); @@ -3958,6 +4130,7 @@ function lowerAssignment( builder.errors.push({ reason: `(BuildHIR::lowerAssignment) Expected object property value to be an LVal, got: ${element.type}`, severity: ErrorSeverity.Todo, + category: ErrorCategory.Todo, loc: element.node.loc ?? null, suggestions: null, }); @@ -3980,6 +4153,7 @@ function lowerAssignment( } else if (identifier.kind === 'Global') { builder.errors.push({ severity: ErrorSeverity.Todo, + category: ErrorCategory.Todo, reason: 'Expected reassignment of globals to enable forceTemporaries', loc: element.node.loc ?? GeneratedSource, @@ -4129,6 +4303,7 @@ function lowerAssignment( builder.errors.push({ reason: `(BuildHIR::lowerAssignment) Handle ${lvaluePath.type} assignments`, severity: ErrorSeverity.Todo, + category: ErrorCategory.Todo, loc: lvaluePath.node.loc ?? null, suggestions: null, }); @@ -4151,6 +4326,11 @@ function captureScopes({from, to}: {from: Scope; to: Scope}): Set { return scopes; } +/** + * Returns a mapping of "context" identifiers — references to free variables that + * will become part of the function expression's `context` array — along with the + * source location of their first reference within the function. + */ function gatherCapturedContext( fn: NodePath< | t.FunctionExpression @@ -4159,8 +4339,8 @@ function gatherCapturedContext( | t.ObjectMethod >, componentScope: Scope, -): Array { - const capturedIds = new Set(); +): Map { + const capturedIds = new Map(); /* * Capture all the scopes from the parent of this function up to and including @@ -4203,8 +4383,15 @@ function gatherCapturedContext( // Add the base identifier binding as a dependency. const binding = baseIdentifier.scope.getBinding(baseIdentifier.node.name); - if (binding !== undefined && pureScopes.has(binding.scope)) { - capturedIds.add(binding.identifier); + if ( + binding !== undefined && + pureScopes.has(binding.scope) && + !capturedIds.has(binding.identifier) + ) { + capturedIds.set( + binding.identifier, + path.node.loc ?? binding.identifier.loc ?? GeneratedSource, + ); } } @@ -4241,7 +4428,7 @@ function gatherCapturedContext( }, }); - return [...capturedIds.keys()]; + return capturedIds; } function notNull(value: T | null): value is T { diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/CollectHoistablePropertyLoads.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/CollectHoistablePropertyLoads.ts index ea7268c573379..a11822538f54f 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/CollectHoistablePropertyLoads.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/CollectHoistablePropertyLoads.ts @@ -241,7 +241,10 @@ type PropertyPathNode = class PropertyPathRegistry { roots: Map = new Map(); - getOrCreateIdentifier(identifier: Identifier): PropertyPathNode { + getOrCreateIdentifier( + identifier: Identifier, + reactive: boolean, + ): PropertyPathNode { /** * Reads from a statically scoped variable are always safe in JS, * with the exception of TDZ (not addressed by this pass). @@ -255,12 +258,19 @@ class PropertyPathRegistry { optionalProperties: new Map(), fullPath: { identifier, + reactive, path: [], }, hasOptional: false, parent: null, }; this.roots.set(identifier.id, rootNode); + } else { + CompilerError.invariant(reactive === rootNode.fullPath.reactive, { + reason: + '[HoistablePropertyLoads] Found inconsistencies in `reactive` flag when deduping identifier reads within the same scope', + loc: identifier.loc, + }); } return rootNode; } @@ -278,6 +288,7 @@ class PropertyPathRegistry { parent: parent, fullPath: { identifier: parent.fullPath.identifier, + reactive: parent.fullPath.reactive, path: parent.fullPath.path.concat(entry), }, hasOptional: parent.hasOptional || entry.optional, @@ -293,7 +304,7 @@ class PropertyPathRegistry { * so all subpaths of a PropertyLoad should already exist * (e.g. a.b is added before a.b.c), */ - let currNode = this.getOrCreateIdentifier(n.identifier); + let currNode = this.getOrCreateIdentifier(n.identifier, n.reactive); if (n.path.length === 0) { return currNode; } @@ -315,10 +326,11 @@ function getMaybeNonNullInInstruction( instr: InstructionValue, context: CollectHoistablePropertyLoadsContext, ): PropertyPathNode | null { - let path = null; + let path: ReactiveScopeDependency | null = null; if (instr.kind === 'PropertyLoad') { path = context.temporaries.get(instr.object.identifier.id) ?? { identifier: instr.object.identifier, + reactive: instr.object.reactive, path: [], }; } else if (instr.kind === 'Destructure') { @@ -381,7 +393,7 @@ function collectNonNullsInBlocks( ) { const identifier = fn.params[0].identifier; knownNonNullIdentifiers.add( - context.registry.getOrCreateIdentifier(identifier), + context.registry.getOrCreateIdentifier(identifier, true), ); } const nodes = new Map< @@ -616,9 +628,11 @@ function reduceMaybeOptionalChains( changed = false; for (const original of optionalChainNodes) { - let {identifier, path: origPath} = original.fullPath; - let currNode: PropertyPathNode = - registry.getOrCreateIdentifier(identifier); + let {identifier, path: origPath, reactive} = original.fullPath; + let currNode: PropertyPathNode = registry.getOrCreateIdentifier( + identifier, + reactive, + ); for (let i = 0; i < origPath.length; i++) { const entry = origPath[i]; // If the base is known to be non-null, replace with a non-optional load diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/CollectOptionalChainDependencies.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/CollectOptionalChainDependencies.ts index cb787d04d0623..75dad4c1bfe63 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/CollectOptionalChainDependencies.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/CollectOptionalChainDependencies.ts @@ -290,6 +290,7 @@ function traverseOptionalBlock( ); baseObject = { identifier: maybeTest.instructions[0].value.place.identifier, + reactive: maybeTest.instructions[0].value.place.reactive, path, }; test = maybeTest.terminal; @@ -391,6 +392,7 @@ function traverseOptionalBlock( ); const load = { identifier: baseObject.identifier, + reactive: baseObject.reactive, path: [ ...baseObject.path, { diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/DefaultModuleTypeProvider.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/DefaultModuleTypeProvider.ts new file mode 100644 index 0000000000000..3b3e120f39076 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/DefaultModuleTypeProvider.ts @@ -0,0 +1,91 @@ +/** + * 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 {Effect, ValueKind} from '..'; +import {TypeConfig} from './TypeSchema'; + +/** + * Libraries developed before we officially documented the [Rules of React](https://react.dev/reference/rules) + * implement APIs which cannot be memoized safely, either via manual or automatic memoization. + * + * Any non-hook API that is designed to be called during render (not events/effects) should be safe to memoize: + * + * ```js + * function Component() { + * const {someFunction} = useLibrary(); + * // it should always be safe to memoize functions like this + * const result = useMemo(() => someFunction(), [someFunction]); + * } + * ``` + * + * However, some APIs implement "interior mutability" — mutating values rather than copying into a new value + * and setting state with the new value. Such functions (`someFunction()` in the example) could return different + * values even though the function itself is the same object. This breaks memoization, since React relies on + * the outer object (or function) changing if part of its value has changed. + * + * Given that we didn't have the Rules of React precisely documented prior to the introduction of React compiler, + * it's understandable that some libraries accidentally shipped APIs that break this rule. However, developers + * can easily run into pitfalls with these APIs. They may manually memoize them, which can break their app. Or + * they may try using React Compiler, and think that the compiler has broken their code. + * + * To help ensure that developers can successfully use the compiler with existing code, this file teaches the + * compiler about specific APIs that are known to be incompatible with memoization. We've tried to be as precise + * as possible. + * + * The React team is open to collaborating with library authors to help develop compatible versions of these APIs, + * and we have already reached out to the teams who own any API listed here to ensure they are aware of the issue. + */ +export function defaultModuleTypeProvider( + moduleName: string, +): TypeConfig | null { + switch (moduleName) { + case 'react-hook-form': { + return { + kind: 'object', + properties: { + useForm: { + kind: 'hook', + returnType: { + kind: 'object', + properties: { + // Only the `watch()` function returned by react-hook-form's `useForm()` API is incompatible + watch: { + kind: 'function', + positionalParams: [], + restParam: Effect.Read, + calleeEffect: Effect.Read, + returnType: {kind: 'type', name: 'Any'}, + returnValueKind: ValueKind.Mutable, + knownIncompatible: `React Hook Form's \`useForm()\` API returns a \`watch()\` function which cannot be memoized safely.`, + }, + }, + }, + }, + }, + }; + } + case '@tanstack/react-table': { + return { + kind: 'object', + properties: { + /* + * Many of the properties of `useReactTable()`'s return value are incompatible, so we mark the entire hook + * as incompatible + */ + useReactTable: { + kind: 'hook', + positionalParams: [], + restParam: Effect.Read, + returnType: {kind: 'type', name: 'Any'}, + knownIncompatible: `TanStack Table's \`useReactTable()\` API returns functions that cannot be memoized safely`, + }, + }, + }; + } + } + return null; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/DeriveMinimalDependenciesHIR.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/DeriveMinimalDependenciesHIR.ts index 7f6fb9e88f817..7819ab39b2c69 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/DeriveMinimalDependenciesHIR.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/DeriveMinimalDependenciesHIR.ts @@ -25,8 +25,9 @@ export class ReactiveScopeDependencyTreeHIR { * `identifier.path`, or `identifier?.path` is in this map, it is safe to * evaluate (non-optional) PropertyLoads from. */ - #hoistableObjects: Map = new Map(); - #deps: Map = new Map(); + #hoistableObjects: Map = + new Map(); + #deps: Map = new Map(); /** * @param hoistableObjects a set of paths from which we can safely evaluate @@ -35,9 +36,10 @@ export class ReactiveScopeDependencyTreeHIR { * duplicates when traversing the CFG. */ constructor(hoistableObjects: Iterable) { - for (const {path, identifier} of hoistableObjects) { + for (const {path, identifier, reactive} of hoistableObjects) { let currNode = ReactiveScopeDependencyTreeHIR.#getOrCreateRoot( identifier, + reactive, this.#hoistableObjects, path.length > 0 && path[0].optional ? 'Optional' : 'NonNull', ); @@ -70,7 +72,8 @@ export class ReactiveScopeDependencyTreeHIR { static #getOrCreateRoot( identifier: Identifier, - roots: Map>, + reactive: boolean, + roots: Map & {reactive: boolean}>, defaultAccessType: T, ): TreeNode { // roots can always be accessed unconditionally in JS @@ -79,9 +82,16 @@ export class ReactiveScopeDependencyTreeHIR { if (rootNode === undefined) { rootNode = { properties: new Map(), + reactive, accessType: defaultAccessType, }; roots.set(identifier, rootNode); + } else { + CompilerError.invariant(reactive === rootNode.reactive, { + reason: '[DeriveMinimalDependenciesHIR] Conflicting reactive root flag', + description: `Identifier ${printIdentifier(identifier)}`, + loc: GeneratedSource, + }); } return rootNode; } @@ -92,9 +102,10 @@ export class ReactiveScopeDependencyTreeHIR { * safe-to-evaluate subpath */ addDependency(dep: ReactiveScopeDependency): void { - const {identifier, path} = dep; + const {identifier, reactive, path} = dep; let depCursor = ReactiveScopeDependencyTreeHIR.#getOrCreateRoot( identifier, + reactive, this.#deps, PropertyAccessType.UnconditionalAccess, ); @@ -172,7 +183,13 @@ export class ReactiveScopeDependencyTreeHIR { deriveMinimalDependencies(): Set { const results = new Set(); for (const [rootId, rootNode] of this.#deps.entries()) { - collectMinimalDependenciesInSubtree(rootNode, rootId, [], results); + collectMinimalDependenciesInSubtree( + rootNode, + rootNode.reactive, + rootId, + [], + results, + ); } return results; @@ -294,25 +311,24 @@ type HoistableNode = TreeNode<'Optional' | 'NonNull'>; type DependencyNode = TreeNode; /** - * TODO: this is directly pasted from DeriveMinimalDependencies. Since we no - * longer have conditionally accessed nodes, we can simplify - * * Recursively calculates minimal dependencies in a subtree. * @param node DependencyNode representing a dependency subtree. * @returns a minimal list of dependencies in this subtree. */ function collectMinimalDependenciesInSubtree( node: DependencyNode, + reactive: boolean, rootIdentifier: Identifier, path: Array, results: Set, ): void { if (isDependency(node.accessType)) { - results.add({identifier: rootIdentifier, path}); + results.add({identifier: rootIdentifier, reactive, path}); } else { for (const [childName, childNode] of node.properties) { collectMinimalDependenciesInSubtree( childNode, + reactive, rootIdentifier, [ ...path, 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 6e6643cd1d68f..421b204e655c7 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts @@ -47,8 +47,10 @@ import { ShapeRegistry, addHook, } from './ObjectShape'; -import {Scope as BabelScope} from '@babel/traverse'; +import {Scope as BabelScope, NodePath} from '@babel/traverse'; import {TypeSchema} from './TypeSchema'; +import {FlowTypeEnv} from '../Flood/Types'; +import {defaultModuleTypeProvider} from './DefaultModuleTypeProvider'; export const ReactElementSymbolSchema = z.object({ elementSymbol: z.union([ @@ -243,6 +245,12 @@ export const EnvironmentConfigSchema = z.object({ */ enableUseTypeAnnotations: z.boolean().default(false), + /** + * Allows specifying a function that can populate HIR with type information from + * Flow + */ + flowTypeProvider: z.nullable(z.function().args(z.string())).default(null), + /** * Enables inference of optional dependency chains. Without this flag * a property chain such as `props?.items?.foo` will infer as a dep on @@ -260,21 +268,19 @@ export const EnvironmentConfigSchema = z.object({ * { * module: 'react', * imported: 'useEffect', - * numRequiredArgs: 1, + * autodepsIndex: 1, * },{ * module: 'MyExperimentalEffectHooks', * imported: 'useExperimentalEffect', - * numRequiredArgs: 2, + * autodepsIndex: 2, * }, * ] * would insert dependencies for calls of `useEffect` imported from `react` and calls of * useExperimentalEffect` from `MyExperimentalEffectHooks`. * - * `numRequiredArgs` tells the compiler the amount of arguments required to append a dependency - * array to the end of the call. With the configuration above, we'd insert dependencies for - * `useEffect` if it is only given a single argument and it would be appended to the argument list. - * - * numRequiredArgs must always be greater than 0, otherwise there is no function to analyze for dependencies + * `autodepsIndex` tells the compiler which index we expect the AUTODEPS to appear in. + * With the configuration above, we'd insert dependencies for `useEffect` if it has two + * arguments, and the second is AUTODEPS. * * Still experimental. */ @@ -283,7 +289,7 @@ export const EnvironmentConfigSchema = z.object({ z.array( z.object({ function: ExternalFunctionSchema, - numRequiredArgs: z.number().min(1, 'numRequiredArgs must be > 0'), + autodepsIndex: z.number().min(1, 'autodepsIndex must be > 0'), }), ), ) @@ -315,10 +321,16 @@ export const EnvironmentConfigSchema = z.object({ validateNoSetStateInRender: z.boolean().default(true), /** - * Validates that setState is not called directly within a passive effect (useEffect). + * Validates that setState is not called synchronously within an effect (useEffect and friends). * Scheduling a setState (with an event listener, subscription, etc) is valid. */ - validateNoSetStateInPassiveEffects: z.boolean().default(false), + validateNoSetStateInEffects: z.boolean().default(false), + + /** + * Validates that effects are not used to calculate derived data which could instead be computed + * during render. + */ + validateNoDerivedComputationsInEffects: z.boolean().default(false), /** * Validates against creating JSX within a try block and recommends using an error boundary @@ -605,7 +617,7 @@ export const EnvironmentConfigSchema = z.object({ * * Here the variables `ref` and `myRef` will be typed as Refs. */ - enableTreatRefLikeIdentifiersAsRefs: z.boolean().default(false), + enableTreatRefLikeIdentifiersAsRefs: z.boolean().default(true), /* * If specified a value, the compiler lowers any calls to `useContext` to use @@ -628,6 +640,24 @@ export const EnvironmentConfigSchema = z.object({ * ``` */ lowerContextAccess: ExternalFunctionSchema.nullable().default(null), + + /** + * If enabled, will validate useMemos that don't return any values: + * + * Valid: + * useMemo(() => foo, [foo]); + * useMemo(() => { return foo }, [foo]); + * Invalid: + * useMemo(() => { ... }, [...]); + */ + validateNoVoidUseMemo: z.boolean().default(false), + + /** + * Validates that Components/Hooks are always defined at module level. This prevents scope + * reference errors that occur when the compiler attempts to optimize the nested component/hook + * while its parent function remains uncompiled. + */ + validateNoDynamicallyCreatedComponentsOrHooks: z.boolean().default(false), }); export type EnvironmentConfig = z.infer; @@ -675,6 +705,9 @@ export class Environment { #contextIdentifiers: Set; #hoistedIdentifiers: Set; + parentFunction: NodePath; + + #flowTypeEnvironment: FlowTypeEnv | null; constructor( scope: BabelScope, @@ -682,6 +715,7 @@ export class Environment { compilerMode: CompilerMode, config: EnvironmentConfig, contextIdentifiers: Set, + parentFunction: NodePath, // the outermost function being compiled logger: Logger | null, filename: string | null, code: string | null, @@ -740,8 +774,29 @@ export class Environment { this.#moduleTypes.set(REANIMATED_MODULE_NAME, reanimatedModuleType); } + this.parentFunction = parentFunction; this.#contextIdentifiers = contextIdentifiers; this.#hoistedIdentifiers = new Set(); + + if (config.flowTypeProvider != null) { + this.#flowTypeEnvironment = new FlowTypeEnv(); + CompilerError.invariant(code != null, { + reason: + 'Expected Environment to be initialized with source code when a Flow type provider is specified', + loc: null, + }); + this.#flowTypeEnvironment.init(this, code); + } else { + this.#flowTypeEnvironment = null; + } + } + + get typeContext(): FlowTypeEnv { + CompilerError.invariant(this.#flowTypeEnvironment != null, { + reason: 'Flow type environment not initialized', + loc: null, + }); + return this.#flowTypeEnvironment; } get isInferredMemoEnabled(): boolean { @@ -806,10 +861,16 @@ export class Environment { #resolveModuleType(moduleName: string, loc: SourceLocation): Global | null { let moduleType = this.#moduleTypes.get(moduleName); if (moduleType === undefined) { - if (this.config.moduleTypeProvider == null) { + /* + * NOTE: Zod doesn't work when specifying a function as a default, so we have to + * fallback to the default value here + */ + const moduleTypeProvider = + this.config.moduleTypeProvider ?? defaultModuleTypeProvider; + if (moduleTypeProvider == null) { return null; } - const unparsedModuleConfig = this.config.moduleTypeProvider(moduleName); + const unparsedModuleConfig = moduleTypeProvider(moduleName); if (unparsedModuleConfig != null) { const parsedModuleConfig = TypeSchema.safeParse(unparsedModuleConfig); if (!parsedModuleConfig.success) { diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/Globals.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/Globals.ts index b8504494662d6..561bdab6982d7 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/Globals.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/Globals.ts @@ -9,6 +9,7 @@ import {Effect, ValueKind, ValueReason} from './HIR'; import { BUILTIN_SHAPES, BuiltInArrayId, + BuiltInAutodepsId, BuiltInFireFunctionId, BuiltInFireId, BuiltInMapId, @@ -17,6 +18,7 @@ import { BuiltInSetId, BuiltInUseActionStateId, BuiltInUseContextHookId, + BuiltInUseEffectEventId, BuiltInUseEffectHookId, BuiltInUseInsertionEffectHookId, BuiltInUseLayoutEffectHookId, @@ -27,6 +29,7 @@ import { BuiltInUseTransitionId, BuiltInWeakMapId, BuiltInWeakSetId, + BuiltinEffectEventId, ReanimatedSharedValueId, ShapeRegistry, addFunction, @@ -111,6 +114,99 @@ const TYPED_GLOBALS: Array<[string, BuiltInType]> = [ returnValueKind: ValueKind.Mutable, }), ], + [ + 'entries', + addFunction(DEFAULT_SHAPES, [], { + positionalParams: [Effect.Capture], + restParam: null, + returnType: {kind: 'Object', shapeId: BuiltInArrayId}, + calleeEffect: Effect.Read, + returnValueKind: ValueKind.Mutable, + aliasing: { + receiver: '@receiver', + params: ['@object'], + rest: null, + returns: '@returns', + temporaries: [], + effects: [ + { + kind: 'Create', + into: '@returns', + reason: ValueReason.KnownReturnSignature, + value: ValueKind.Mutable, + }, + // Object values are captured into the return + { + kind: 'Capture', + from: '@object', + into: '@returns', + }, + ], + }, + }), + ], + [ + 'keys', + addFunction(DEFAULT_SHAPES, [], { + positionalParams: [Effect.Read], + restParam: null, + returnType: {kind: 'Object', shapeId: BuiltInArrayId}, + calleeEffect: Effect.Read, + returnValueKind: ValueKind.Mutable, + aliasing: { + receiver: '@receiver', + params: ['@object'], + rest: null, + returns: '@returns', + temporaries: [], + effects: [ + { + kind: 'Create', + into: '@returns', + reason: ValueReason.KnownReturnSignature, + value: ValueKind.Mutable, + }, + // Only keys are captured, and keys are immutable + { + kind: 'ImmutableCapture', + from: '@object', + into: '@returns', + }, + ], + }, + }), + ], + [ + 'values', + addFunction(DEFAULT_SHAPES, [], { + positionalParams: [Effect.Capture], + restParam: null, + returnType: {kind: 'Object', shapeId: BuiltInArrayId}, + calleeEffect: Effect.Read, + returnValueKind: ValueKind.Mutable, + aliasing: { + receiver: '@receiver', + params: ['@object'], + rest: null, + returns: '@returns', + temporaries: [], + effects: [ + { + kind: 'Create', + into: '@returns', + reason: ValueReason.KnownReturnSignature, + value: ValueKind.Mutable, + }, + // Object values are captured into the return + { + kind: 'Capture', + from: '@object', + into: '@returns', + }, + ], + }, + }), + ], ]), ], [ @@ -642,6 +738,41 @@ const REACT_APIS: Array<[string, BuiltInType]> = [ calleeEffect: Effect.Read, hookKind: 'useEffect', returnValueKind: ValueKind.Frozen, + aliasing: { + receiver: '@receiver', + params: [], + rest: '@rest', + returns: '@returns', + temporaries: ['@effect'], + effects: [ + // Freezes the function and deps + { + kind: 'Freeze', + value: '@rest', + reason: ValueReason.Effect, + }, + // Internally creates an effect object that captures the function and deps + { + kind: 'Create', + into: '@effect', + value: ValueKind.Frozen, + reason: ValueReason.KnownReturnSignature, + }, + // The effect stores the function and dependencies + { + kind: 'Capture', + from: '@rest', + into: '@effect', + }, + // Returns undefined + { + kind: 'Create', + into: '@returns', + value: ValueKind.Primitive, + reason: ValueReason.KnownReturnSignature, + }, + ], + }, }, BuiltInUseEffectHookId, ), @@ -722,6 +853,28 @@ const REACT_APIS: Array<[string, BuiltInType]> = [ BuiltInFireId, ), ], + [ + 'useEffectEvent', + addHook( + DEFAULT_SHAPES, + { + positionalParams: [], + restParam: Effect.Freeze, + returnType: { + kind: 'Function', + return: {kind: 'Poly'}, + shapeId: BuiltinEffectEventId, + isConstructor: false, + }, + calleeEffect: Effect.Read, + hookKind: 'useEffectEvent', + // Frozen because it should not mutate any locally-bound values + returnValueKind: ValueKind.Frozen, + }, + BuiltInUseEffectEventId, + ), + ], + ['AUTODEPS', addObject(DEFAULT_SHAPES, BuiltInAutodepsId, [])], ]; TYPED_GLOBALS.push( @@ -847,6 +1000,8 @@ export function installTypeConfig( noAlias: typeConfig.noAlias === true, mutableOnlyIfOperandsAreMutable: typeConfig.mutableOnlyIfOperandsAreMutable === true, + aliasing: typeConfig.aliasing, + knownIncompatible: typeConfig.knownIncompatible ?? null, }); } case 'hook': { @@ -864,6 +1019,8 @@ export function installTypeConfig( ), returnValueKind: typeConfig.returnValueKind ?? ValueKind.Frozen, noAlias: typeConfig.noAlias === true, + aliasing: typeConfig.aliasing, + knownIncompatible: typeConfig.knownIncompatible ?? null, }); } case 'object': { 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 99b8c189ee0fd..6b3ba6f94c8ae 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts @@ -7,12 +7,14 @@ import {BindingKind} from '@babel/traverse'; import * as t from '@babel/types'; -import {CompilerError, CompilerErrorDetailOptions} from '../CompilerError'; +import {CompilerError} from '../CompilerError'; import {assertExhaustive} from '../Utils/utils'; import {Environment, ReactFunctionType} from './Environment'; import type {HookKind} from './ObjectShape'; import {Type, makeType} from './Types'; import {z} from 'zod'; +import type {AliasingEffect} from '../Inference/AliasingEffects'; +import {isReservedWord} from '../Utils/Keyword'; /* * ******************************************************************************************* @@ -100,6 +102,7 @@ export type ReactiveInstruction = { id: InstructionId; lvalue: Place | null; value: ReactiveValue; + effects?: Array | null; // TODO make non-optional loc: SourceLocation; }; @@ -277,31 +280,15 @@ export type HIRFunction = { env: Environment; params: Array; returnTypeAnnotation: t.FlowType | t.TSType | null; - returnType: Type; + returns: Place; context: Array; - effects: Array | null; body: HIR; generator: boolean; async: boolean; directives: Array; + aliasingEffects: Array | null; }; -export type FunctionEffect = - | { - kind: 'GlobalMutation'; - error: CompilerErrorDetailOptions; - } - | { - kind: 'ReactMutation'; - error: CompilerErrorDetailOptions; - } - | { - kind: 'ContextMutation'; - places: ReadonlySet; - effect: Effect; - loc: SourceLocation; - }; - /* * Each reactive scope may have its own control-flow, so the instructions form * a control-flow graph. The graph comprises a set of basic blocks which reference @@ -443,12 +430,25 @@ export type ThrowTerminal = { }; export type Case = {test: Place | null; block: BlockId}; +export type ReturnVariant = 'Void' | 'Implicit' | 'Explicit'; export type ReturnTerminal = { kind: 'return'; + /** + * Void: + * () => { ... } + * function() { ... } + * Implicit (ArrowFunctionExpression only): + * () => foo + * Explicit: + * () => { return ... } + * function () { return ... } + */ + returnVariant: ReturnVariant; loc: SourceLocation; value: Place; id: InstructionId; fallthrough?: never; + effects: Array | null; }; export type GotoTerminal = { @@ -609,6 +609,7 @@ export type MaybeThrowTerminal = { id: InstructionId; loc: SourceLocation; fallthrough?: never; + effects: Array | null; }; export type ReactiveScopeTerminal = { @@ -645,12 +646,14 @@ export type Instruction = { lvalue: Place; value: InstructionValue; loc: SourceLocation; + effects: Array | null; }; export type TInstruction = { id: InstructionId; lvalue: Place; value: T; + effects: Array | null; loc: SourceLocation; }; @@ -1301,12 +1304,21 @@ export function forkTemporaryIdentifier( * original source code. */ export function makeIdentifierName(name: string): ValidatedIdentifier { - CompilerError.invariant(t.isValidIdentifier(name), { - reason: `Expected a valid identifier name`, - loc: GeneratedSource, - description: `\`${name}\` is not a valid JavaScript identifier`, - suggestions: null, - }); + if (isReservedWord(name)) { + CompilerError.throwInvalidJS({ + reason: 'Expected a non-reserved identifier name', + loc: GeneratedSource, + description: `\`${name}\` is a reserved word in JavaScript and cannot be used as an identifier name`, + suggestions: null, + }); + } else { + CompilerError.invariant(t.isValidIdentifier(name), { + reason: `Expected a valid identifier name`, + loc: GeneratedSource, + description: `\`${name}\` is not a valid JavaScript identifier`, + suggestions: null, + }); + } return { kind: 'named', value: name as ValidIdentifierName, @@ -1380,6 +1392,21 @@ export enum ValueReason { */ JsxCaptured = 'jsx-captured', + /** + * Argument to a hook + */ + HookCaptured = 'hook-captured', + + /** + * Return value of a hook + */ + HookReturn = 'hook-return', + + /** + * Passed to an effect + */ + Effect = 'effect', + /** * Return value of a function with known frozen return value, e.g. `useState`. */ @@ -1430,6 +1457,20 @@ export const ValueKindSchema = z.enum([ ValueKind.Context, ]); +export const ValueReasonSchema = z.enum([ + ValueReason.Context, + ValueReason.Effect, + ValueReason.Global, + ValueReason.HookCaptured, + ValueReason.HookReturn, + ValueReason.JsxCaptured, + ValueReason.KnownReturnSignature, + ValueReason.Other, + ValueReason.ReactiveFunctionArgument, + ValueReason.ReducerState, + ValueReason.State, +]); + // The effect with which a value is modified. export enum Effect { // Default value: not allowed after lifetime inference @@ -1568,6 +1609,18 @@ export type DependencyPathEntry = { export type DependencyPath = Array; export type ReactiveScopeDependency = { identifier: Identifier; + /** + * Reflects whether the base identifier is reactive. Note that some reactive + * objects may have non-reactive properties, but we do not currently track + * this. + * + * ```js + * // Technically, result[0] is reactive and result[1] is not. + * // Currently, both dependencies would be marked as reactive. + * const result = useState(); + * ``` + */ + reactive: boolean; path: DependencyPath; }; @@ -1721,6 +1774,10 @@ export function isUseStateType(id: Identifier): boolean { return id.type.kind === 'Object' && id.type.shapeId === 'BuiltInUseState'; } +export function isJsxType(type: Type): boolean { + return type.kind === 'Object' && type.shapeId === 'BuiltInJsx'; +} + export function isRefOrRefValue(id: Identifier): boolean { return isUseRefType(id) || isRefValueType(id); } @@ -1773,6 +1830,13 @@ export function isFireFunctionType(id: Identifier): boolean { ); } +export function isEffectEventFunctionType(id: Identifier): boolean { + return ( + id.type.kind === 'Function' && + id.type.shapeId === 'BuiltInEffectEventFunction' + ); +} + export function isStableType(id: Identifier): boolean { return ( isSetStateType(id) || 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 44dd34b7d6cae..78c756f812d06 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/HIRBuilder.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/HIRBuilder.ts @@ -7,7 +7,7 @@ import {Binding, NodePath} from '@babel/traverse'; import * as t from '@babel/types'; -import {CompilerError} from '../CompilerError'; +import {CompilerError, ErrorCategory, ErrorSeverity} from '../CompilerError'; import {Environment} from './Environment'; import { BasicBlock, @@ -106,11 +106,10 @@ export default class HIRBuilder { #current: WipBlock; #entry: BlockId; #scopes: Array = []; - #context: Array; + #context: Map; #bindings: Bindings; #env: Environment; #exceptionHandlerStack: Array = []; - parentFunction: NodePath; errors: CompilerError = new CompilerError(); /** * Traversal context: counts the number of `fbt` tag parents @@ -122,7 +121,7 @@ export default class HIRBuilder { return this.#env.nextIdentifierId; } - get context(): Array { + get context(): Map { return this.#context; } @@ -136,16 +135,17 @@ export default class HIRBuilder { constructor( env: Environment, - parentFunction: NodePath, // the outermost function being compiled - bindings: Bindings | null = null, - context: Array | null = null, + options?: { + bindings?: Bindings | null; + context?: Map; + entryBlockKind?: BlockKind; + }, ) { this.#env = env; - this.#bindings = bindings ?? new Map(); - this.parentFunction = parentFunction; - this.#context = context ?? []; + this.#bindings = options?.bindings ?? new Map(); + this.#context = options?.context ?? new Map(); this.#entry = makeBlockId(env.nextBlockId); - this.#current = newBlock(this.#entry, 'block'); + this.#current = newBlock(this.#entry, options?.entryBlockKind ?? 'block'); } currentBlockKind(): BlockKind { @@ -165,6 +165,7 @@ export default class HIRBuilder { handler: exceptionHandler, id: makeInstructionId(0), loc: instruction.loc, + effects: null, }, continuationBlock, ); @@ -239,7 +240,7 @@ export default class HIRBuilder { // Check if the binding is from module scope const outerBinding = - this.parentFunction.scope.parent.getBinding(originalName); + this.#env.parentFunction.scope.parent.getBinding(originalName); if (babelBinding === outerBinding) { const path = babelBinding.path; if (path.isImportDefaultSpecifier()) { @@ -293,7 +294,7 @@ export default class HIRBuilder { const binding = this.#resolveBabelBinding(path); if (binding) { // Check if the binding is from module scope, if so return null - const outerBinding = this.parentFunction.scope.parent.getBinding( + const outerBinding = this.#env.parentFunction.scope.parent.getBinding( path.node.name, ); if (binding === outerBinding) { @@ -307,9 +308,35 @@ export default class HIRBuilder { resolveBinding(node: t.Identifier): Identifier { if (node.name === 'fbt') { - CompilerError.throwTodo({ - reason: 'Support local variables named "fbt"', - loc: node.loc ?? null, + CompilerError.throwDiagnostic({ + severity: ErrorSeverity.Todo, + category: ErrorCategory.FBT, + reason: 'Support local variables named `fbt`', + description: + 'Local variables named `fbt` may conflict with the fbt plugin and are not yet supported', + details: [ + { + kind: 'error', + message: 'Rename to avoid conflict with fbt plugin', + loc: node.loc ?? GeneratedSource, + }, + ], + }); + } + if (node.name === 'this') { + CompilerError.throwDiagnostic({ + severity: ErrorSeverity.UnsupportedJS, + category: ErrorCategory.UnsupportedSyntax, + reason: '`this` is not supported syntax', + description: + 'React Compiler does not support compiling functions that use `this`', + details: [ + { + kind: 'error', + message: '`this` was used here', + loc: node.loc ?? GeneratedSource, + }, + ], }); } const originalName = node.name; @@ -376,7 +403,7 @@ export default class HIRBuilder { } // Terminate the current block w the given terminal, and start a new block - terminate(terminal: Terminal, nextBlockKind: BlockKind | null): void { + terminate(terminal: Terminal, nextBlockKind: BlockKind | null): BlockId { const {id: blockId, kind, instructions} = this.#current; this.#completed.set(blockId, { kind, @@ -390,6 +417,7 @@ export default class HIRBuilder { const nextId = this.#env.nextBlockId; this.#current = newBlock(nextId, nextBlockKind); } + return blockId; } /* @@ -746,6 +774,11 @@ function getReversePostorderedBlocks(func: HIR): HIR['blocks'] { * (eg bb2 then bb1), we ensure that they get reversed back to the correct order. */ const block = func.blocks.get(blockId)!; + CompilerError.invariant(block != null, { + reason: '[HIRBuilder] Unexpected null block', + description: `expected block ${blockId} to exist`, + loc: GeneratedSource, + }); const successors = [...eachTerminalSuccessor(block.terminal)].reverse(); const fallthrough = terminalFallthrough(block.terminal); diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/MergeConsecutiveBlocks.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/MergeConsecutiveBlocks.ts index ea132b772aa44..881e4e93ffcdb 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/MergeConsecutiveBlocks.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/MergeConsecutiveBlocks.ts @@ -12,6 +12,7 @@ import { GeneratedSource, HIRFunction, Instruction, + Place, } from './HIR'; import {markPredecessors} from './HIRBuilder'; import {terminalFallthrough, terminalHasFallthrough} from './visitors'; @@ -80,20 +81,22 @@ export function mergeConsecutiveBlocks(fn: HIRFunction): void { suggestions: null, }); const operand = Array.from(phi.operands.values())[0]!; + const lvalue: Place = { + kind: 'Identifier', + identifier: phi.place.identifier, + effect: Effect.ConditionallyMutate, + reactive: false, + loc: GeneratedSource, + }; const instr: Instruction = { id: predecessor.terminal.id, - lvalue: { - kind: 'Identifier', - identifier: phi.place.identifier, - effect: Effect.ConditionallyMutate, - reactive: false, - loc: GeneratedSource, - }, + lvalue: {...lvalue}, value: { kind: 'LoadLocal', place: {...operand}, loc: GeneratedSource, }, + effects: [{kind: 'Alias', from: {...operand}, into: {...lvalue}}], loc: GeneratedSource, }; predecessor.instructions.push(instr); @@ -104,6 +107,17 @@ export function mergeConsecutiveBlocks(fn: HIRFunction): void { merged.merge(block.id, predecessorId); fn.body.blocks.delete(block.id); } + for (const [, block] of fn.body.blocks) { + for (const phi of block.phis) { + for (const [predecessorId, operand] of phi.operands) { + const mapped = merged.get(predecessorId); + if (mapped !== predecessorId) { + phi.operands.delete(predecessorId); + phi.operands.set(mapped, operand); + } + } + } + } markPredecessors(fn.body); for (const [, {terminal}] of fn.body.blocks) { if (terminalHasFallthrough(terminal)) { diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/ObjectShape.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/ObjectShape.ts index 03f4120149b0e..2c626243e7075 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/ObjectShape.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/ObjectShape.ts @@ -6,14 +6,30 @@ */ import {CompilerError} from '../CompilerError'; -import {Effect, ValueKind, ValueReason} from './HIR'; +import {AliasingEffect, AliasingSignature} from '../Inference/AliasingEffects'; +import {assertExhaustive} from '../Utils/utils'; +import { + Effect, + GeneratedSource, + Hole, + makeDeclarationId, + makeIdentifierId, + makeInstructionId, + Place, + SourceLocation, + SpreadPattern, + ValueKind, + ValueReason, +} from './HIR'; import { BuiltInType, FunctionType, + makeType, ObjectType, PolyType, PrimitiveType, } from './Types'; +import {AliasingEffectConfig, AliasingSignatureConfig} from './TypeSchema'; /* * This file exports types and defaults for JavaScript object shapes. These are @@ -42,13 +58,20 @@ function createAnonId(): string { export function addFunction( registry: ShapeRegistry, properties: Iterable<[string, BuiltInType | PolyType]>, - fn: Omit, + fn: Omit & { + aliasing?: AliasingSignatureConfig | null | undefined; + }, id: string | null = null, isConstructor: boolean = false, ): FunctionType { const shapeId = id ?? createAnonId(); + const aliasing = + fn.aliasing != null + ? parseAliasingSignatureConfig(fn.aliasing, '', GeneratedSource) + : null; addShape(registry, shapeId, properties, { ...fn, + aliasing, hookKind: null, }); return { @@ -66,11 +89,18 @@ export function addFunction( */ export function addHook( registry: ShapeRegistry, - fn: FunctionSignature & {hookKind: HookKind}, + fn: Omit & { + hookKind: HookKind; + aliasing?: AliasingSignatureConfig | null | undefined; + }, id: string | null = null, ): FunctionType { const shapeId = id ?? createAnonId(); - addShape(registry, shapeId, [], fn); + const aliasing = + fn.aliasing != null + ? parseAliasingSignatureConfig(fn.aliasing, '', GeneratedSource) + : null; + addShape(registry, shapeId, [], {...fn, aliasing}); return { kind: 'Function', return: fn.returnType, @@ -79,6 +109,130 @@ export function addHook( }; } +function parseAliasingSignatureConfig( + typeConfig: AliasingSignatureConfig, + moduleName: string, + loc: SourceLocation, +): AliasingSignature { + const lifetimes = new Map(); + function define(temp: string): Place { + CompilerError.invariant(!lifetimes.has(temp), { + reason: `Invalid type configuration for module`, + description: `Expected aliasing signature to have unique names for receiver, params, rest, returns, and temporaries in module '${moduleName}'`, + loc, + }); + const place = signatureArgument(lifetimes.size); + lifetimes.set(temp, place); + return place; + } + function lookup(temp: string): Place { + const place = lifetimes.get(temp); + CompilerError.invariant(place != null, { + reason: `Invalid type configuration for module`, + description: `Expected aliasing signature effects to reference known names from receiver/params/rest/returns/temporaries, but '${temp}' is not a known name in '${moduleName}'`, + loc, + }); + return place; + } + const receiver = define(typeConfig.receiver); + const params = typeConfig.params.map(define); + const rest = typeConfig.rest != null ? define(typeConfig.rest) : null; + const returns = define(typeConfig.returns); + const temporaries = typeConfig.temporaries.map(define); + const effects = typeConfig.effects.map( + (effect: AliasingEffectConfig): AliasingEffect => { + switch (effect.kind) { + case 'ImmutableCapture': + case 'CreateFrom': + case 'Capture': + case 'Alias': + case 'Assign': { + const from = lookup(effect.from); + const into = lookup(effect.into); + return { + kind: effect.kind, + from, + into, + }; + } + case 'Mutate': + case 'MutateTransitiveConditionally': { + const value = lookup(effect.value); + return {kind: effect.kind, value}; + } + case 'Create': { + const into = lookup(effect.into); + return { + kind: 'Create', + into, + reason: effect.reason, + value: effect.value, + }; + } + case 'Freeze': { + const value = lookup(effect.value); + return { + kind: 'Freeze', + value, + reason: effect.reason, + }; + } + case 'Impure': { + const place = lookup(effect.place); + return { + kind: 'Impure', + place, + error: CompilerError.throwTodo({ + reason: 'Support impure effect declarations', + loc: GeneratedSource, + }), + }; + } + case 'Apply': { + const receiver = lookup(effect.receiver); + const fn = lookup(effect.function); + const args: Array = effect.args.map( + arg => { + if (typeof arg === 'string') { + return lookup(arg); + } else if (arg.kind === 'Spread') { + return {kind: 'Spread', place: lookup(arg.place)}; + } else { + return arg; + } + }, + ); + const into = lookup(effect.into); + return { + kind: 'Apply', + receiver, + function: fn, + mutatesFunction: effect.mutatesFunction, + args, + into, + loc, + signature: null, + }; + } + default: { + assertExhaustive( + effect, + `Unexpected effect kind '${(effect as any).kind}'`, + ); + } + } + }, + ); + return { + receiver: receiver.identifier.id, + params: params.map(p => p.identifier.id), + rest: rest != null ? rest.identifier.id : null, + returns: returns.identifier.id, + temporaries, + effects, + }; +} + /* * Add an object to an existing ShapeRegistry. * @@ -131,6 +285,7 @@ export type HookKind = | 'useCallback' | 'useTransition' | 'useImperativeHandle' + | 'useEffectEvent' | 'Custom'; /* @@ -177,8 +332,11 @@ export type FunctionSignature = { mutableOnlyIfOperandsAreMutable?: boolean; impure?: boolean; + knownIncompatible?: string | null | undefined; canonicalName?: string; + + aliasing?: AliasingSignature | null | undefined; }; /* @@ -226,6 +384,9 @@ export const BuiltInUseTransitionId = 'BuiltInUseTransition'; export const BuiltInStartTransitionId = 'BuiltInStartTransition'; export const BuiltInFireId = 'BuiltInFire'; export const BuiltInFireFunctionId = 'BuiltInFireFunction'; +export const BuiltInUseEffectEventId = 'BuiltInUseEffectEvent'; +export const BuiltinEffectEventId = 'BuiltInEffectEventFunction'; +export const BuiltInAutodepsId = 'BuiltInAutoDepsId'; // See getReanimatedModuleType() in Globals.ts — this is part of supporting Reanimated's ref-like types export const ReanimatedSharedValueId = 'ReanimatedSharedValueId'; @@ -302,6 +463,30 @@ addObject(BUILTIN_SHAPES, BuiltInArrayId, [ returnType: PRIMITIVE_TYPE, calleeEffect: Effect.Store, returnValueKind: ValueKind.Primitive, + aliasing: { + receiver: '@receiver', + params: [], + rest: '@rest', + returns: '@returns', + temporaries: [], + effects: [ + // Push directly mutates the array itself + {kind: 'Mutate', value: '@receiver'}, + // The arguments are captured into the array + { + kind: 'Capture', + from: '@rest', + into: '@receiver', + }, + // Returns the new length, a primitive + { + kind: 'Create', + into: '@returns', + value: ValueKind.Primitive, + reason: ValueReason.KnownReturnSignature, + }, + ], + }, }), ], [ @@ -332,6 +517,60 @@ addObject(BUILTIN_SHAPES, BuiltInArrayId, [ returnValueKind: ValueKind.Mutable, noAlias: true, mutableOnlyIfOperandsAreMutable: true, + aliasing: { + receiver: '@receiver', + params: ['@callback'], + rest: null, + returns: '@returns', + temporaries: [ + // Temporary representing captured items of the receiver + '@item', + // Temporary representing the result of the callback + '@callbackReturn', + /* + * Undefined `this` arg to the callback. Note the signature does not + * support passing an explicit thisArg second param + */ + '@thisArg', + ], + effects: [ + // Map creates a new mutable array + { + kind: 'Create', + into: '@returns', + value: ValueKind.Mutable, + reason: ValueReason.KnownReturnSignature, + }, + // The first arg to the callback is an item extracted from the receiver array + { + kind: 'CreateFrom', + from: '@receiver', + into: '@item', + }, + // The undefined this for the callback + { + kind: 'Create', + into: '@thisArg', + value: ValueKind.Primitive, + reason: ValueReason.KnownReturnSignature, + }, + // calls the callback, returning the result into a temporary + { + kind: 'Apply', + receiver: '@thisArg', + args: ['@item', {kind: 'Hole'}, '@receiver'], + function: '@callback', + into: '@callbackReturn', + mutatesFunction: false, + }, + // captures the result of the callback into the return array + { + kind: 'Capture', + from: '@callbackReturn', + into: '@returns', + }, + ], + }, }), ], [ @@ -479,6 +718,32 @@ addObject(BUILTIN_SHAPES, BuiltInSetId, [ calleeEffect: Effect.Store, // returnValueKind is technically dependent on the ValueKind of the set itself returnValueKind: ValueKind.Mutable, + aliasing: { + receiver: '@receiver', + params: [], + rest: '@rest', + returns: '@returns', + temporaries: [], + effects: [ + // Set.add returns the receiver Set + { + kind: 'Assign', + from: '@receiver', + into: '@returns', + }, + // Set.add mutates the set itself + { + kind: 'Mutate', + value: '@receiver', + }, + // Captures the rest params into the set + { + kind: 'Capture', + from: '@rest', + into: '@receiver', + }, + ], + }, }), ], [ @@ -948,6 +1213,21 @@ addObject(BUILTIN_SHAPES, BuiltInRefValueId, [ ['*', {kind: 'Object', shapeId: BuiltInRefValueId}], ]); +addObject(BUILTIN_SHAPES, ReanimatedSharedValueId, []); + +addFunction( + BUILTIN_SHAPES, + [], + { + positionalParams: [], + restParam: Effect.ConditionallyMutate, + returnType: {kind: 'Poly'}, + calleeEffect: Effect.ConditionallyMutate, + returnValueKind: ValueKind.Mutable, + }, + BuiltinEffectEventId, +); + /** * MixedReadOnly = * | primitive @@ -1166,6 +1446,53 @@ export const DefaultNonmutatingHook = addHook( calleeEffect: Effect.Read, hookKind: 'Custom', returnValueKind: ValueKind.Frozen, + aliasing: { + receiver: '@receiver', + params: [], + rest: '@rest', + returns: '@returns', + temporaries: [], + effects: [ + // Freeze the arguments + { + kind: 'Freeze', + value: '@rest', + reason: ValueReason.HookCaptured, + }, + // Returns a frozen value + { + kind: 'Create', + into: '@returns', + value: ValueKind.Frozen, + reason: ValueReason.HookReturn, + }, + // May alias any arguments into the return + { + kind: 'Alias', + from: '@rest', + into: '@returns', + }, + ], + }, }, 'DefaultNonmutatingHook', ); + +export function signatureArgument(id: number): Place { + const place: Place = { + kind: 'Identifier', + effect: Effect.Unknown, + loc: GeneratedSource, + reactive: false, + identifier: { + declarationId: makeDeclarationId(id), + id: makeIdentifierId(id), + loc: GeneratedSource, + mutableRange: {start: makeInstructionId(0), end: makeInstructionId(0)}, + name: null, + scope: null, + type: makeType(), + }, + }; + return place; +} 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 c8182c9e72a7c..a8fb837262e74 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/PrintHIR.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/PrintHIR.ts @@ -5,7 +5,6 @@ * LICENSE file in the root directory of this source tree. */ -import generate from '@babel/generator'; import {CompilerError} from '../CompilerError'; import {printReactiveScopeSummary} from '../ReactiveScopes/PrintReactiveFunction'; import DisjointSet from '../Utils/DisjointSet'; @@ -35,6 +34,7 @@ import type { Type, } from './HIR'; import {GotoVariant, InstructionKind} from './HIR'; +import {AliasingEffect, AliasingSignature} from '../Inference/AliasingEffects'; export type Options = { indent: number; @@ -53,6 +53,8 @@ export function printFunction(fn: HIRFunction): string { let definition = ''; if (fn.id !== null) { definition += fn.id; + } else { + definition += '<>'; } if (fn.params.length !== 0) { definition += @@ -67,13 +69,13 @@ export function printFunction(fn: HIRFunction): string { }) .join(', ') + ')'; + } else { + definition += '()'; } - if (definition.length !== 0) { - output.push(definition); - } - output.push(printType(fn.returnType)); - output.push(printHIR(fn.body)); + definition += `: ${printPlace(fn.returns)}`; + output.push(definition); output.push(...fn.directives); + output.push(printHIR(fn.body)); return output.join('\n'); } @@ -151,7 +153,10 @@ export function printMixedHIR( export function printInstruction(instr: ReactiveInstruction): string { const id = `[${instr.id}]`; - const value = printInstructionValue(instr.value); + let value = printInstructionValue(instr.value); + if (instr.effects != null) { + value += `\n ${instr.effects.map(printAliasingEffect).join('\n ')}`; + } if (instr.lvalue !== null) { return `${id} ${printPlace(instr.lvalue)} = ${value}`; @@ -210,9 +215,12 @@ export function printTerminal(terminal: Terminal): Array | string { break; } case 'return': { - value = `[${terminal.id}] Return${ + value = `[${terminal.id}] Return ${terminal.returnVariant}${ terminal.value != null ? ' ' + printPlace(terminal.value) : '' }`; + if (terminal.effects != null) { + value += `\n ${terminal.effects.map(printAliasingEffect).join('\n ')}`; + } break; } case 'goto': { @@ -281,6 +289,9 @@ export function printTerminal(terminal: Terminal): Array | string { } case 'maybe-throw': { value = `[${terminal.id}] MaybeThrow continuation=bb${terminal.continuation} handler=bb${terminal.handler}`; + if (terminal.effects != null) { + value += `\n ${terminal.effects.map(printAliasingEffect).join('\n ')}`; + } break; } case 'scope': { @@ -454,7 +465,7 @@ export function printInstructionValue(instrValue: ReactiveValue): string { break; } case 'UnsupportedNode': { - value = `UnsupportedNode(${generate(instrValue.node).code})`; + value = `UnsupportedNode ${instrValue.node.type}`; break; } case 'LoadLocal': { @@ -543,20 +554,11 @@ export function printInstructionValue(instrValue: ReactiveValue): string { const context = instrValue.loweredFunc.func.context .map(dep => printPlace(dep)) .join(','); - const effects = - instrValue.loweredFunc.func.effects - ?.map(effect => { - if (effect.kind === 'ContextMutation') { - return `ContextMutation places=[${[...effect.places] - .map(place => printPlace(place)) - .join(', ')}] effect=${effect.effect}`; - } else { - return `GlobalMutation`; - } - }) - .join(', ') ?? ''; - const type = printType(instrValue.loweredFunc.func.returnType).trim(); - value = `${kind} ${name} @context[${context}] @effects[${effects}]${type !== '' ? ` return${type}` : ''}:\n${fn}`; + const aliasingEffects = + instrValue.loweredFunc.func.aliasingEffects + ?.map(printAliasingEffect) + ?.join(', ') ?? ''; + value = `${kind} ${name} @context[${context}] @aliasingEffects=[${aliasingEffects}]\n${fn}`; break; } case 'TaggedTemplateExpression': { @@ -700,7 +702,7 @@ export function printInstructionValue(instrValue: ReactiveValue): string { break; } case 'FinishMemoize': { - value = `FinishMemoize decl=${printPlace(instrValue.decl)}`; + value = `FinishMemoize decl=${printPlace(instrValue.decl)}${instrValue.pruned ? ' pruned' : ''}`; break; } default: { @@ -878,7 +880,8 @@ export function printType(type: Type): string { if (type.kind === 'Object' && type.shapeId != null) { return `:T${type.kind}<${type.shapeId}>`; } else if (type.kind === 'Function' && type.shapeId != null) { - return `:T${type.kind}<${type.shapeId}>`; + const returnType = printType(type.return); + return `:T${type.kind}<${type.shapeId}>()${returnType !== '' ? `: ${returnType}` : ''}`; } else { return `:T${type.kind}`; } @@ -922,3 +925,110 @@ function getFunctionName( return defaultValue; } } + +export function printAliasingEffect(effect: AliasingEffect): string { + switch (effect.kind) { + case 'Assign': { + return `Assign ${printPlaceForAliasEffect(effect.into)} = ${printPlaceForAliasEffect(effect.from)}`; + } + case 'Alias': { + return `Alias ${printPlaceForAliasEffect(effect.into)} <- ${printPlaceForAliasEffect(effect.from)}`; + } + case 'MaybeAlias': { + return `MaybeAlias ${printPlaceForAliasEffect(effect.into)} <- ${printPlaceForAliasEffect(effect.from)}`; + } + case 'Capture': { + return `Capture ${printPlaceForAliasEffect(effect.into)} <- ${printPlaceForAliasEffect(effect.from)}`; + } + case 'ImmutableCapture': { + return `ImmutableCapture ${printPlaceForAliasEffect(effect.into)} <- ${printPlaceForAliasEffect(effect.from)}`; + } + case 'Create': { + return `Create ${printPlaceForAliasEffect(effect.into)} = ${effect.value}`; + } + case 'CreateFrom': { + return `Create ${printPlaceForAliasEffect(effect.into)} = kindOf(${printPlaceForAliasEffect(effect.from)})`; + } + case 'CreateFunction': { + return `Function ${printPlaceForAliasEffect(effect.into)} = Function captures=[${effect.captures.map(printPlaceForAliasEffect).join(', ')}]`; + } + case 'Apply': { + const receiverCallee = + effect.receiver.identifier.id === effect.function.identifier.id + ? printPlaceForAliasEffect(effect.receiver) + : `${printPlaceForAliasEffect(effect.receiver)}.${printPlaceForAliasEffect(effect.function)}`; + const args = effect.args + .map(arg => { + if (arg.kind === 'Identifier') { + return printPlaceForAliasEffect(arg); + } else if (arg.kind === 'Hole') { + return ' '; + } + return `...${printPlaceForAliasEffect(arg.place)}`; + }) + .join(', '); + let signature = ''; + if (effect.signature != null) { + if (effect.signature.aliasing != null) { + signature = printAliasingSignature(effect.signature.aliasing); + } else { + signature = JSON.stringify(effect.signature, null, 2); + } + } + return `Apply ${printPlaceForAliasEffect(effect.into)} = ${receiverCallee}(${args})${signature != '' ? '\n ' : ''}${signature}`; + } + case 'Freeze': { + return `Freeze ${printPlaceForAliasEffect(effect.value)} ${effect.reason}`; + } + case 'Mutate': + case 'MutateConditionally': + case 'MutateTransitive': + case 'MutateTransitiveConditionally': { + return `${effect.kind} ${printPlaceForAliasEffect(effect.value)}${effect.kind === 'Mutate' && effect.reason?.kind === 'AssignCurrentProperty' ? ' (assign `.current`)' : ''}`; + } + case 'MutateFrozen': { + return `MutateFrozen ${printPlaceForAliasEffect(effect.place)} reason=${JSON.stringify(effect.error.reason)}`; + } + case 'MutateGlobal': { + return `MutateGlobal ${printPlaceForAliasEffect(effect.place)} reason=${JSON.stringify(effect.error.reason)}`; + } + case 'Impure': { + return `Impure ${printPlaceForAliasEffect(effect.place)} reason=${JSON.stringify(effect.error.reason)}`; + } + case 'Render': { + return `Render ${printPlaceForAliasEffect(effect.place)}`; + } + default: { + assertExhaustive(effect, `Unexpected kind '${(effect as any).kind}'`); + } + } +} + +function printPlaceForAliasEffect(place: Place): string { + return printIdentifier(place.identifier); +} + +export function printAliasingSignature(signature: AliasingSignature): string { + const tokens: Array = ['function ']; + if (signature.temporaries.length !== 0) { + tokens.push('<'); + tokens.push( + signature.temporaries.map(temp => `$${temp.identifier.id}`).join(', '), + ); + tokens.push('>'); + } + tokens.push('('); + tokens.push('this=$' + String(signature.receiver)); + for (const param of signature.params) { + tokens.push(', $' + String(param)); + } + if (signature.rest != null) { + tokens.push(`, ...$${String(signature.rest)}`); + } + tokens.push('): '); + tokens.push('$' + String(signature.returns) + ':'); + for (const effect of signature.effects) { + tokens.push('\n ' + printAliasingEffect(effect)); + } + return tokens.join(''); +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/PropagateScopeDependenciesHIR.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/PropagateScopeDependenciesHIR.ts index 96b9e51710887..91b7712b881fc 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/PropagateScopeDependenciesHIR.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/PropagateScopeDependenciesHIR.ts @@ -316,6 +316,7 @@ function collectTemporariesSidemapImpl( ) { temporaries.set(lvalue.identifier.id, { identifier: value.place.identifier, + reactive: value.place.reactive, path: [], }); } @@ -369,11 +370,13 @@ function getProperty( if (resolvedDependency == null) { property = { identifier: object.identifier, + reactive: object.reactive, path: [{property: propertyName, optional}], }; } else { property = { identifier: resolvedDependency.identifier, + reactive: resolvedDependency.reactive, path: [...resolvedDependency.path, {property: propertyName, optional}], }; } @@ -532,6 +535,7 @@ export class DependencyCollectionContext { this.visitDependency( this.#temporaries.get(place.identifier.id) ?? { identifier: place.identifier, + reactive: place.reactive, path: [], }, ); @@ -596,6 +600,7 @@ export class DependencyCollectionContext { ) { maybeDependency = { identifier: maybeDependency.identifier, + reactive: maybeDependency.reactive, path: [], }; } @@ -617,7 +622,11 @@ export class DependencyCollectionContext { identifier => identifier.declarationId === place.identifier.declarationId, ) && - this.#checkValidDependency({identifier: place.identifier, path: []}) + this.#checkValidDependency({ + identifier: place.identifier, + reactive: place.reactive, + path: [], + }) ) { currentScope.reassignments.add(place.identifier); } diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/ScopeDependencyUtils.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/ScopeDependencyUtils.ts new file mode 100644 index 0000000000000..6e9ff08b86242 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/ScopeDependencyUtils.ts @@ -0,0 +1,285 @@ +import { + Place, + ReactiveScopeDependency, + Identifier, + makeInstructionId, + InstructionKind, + GeneratedSource, + BlockId, + makeTemporaryIdentifier, + Effect, + GotoVariant, + HIR, +} from './HIR'; +import {CompilerError} from '../CompilerError'; +import {Environment} from './Environment'; +import HIRBuilder from './HIRBuilder'; +import {lowerValueToTemporary} from './BuildHIR'; + +type DependencyInstructions = { + place: Place; + value: HIR; + exitBlockId: BlockId; +}; + +export function buildDependencyInstructions( + dep: ReactiveScopeDependency, + env: Environment, +): DependencyInstructions { + const builder = new HIRBuilder(env, { + entryBlockKind: 'value', + }); + let dependencyValue: Identifier; + if (dep.path.every(path => !path.optional)) { + dependencyValue = writeNonOptionalDependency(dep, env, builder); + } else { + dependencyValue = writeOptionalDependency(dep, builder, null); + } + + const exitBlockId = builder.terminate( + { + kind: 'unsupported', + loc: GeneratedSource, + id: makeInstructionId(0), + }, + null, + ); + return { + place: { + kind: 'Identifier', + identifier: dependencyValue, + effect: Effect.Freeze, + reactive: dep.reactive, + loc: GeneratedSource, + }, + value: builder.build(), + exitBlockId, + }; +} + +/** + * Write instructions for a simple dependency (without optional chains) + */ +function writeNonOptionalDependency( + dep: ReactiveScopeDependency, + env: Environment, + builder: HIRBuilder, +): Identifier { + const loc = dep.identifier.loc; + let curr: Identifier = makeTemporaryIdentifier(env.nextIdentifierId, loc); + builder.push({ + lvalue: { + identifier: curr, + kind: 'Identifier', + effect: Effect.Mutate, + reactive: dep.reactive, + loc, + }, + value: { + kind: 'LoadLocal', + place: { + identifier: dep.identifier, + kind: 'Identifier', + effect: Effect.Freeze, + reactive: dep.reactive, + loc, + }, + loc, + }, + id: makeInstructionId(1), + loc: loc, + effects: null, + }); + + /** + * Iteratively build up dependency instructions by reading from the last written + * instruction. + */ + for (const path of dep.path) { + const next = makeTemporaryIdentifier(env.nextIdentifierId, loc); + builder.push({ + lvalue: { + identifier: next, + kind: 'Identifier', + effect: Effect.Mutate, + reactive: dep.reactive, + loc, + }, + value: { + kind: 'PropertyLoad', + object: { + identifier: curr, + kind: 'Identifier', + effect: Effect.Freeze, + reactive: dep.reactive, + loc, + }, + property: path.property, + loc, + }, + id: makeInstructionId(1), + loc: loc, + effects: null, + }); + curr = next; + } + return curr; +} + +/** + * Write a dependency into optional blocks. + * + * e.g. `a.b?.c.d` is written to an optional block that tests `a.b` and + * conditionally evaluates `c.d`. + */ +function writeOptionalDependency( + dep: ReactiveScopeDependency, + builder: HIRBuilder, + parentAlternate: BlockId | null, +): Identifier { + const env = builder.environment; + /** + * Reserve an identifier which will be used to store the result of this + * dependency. + */ + const dependencyValue: Place = { + kind: 'Identifier', + identifier: makeTemporaryIdentifier(env.nextIdentifierId, GeneratedSource), + effect: Effect.Mutate, + reactive: dep.reactive, + loc: GeneratedSource, + }; + + /** + * Reserve a block which is the fallthrough (and transitive successor) of this + * optional chain. + */ + const continuationBlock = builder.reserve(builder.currentBlockKind()); + let alternate; + if (parentAlternate != null) { + alternate = parentAlternate; + } else { + /** + * If an outermost alternate block has not been reserved, write one + * + * $N = Primitive undefined + * $M = StoreLocal $OptionalResult = $N + * goto fallthrough + */ + alternate = builder.enter('value', () => { + const temp = lowerValueToTemporary(builder, { + kind: 'Primitive', + value: undefined, + loc: GeneratedSource, + }); + lowerValueToTemporary(builder, { + kind: 'StoreLocal', + lvalue: {kind: InstructionKind.Const, place: {...dependencyValue}}, + value: {...temp}, + type: null, + loc: GeneratedSource, + }); + return { + kind: 'goto', + variant: GotoVariant.Break, + block: continuationBlock.id, + id: makeInstructionId(0), + loc: GeneratedSource, + }; + }); + } + + // Reserve the consequent block, which is the successor of the test block + const consequent = builder.reserve('value'); + + let testIdentifier: Identifier | null = null; + const testBlock = builder.enter('value', () => { + const testDependency = { + ...dep, + path: dep.path.slice(0, dep.path.length - 1), + }; + const firstOptional = dep.path.findIndex(path => path.optional); + CompilerError.invariant(firstOptional !== -1, { + reason: + '[ScopeDependencyUtils] Internal invariant broken: expected optional path', + loc: dep.identifier.loc, + description: null, + suggestions: null, + }); + if (firstOptional === dep.path.length - 1) { + // Base case: the test block is simple + testIdentifier = writeNonOptionalDependency(testDependency, env, builder); + } else { + // Otherwise, the test block is a nested optional chain + testIdentifier = writeOptionalDependency( + testDependency, + builder, + alternate, + ); + } + + return { + kind: 'branch', + test: { + identifier: testIdentifier, + effect: Effect.Freeze, + kind: 'Identifier', + loc: GeneratedSource, + reactive: dep.reactive, + }, + consequent: consequent.id, + alternate, + id: makeInstructionId(0), + loc: GeneratedSource, + fallthrough: continuationBlock.id, + }; + }); + + builder.enterReserved(consequent, () => { + CompilerError.invariant(testIdentifier !== null, { + reason: 'Satisfy type checker', + description: null, + loc: null, + suggestions: null, + }); + + lowerValueToTemporary(builder, { + kind: 'StoreLocal', + lvalue: {kind: InstructionKind.Const, place: {...dependencyValue}}, + value: lowerValueToTemporary(builder, { + kind: 'PropertyLoad', + object: { + identifier: testIdentifier, + kind: 'Identifier', + effect: Effect.Freeze, + reactive: dep.reactive, + loc: GeneratedSource, + }, + property: dep.path.at(-1)!.property, + loc: GeneratedSource, + }), + type: null, + loc: GeneratedSource, + }); + return { + kind: 'goto', + variant: GotoVariant.Break, + block: continuationBlock.id, + id: makeInstructionId(0), + loc: GeneratedSource, + }; + }); + builder.terminateWithContinuation( + { + kind: 'optional', + optional: dep.path.at(-1)!.optional, + test: testBlock, + fallthrough: continuationBlock.id, + id: makeInstructionId(0), + loc: GeneratedSource, + }, + continuationBlock, + ); + + return dependencyValue.identifier; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/TypeSchema.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/TypeSchema.ts index 9aac2a264f60a..42c7d2d89dce1 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/TypeSchema.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/TypeSchema.ts @@ -8,7 +8,12 @@ import {isValidIdentifier} from '@babel/types'; import {z} from 'zod'; import {Effect, ValueKind} from '..'; -import {EffectSchema, ValueKindSchema} from './HIR'; +import { + EffectSchema, + ValueKindSchema, + ValueReason, + ValueReasonSchema, +} from './HIR'; export type ObjectPropertiesConfig = {[key: string]: TypeConfig}; export const ObjectPropertiesSchema: z.ZodType = z @@ -31,6 +36,209 @@ export const ObjectTypeSchema: z.ZodType = z.object({ properties: ObjectPropertiesSchema.nullable(), }); +export const LifetimeIdSchema = z.string().refine(id => id.startsWith('@'), { + message: "Placeholder names must start with '@'", +}); + +export type FreezeEffectConfig = { + kind: 'Freeze'; + value: string; + reason: ValueReason; +}; + +export const FreezeEffectSchema: z.ZodType = z.object({ + kind: z.literal('Freeze'), + value: LifetimeIdSchema, + reason: ValueReasonSchema, +}); + +export type MutateEffectConfig = { + kind: 'Mutate'; + value: string; +}; + +export const MutateEffectSchema: z.ZodType = z.object({ + kind: z.literal('Mutate'), + value: LifetimeIdSchema, +}); + +export type MutateTransitiveConditionallyConfig = { + kind: 'MutateTransitiveConditionally'; + value: string; +}; + +export const MutateTransitiveConditionallySchema: z.ZodType = + z.object({ + kind: z.literal('MutateTransitiveConditionally'), + value: LifetimeIdSchema, + }); + +export type CreateEffectConfig = { + kind: 'Create'; + into: string; + value: ValueKind; + reason: ValueReason; +}; + +export const CreateEffectSchema: z.ZodType = z.object({ + kind: z.literal('Create'), + into: LifetimeIdSchema, + value: ValueKindSchema, + reason: ValueReasonSchema, +}); + +export type AssignEffectConfig = { + kind: 'Assign'; + from: string; + into: string; +}; + +export const AssignEffectSchema: z.ZodType = z.object({ + kind: z.literal('Assign'), + from: LifetimeIdSchema, + into: LifetimeIdSchema, +}); + +export type AliasEffectConfig = { + kind: 'Alias'; + from: string; + into: string; +}; + +export const AliasEffectSchema: z.ZodType = z.object({ + kind: z.literal('Alias'), + from: LifetimeIdSchema, + into: LifetimeIdSchema, +}); + +export type ImmutableCaptureEffectConfig = { + kind: 'ImmutableCapture'; + from: string; + into: string; +}; + +export const ImmutableCaptureEffectSchema: z.ZodType = + z.object({ + kind: z.literal('ImmutableCapture'), + from: LifetimeIdSchema, + into: LifetimeIdSchema, + }); + +export type CaptureEffectConfig = { + kind: 'Capture'; + from: string; + into: string; +}; + +export const CaptureEffectSchema: z.ZodType = z.object({ + kind: z.literal('Capture'), + from: LifetimeIdSchema, + into: LifetimeIdSchema, +}); + +export type CreateFromEffectConfig = { + kind: 'CreateFrom'; + from: string; + into: string; +}; + +export const CreateFromEffectSchema: z.ZodType = + z.object({ + kind: z.literal('CreateFrom'), + from: LifetimeIdSchema, + into: LifetimeIdSchema, + }); + +export type ApplyArgConfig = + | string + | {kind: 'Spread'; place: string} + | {kind: 'Hole'}; + +export const ApplyArgSchema: z.ZodType = z.union([ + LifetimeIdSchema, + z.object({ + kind: z.literal('Spread'), + place: LifetimeIdSchema, + }), + z.object({ + kind: z.literal('Hole'), + }), +]); + +export type ApplyEffectConfig = { + kind: 'Apply'; + receiver: string; + function: string; + mutatesFunction: boolean; + args: Array; + into: string; +}; + +export const ApplyEffectSchema: z.ZodType = z.object({ + kind: z.literal('Apply'), + receiver: LifetimeIdSchema, + function: LifetimeIdSchema, + mutatesFunction: z.boolean(), + args: z.array(ApplyArgSchema), + into: LifetimeIdSchema, +}); + +export type ImpureEffectConfig = { + kind: 'Impure'; + place: string; +}; + +export const ImpureEffectSchema: z.ZodType = z.object({ + kind: z.literal('Impure'), + place: LifetimeIdSchema, +}); + +export type AliasingEffectConfig = + | FreezeEffectConfig + | CreateEffectConfig + | CreateFromEffectConfig + | AssignEffectConfig + | AliasEffectConfig + | CaptureEffectConfig + | ImmutableCaptureEffectConfig + | ImpureEffectConfig + | MutateEffectConfig + | MutateTransitiveConditionallyConfig + | ApplyEffectConfig; + +export const AliasingEffectSchema: z.ZodType = z.union([ + FreezeEffectSchema, + CreateEffectSchema, + CreateFromEffectSchema, + AssignEffectSchema, + AliasEffectSchema, + CaptureEffectSchema, + ImmutableCaptureEffectSchema, + ImpureEffectSchema, + MutateEffectSchema, + MutateTransitiveConditionallySchema, + ApplyEffectSchema, +]); + +export type AliasingSignatureConfig = { + receiver: string; + params: Array; + rest: string | null; + returns: string; + effects: Array; + temporaries: Array; +}; + +export const AliasingSignatureSchema: z.ZodType = + z.object({ + receiver: LifetimeIdSchema, + params: z.array(LifetimeIdSchema), + rest: LifetimeIdSchema.nullable(), + returns: LifetimeIdSchema, + effects: z.array(AliasingEffectSchema), + temporaries: z.array(LifetimeIdSchema), + }); + export type FunctionTypeConfig = { kind: 'function'; positionalParams: Array; @@ -42,6 +250,8 @@ export type FunctionTypeConfig = { mutableOnlyIfOperandsAreMutable?: boolean | null | undefined; impure?: boolean | null | undefined; canonicalName?: string | null | undefined; + aliasing?: AliasingSignatureConfig | null | undefined; + knownIncompatible?: string | null | undefined; }; export const FunctionTypeSchema: z.ZodType = z.object({ kind: z.literal('function'), @@ -54,6 +264,8 @@ export const FunctionTypeSchema: z.ZodType = z.object({ mutableOnlyIfOperandsAreMutable: z.boolean().nullable().optional(), impure: z.boolean().nullable().optional(), canonicalName: z.string().nullable().optional(), + aliasing: AliasingSignatureSchema.nullable().optional(), + knownIncompatible: z.string().nullable().optional(), }); export type HookTypeConfig = { @@ -63,6 +275,8 @@ export type HookTypeConfig = { returnType: TypeConfig; returnValueKind?: ValueKind | null | undefined; noAlias?: boolean | null | undefined; + aliasing?: AliasingSignatureConfig | null | undefined; + knownIncompatible?: string | null | undefined; }; export const HookTypeSchema: z.ZodType = z.object({ kind: z.literal('hook'), @@ -71,6 +285,8 @@ export const HookTypeSchema: z.ZodType = z.object({ returnType: z.lazy(() => TypeSchema), returnValueKind: ValueKindSchema.nullable().optional(), noAlias: z.boolean().nullable().optional(), + aliasing: AliasingSignatureSchema.nullable().optional(), + knownIncompatible: z.string().nullable().optional(), }); export type BuiltInTypeConfig = 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 49ff3c256e016..64702c8abcdb6 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/visitors.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/visitors.ts @@ -345,6 +345,51 @@ export function* eachPatternOperand(pattern: Pattern): Iterable { } } +export function* eachPatternItem( + pattern: Pattern, +): Iterable { + switch (pattern.kind) { + case 'ArrayPattern': { + for (const item of pattern.items) { + if (item.kind === 'Identifier') { + yield item; + } else if (item.kind === 'Spread') { + yield item; + } else if (item.kind === 'Hole') { + continue; + } else { + assertExhaustive( + item, + `Unexpected item kind \`${(item as any).kind}\``, + ); + } + } + break; + } + case 'ObjectPattern': { + for (const property of pattern.properties) { + if (property.kind === 'ObjectProperty') { + yield property.place; + } else if (property.kind === 'Spread') { + yield property; + } else { + assertExhaustive( + property, + `Unexpected item kind \`${(property as any).kind}\``, + ); + } + } + break; + } + default: { + assertExhaustive( + pattern, + `Unexpected pattern kind \`${(pattern as any).kind}\``, + ); + } + } +} + export function mapInstructionLValues( instr: Instruction, fn: (place: Place) => Place, @@ -732,9 +777,11 @@ export function mapTerminalSuccessors( case 'return': { return { kind: 'return', + returnVariant: terminal.returnVariant, loc: terminal.loc, value: terminal.value, id: makeInstructionId(0), + effects: terminal.effects, }; } case 'throw': { @@ -842,6 +889,7 @@ export function mapTerminalSuccessors( handler, id: makeInstructionId(0), loc: terminal.loc, + effects: terminal.effects, }; } case 'try': { diff --git a/compiler/packages/babel-plugin-react-compiler/src/Inference/AliasingEffects.ts b/compiler/packages/babel-plugin-react-compiler/src/Inference/AliasingEffects.ts new file mode 100644 index 0000000000000..7f30e25a5c060 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/Inference/AliasingEffects.ts @@ -0,0 +1,264 @@ +/** + * 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 {CompilerDiagnostic} from '../CompilerError'; +import { + FunctionExpression, + GeneratedSource, + Hole, + IdentifierId, + ObjectMethod, + Place, + SourceLocation, + SpreadPattern, + ValueKind, + ValueReason, +} from '../HIR'; +import {FunctionSignature} from '../HIR/ObjectShape'; +import {printSourceLocation} from '../HIR/PrintHIR'; + +/** + * `AliasingEffect` describes a set of "effects" that an instruction/terminal has on one or + * more values in a program. These effects include mutation of values, freezing values, + * tracking data flow between values, and other specialized cases. + */ +export type AliasingEffect = + /** + * Marks the given value and its direct aliases as frozen. + * + * Captured values are *not* considered frozen, because we cannot be sure that a previously + * captured value will still be captured at the point of the freeze. + * + * For example: + * const x = {}; + * const y = [x]; + * y.pop(); // y dosn't contain x anymore! + * freeze(y); + * mutate(x); // safe to mutate! + * + * The exception to this is FunctionExpressions - since it is impossible to change which + * value a function closes over[1] we can transitively freeze functions and their captures. + * + * [1] Except for `let` values that are reassigned and closed over by a function, but we + * handle this explicitly with StoreContext/LoadContext. + */ + | {kind: 'Freeze'; value: Place; reason: ValueReason} + /** + * Mutate the value and any direct aliases (not captures). Errors if the value is not mutable. + */ + | {kind: 'Mutate'; value: Place; reason?: MutationReason | null} + /** + * Mutate the value and any direct aliases (not captures), but only if the value is known mutable. + * This should be rare. + * + * TODO: this is only used for IteratorNext, but even then MutateTransitiveConditionally is more + * correct for iterators of unknown types. + */ + | {kind: 'MutateConditionally'; value: Place} + /** + * Mutate the value, any direct aliases, and any transitive captures. Errors if the value is not mutable. + */ + | {kind: 'MutateTransitive'; value: Place} + /** + * Mutates any of the value, its direct aliases, and its transitive captures that are mutable. + */ + | {kind: 'MutateTransitiveConditionally'; value: Place} + /** + * Records information flow from `from` to `into` in cases where local mutation of the destination + * will *not* mutate the source: + * + * - Capture a -> b and Mutate(b) X=> (does not imply) Mutate(a) + * - Capture a -> b and MutateTransitive(b) => (does imply) Mutate(a) + * + * Example: `array.push(item)`. Information from item is captured into array, but there is not a + * direct aliasing, and local mutations of array will not modify item. + */ + | {kind: 'Capture'; from: Place; into: Place} + /** + * Records information flow from `from` to `into` in cases where local mutation of the destination + * *will* mutate the source: + * + * - Alias a -> b and Mutate(b) => (does imply) Mutate(a) + * - Alias a -> b and MutateTransitive(b) => (does imply) Mutate(a) + * + * Example: `c = identity(a)`. We don't know what `identity()` returns so we can't use Assign. + * But we have to assume that it _could_ be returning its input, such that a local mutation of + * c could be mutating a. + */ + | {kind: 'Alias'; from: Place; into: Place} + + /** + * Indicates the potential for information flow from `from` to `into`. This is used for a specific + * case: functions with unknown signatures. If the compiler sees a call such as `foo(x)`, it has to + * consider several possibilities (which may depend on the arguments): + * - foo(x) returns a new mutable value that does not capture any information from x. + * - foo(x) returns a new mutable value that *does* capture information from x. + * - foo(x) returns x itself, ie foo is the identity function + * + * The same is true of functions that take multiple arguments: `cond(a, b, c)` could conditionally + * return b or c depending on the value of a. + * + * To represent this case, MaybeAlias represents the fact that an aliasing relationship could exist. + * Any mutations that flow through this relationship automatically become conditional. + */ + | {kind: 'MaybeAlias'; from: Place; into: Place} + + /** + * Records direct assignment: `into = from`. + */ + | {kind: 'Assign'; from: Place; into: Place} + /** + * Creates a value of the given type at the given place + */ + | {kind: 'Create'; into: Place; value: ValueKind; reason: ValueReason} + /** + * Creates a new value with the same kind as the starting value. + */ + | {kind: 'CreateFrom'; from: Place; into: Place} + /** + * Immutable data flow, used for escape analysis. Does not influence mutable range analysis: + */ + | {kind: 'ImmutableCapture'; from: Place; into: Place} + /** + * Calls the function at the given place with the given arguments either captured or aliased, + * and captures/aliases the result into the given place. + */ + | { + kind: 'Apply'; + receiver: Place; + function: Place; + mutatesFunction: boolean; + args: Array; + into: Place; + signature: FunctionSignature | null; + loc: SourceLocation; + } + /** + * Constructs a function value with the given captures. The mutability of the function + * will be determined by the mutability of the capture values when evaluated. + */ + | { + kind: 'CreateFunction'; + captures: Array; + function: FunctionExpression | ObjectMethod; + into: Place; + } + /** + * Mutation of a value known to be immutable + */ + | {kind: 'MutateFrozen'; place: Place; error: CompilerDiagnostic} + /** + * Mutation of a global + */ + | { + kind: 'MutateGlobal'; + place: Place; + error: CompilerDiagnostic; + } + /** + * Indicates a side-effect that is not safe during render + */ + | {kind: 'Impure'; place: Place; error: CompilerDiagnostic} + /** + * Indicates that a given place is accessed during render. Used to distingush + * hook arguments that are known to be called immediately vs those used for + * event handlers/effects, and for JSX values known to be called during render + * (tags, children) vs those that may be events/effect (other props). + */ + | { + kind: 'Render'; + place: Place; + }; + +export type MutationReason = {kind: 'AssignCurrentProperty'}; + +export function hashEffect(effect: AliasingEffect): string { + switch (effect.kind) { + case 'Apply': { + return [ + effect.kind, + effect.receiver.identifier.id, + effect.function.identifier.id, + effect.mutatesFunction, + effect.args + .map(a => { + if (a.kind === 'Hole') { + return ''; + } else if (a.kind === 'Identifier') { + return a.identifier.id; + } else { + return `...${a.place.identifier.id}`; + } + }) + .join(','), + effect.into.identifier.id, + ].join(':'); + } + case 'CreateFrom': + case 'ImmutableCapture': + case 'Assign': + case 'Alias': + case 'Capture': + case 'MaybeAlias': { + return [ + effect.kind, + effect.from.identifier.id, + effect.into.identifier.id, + ].join(':'); + } + case 'Create': { + return [ + effect.kind, + effect.into.identifier.id, + effect.value, + effect.reason, + ].join(':'); + } + case 'Freeze': { + return [effect.kind, effect.value.identifier.id, effect.reason].join(':'); + } + case 'Impure': + case 'Render': { + return [effect.kind, effect.place.identifier.id].join(':'); + } + case 'MutateFrozen': + case 'MutateGlobal': { + return [ + effect.kind, + effect.place.identifier.id, + effect.error.severity, + effect.error.reason, + effect.error.description, + printSourceLocation(effect.error.primaryLocation() ?? GeneratedSource), + ].join(':'); + } + case 'Mutate': + case 'MutateConditionally': + case 'MutateTransitive': + case 'MutateTransitiveConditionally': { + return [effect.kind, effect.value.identifier.id].join(':'); + } + case 'CreateFunction': { + return [ + effect.kind, + effect.into.identifier.id, + // return places are a unique way to identify functions themselves + effect.function.loweredFunc.func.returns.identifier.id, + effect.captures.map(p => p.identifier.id).join(','), + ].join(':'); + } + } +} + +export type AliasingSignature = { + receiver: IdentifierId; + params: Array; + rest: IdentifierId | null; + returns: IdentifierId; + effects: Array; + temporaries: Array; +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/Inference/AnalyseFunctions.ts b/compiler/packages/babel-plugin-react-compiler/src/Inference/AnalyseFunctions.ts index a439b4cd01232..77a2bdcde596a 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Inference/AnalyseFunctions.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Inference/AnalyseFunctions.ts @@ -6,19 +6,13 @@ */ import {CompilerError} from '../CompilerError'; -import { - Effect, - HIRFunction, - Identifier, - LoweredFunction, - isRefOrRefValue, - makeInstructionId, -} from '../HIR'; +import {Effect, HIRFunction, IdentifierId, makeInstructionId} from '../HIR'; import {deadCodeElimination} from '../Optimization'; import {inferReactiveScopeVariables} from '../ReactiveScopes'; import {rewriteInstructionKindsBasedOnReassignment} from '../SSA'; -import {inferMutableRanges} from './InferMutableRanges'; -import inferReferenceEffects from './InferReferenceEffects'; +import {assertExhaustive} from '../Utils/utils'; +import {inferMutationAliasingEffects} from './InferMutationAliasingEffects'; +import {inferMutationAliasingRanges} from './InferMutationAliasingRanges'; export default function analyseFunctions(func: HIRFunction): void { for (const [_, block] of func.body.blocks) { @@ -26,15 +20,22 @@ export default function analyseFunctions(func: HIRFunction): void { switch (instr.value.kind) { case 'ObjectMethod': case 'FunctionExpression': { - lower(instr.value.loweredFunc.func); - infer(instr.value.loweredFunc); + lowerWithMutationAliasing(instr.value.loweredFunc.func); /** * Reset mutable range for outer inferReferenceEffects */ for (const operand of instr.value.loweredFunc.func.context) { - operand.identifier.mutableRange.start = makeInstructionId(0); - operand.identifier.mutableRange.end = makeInstructionId(0); + /** + * NOTE: inferReactiveScopeVariables makes identifiers in the scope + * point to the *same* mutableRange instance. Resetting start/end + * here is insufficient, because a later mutation of the range + * for any one identifier could affect the range for other identifiers. + */ + operand.identifier.mutableRange = { + start: makeInstructionId(0), + end: makeInstructionId(0), + }; operand.identifier.scope = null; } break; @@ -44,57 +45,83 @@ export default function analyseFunctions(func: HIRFunction): void { } } -function lower(func: HIRFunction): void { - analyseFunctions(func); - inferReferenceEffects(func, {isFunctionExpression: true}); - deadCodeElimination(func); - inferMutableRanges(func); - rewriteInstructionKindsBasedOnReassignment(func); - inferReactiveScopeVariables(func); - func.env.logger?.debugLogIRs?.({ - kind: 'hir', - name: 'AnalyseFunction (inner)', - value: func, - }); -} +function lowerWithMutationAliasing(fn: HIRFunction): void { + /** + * Phase 1: similar to lower(), but using the new mutation/aliasing inference + */ + analyseFunctions(fn); + inferMutationAliasingEffects(fn, {isFunctionExpression: true}); + deadCodeElimination(fn); + const functionEffects = inferMutationAliasingRanges(fn, { + isFunctionExpression: true, + }).unwrap(); + rewriteInstructionKindsBasedOnReassignment(fn); + inferReactiveScopeVariables(fn); + fn.aliasingEffects = functionEffects; -function infer(loweredFunc: LoweredFunction): void { - for (const operand of loweredFunc.func.context) { - const identifier = operand.identifier; - CompilerError.invariant(operand.effect === Effect.Unknown, { - reason: - '[AnalyseFunctions] Expected Function context effects to not have been set', - loc: operand.loc, - }); - if (isRefOrRefValue(identifier)) { - /* - * TODO: this is a hack to ensure we treat functions which reference refs - * as having a capture and therefore being considered mutable. this ensures - * the function gets a mutable range which accounts for anywhere that it - * could be called, and allows us to help ensure it isn't called during - * render - */ - operand.effect = Effect.Capture; - } else if (isMutatedOrReassigned(identifier)) { - /** - * Reflects direct reassignments, PropertyStores, and ConditionallyMutate - * (directly or through maybe-aliases) - */ + /** + * Phase 2: populate the Effect of each context variable to use in inferring + * the outer function. For example, InferMutationAliasingEffects uses context variable + * effects to decide if the function may be mutable or not. + */ + const capturedOrMutated = new Set(); + for (const effect of functionEffects) { + switch (effect.kind) { + case 'Assign': + case 'Alias': + case 'Capture': + case 'CreateFrom': + case 'MaybeAlias': { + capturedOrMutated.add(effect.from.identifier.id); + break; + } + case 'Apply': { + CompilerError.invariant(false, { + reason: `[AnalyzeFunctions] Expected Apply effects to be replaced with more precise effects`, + loc: effect.function.loc, + }); + } + case 'Mutate': + case 'MutateConditionally': + case 'MutateTransitive': + case 'MutateTransitiveConditionally': { + capturedOrMutated.add(effect.value.identifier.id); + break; + } + case 'Impure': + case 'Render': + case 'MutateFrozen': + case 'MutateGlobal': + case 'CreateFunction': + case 'Create': + case 'Freeze': + case 'ImmutableCapture': { + // no-op + break; + } + default: { + assertExhaustive( + effect, + `Unexpected effect kind ${(effect as any).kind}`, + ); + } + } + } + + for (const operand of fn.context) { + if ( + capturedOrMutated.has(operand.identifier.id) || + operand.effect === Effect.Capture + ) { operand.effect = Effect.Capture; } else { operand.effect = Effect.Read; } } -} -function isMutatedOrReassigned(id: Identifier): boolean { - /* - * This check checks for mutation and reassingnment, so the usual check for - * mutation (ie, `mutableRange.end - mutableRange.start > 1`) isn't quite - * enough. - * - * We need to track re-assignments in context refs as we need to reflect the - * re-assignment back to the captured refs. - */ - return id.mutableRange.end > id.mutableRange.start; + fn.env.logger?.debugLogIRs?.({ + kind: 'hir', + name: 'AnalyseFunction (inner)', + value: fn, + }); } 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 8d123845c3739..412efcfe7aede 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Inference/DropManualMemoization.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Inference/DropManualMemoization.ts @@ -5,7 +5,13 @@ * LICENSE file in the root directory of this source tree. */ -import {CompilerError, SourceLocation} from '..'; +import { + CompilerDiagnostic, + CompilerError, + ErrorSeverity, + SourceLocation, +} from '..'; +import {ErrorCategory} from '../CompilerError'; import { CallExpression, Effect, @@ -30,6 +36,7 @@ import { makeInstructionId, } from '../HIR'; import {createTemporaryPlace, markInstructionIds} from '../HIR/HIRBuilder'; +import {Result} from '../Utils/Result'; type ManualMemoCallee = { kind: 'useMemo' | 'useCallback'; @@ -197,6 +204,7 @@ function makeManualMemoizationMarkers( deps: depsList, loc: fnExpr.loc, }, + effects: null, loc: fnExpr.loc, }, { @@ -208,6 +216,7 @@ function makeManualMemoizationMarkers( decl: {...memoDecl}, loc: fnExpr.loc, }, + effects: null, loc: fnExpr.loc, }, ]; @@ -281,26 +290,45 @@ function extractManualMemoizationArgs( instr: TInstruction | TInstruction, kind: 'useCallback' | 'useMemo', sidemap: IdentifierSidemap, + errors: CompilerError, ): { - fnPlace: Place; + fnPlace: Place | null; depsList: Array | null; } { const [fnPlace, depsListPlace] = instr.value.args as Array< Place | SpreadPattern | undefined >; if (fnPlace == null) { - CompilerError.throwInvalidReact({ - reason: `Expected a callback function to be passed to ${kind}`, - loc: instr.value.loc, - suggestions: null, - }); + errors.pushDiagnostic( + CompilerDiagnostic.create({ + category: ErrorCategory.UseMemo, + severity: ErrorSeverity.InvalidReact, + reason: `Expected a callback function to be passed to ${kind}`, + description: `Expected a callback function to be passed to ${kind}`, + suggestions: null, + }).withDetail({ + kind: 'error', + loc: instr.value.loc, + message: `Expected a callback function to be passed to ${kind}`, + }), + ); + return {fnPlace: null, depsList: null}; } if (fnPlace.kind === 'Spread' || depsListPlace?.kind === 'Spread') { - CompilerError.throwInvalidReact({ - reason: `Unexpected spread argument to ${kind}`, - loc: instr.value.loc, - suggestions: null, - }); + errors.pushDiagnostic( + CompilerDiagnostic.create({ + category: ErrorCategory.UseMemo, + severity: ErrorSeverity.InvalidReact, + reason: `Unexpected spread argument to ${kind}`, + description: `Unexpected spread argument to ${kind}`, + suggestions: null, + }).withDetail({ + kind: 'error', + loc: instr.value.loc, + message: `Unexpected spread argument to ${kind}`, + }), + ); + return {fnPlace: null, depsList: null}; } let depsList: Array | null = null; if (depsListPlace != null) { @@ -308,23 +336,42 @@ function extractManualMemoizationArgs( depsListPlace.identifier.id, ); if (maybeDepsList == null) { - CompilerError.throwInvalidReact({ - reason: `Expected the dependency list for ${kind} to be an array literal`, - suggestions: null, - loc: depsListPlace.loc, - }); + errors.pushDiagnostic( + CompilerDiagnostic.create({ + category: ErrorCategory.UseMemo, + severity: ErrorSeverity.InvalidReact, + reason: `Expected the dependency list for ${kind} to be an array literal`, + description: `Expected the dependency list for ${kind} to be an array literal`, + suggestions: null, + }).withDetail({ + kind: 'error', + loc: depsListPlace.loc, + message: `Expected the dependency list for ${kind} to be an array literal`, + }), + ); + return {fnPlace, depsList: null}; } - depsList = maybeDepsList.map(dep => { + depsList = []; + for (const dep of maybeDepsList) { const maybeDep = sidemap.maybeDeps.get(dep.identifier.id); if (maybeDep == null) { - CompilerError.throwInvalidReact({ - reason: `Expected the dependency list to be an array of simple expressions (e.g. \`x\`, \`x.y.z\`, \`x?.y?.z\`)`, - suggestions: null, - loc: dep.loc, - }); + errors.pushDiagnostic( + CompilerDiagnostic.create({ + category: ErrorCategory.UseMemo, + severity: ErrorSeverity.InvalidReact, + reason: `Expected the dependency list to be an array of simple expressions (e.g. \`x\`, \`x.y.z\`, \`x?.y?.z\`)`, + description: `Expected the dependency list to be an array of simple expressions (e.g. \`x\`, \`x.y.z\`, \`x?.y?.z\`)`, + suggestions: null, + }).withDetail({ + kind: 'error', + loc: dep.loc, + message: `Expected the dependency list to be an array of simple expressions (e.g. \`x\`, \`x.y.z\`, \`x?.y?.z\`)`, + }), + ); + } else { + depsList.push(maybeDep); } - return maybeDep; - }); + } } return { fnPlace, @@ -339,8 +386,14 @@ function extractManualMemoizationArgs( * rely on type inference to find useMemo/useCallback invocations, and instead does basic tracking * of globals and property loads to find both direct calls as well as usage via the React namespace, * eg `React.useMemo()`. + * + * This pass also validates that useMemo callbacks return a value (not void), ensuring that useMemo + * is only used for memoizing values and not for running arbitrary side effects. */ -export function dropManualMemoization(func: HIRFunction): void { +export function dropManualMemoization( + func: HIRFunction, +): Result { + const errors = new CompilerError(); const isValidationEnabled = func.env.config.validatePreserveExistingMemoizationGuarantees || func.env.config.validateNoSetStateInRender || @@ -387,7 +440,48 @@ export function dropManualMemoization(func: HIRFunction): void { instr as TInstruction | TInstruction, manualMemo.kind, sidemap, + errors, ); + + if (fnPlace == null) { + continue; + } + + /** + * Bailout on void return useMemos. This is an anti-pattern where code might be using + * useMemo like useEffect: running arbirtary side-effects synced to changes in specific + * values. + */ + if ( + func.env.config.validateNoVoidUseMemo && + manualMemo.kind === 'useMemo' + ) { + const funcToCheck = sidemap.functions.get( + fnPlace.identifier.id, + )?.value; + if (funcToCheck !== undefined && funcToCheck.loweredFunc.func) { + if (!hasNonVoidReturn(funcToCheck.loweredFunc.func)) { + errors.pushDiagnostic( + CompilerDiagnostic.create({ + severity: ErrorSeverity.InvalidReact, + category: ErrorCategory.UseMemo, + reason: 'useMemo() callbacks must return a value', + description: `This ${ + manualMemo.loadInstr.value.kind === 'PropertyLoad' + ? 'React.useMemo' + : 'useMemo' + } callback doesn't return a value. useMemo is for computing and caching values, not for arbitrary side effects.`, + suggestions: null, + }).withDetail({ + kind: 'error', + loc: instr.value.loc, + message: 'useMemo() callbacks must return a value', + }), + ); + } + } + } + instr.value = getManualMemoizationReplacement( fnPlace, instr.value.loc, @@ -408,11 +502,20 @@ export function dropManualMemoization(func: HIRFunction): void { * is rare and likely sketchy. */ if (!sidemap.functions.has(fnPlace.identifier.id)) { - CompilerError.throwInvalidReact({ - reason: `Expected the first argument to be an inline function expression`, - suggestions: [], - loc: fnPlace.loc, - }); + errors.pushDiagnostic( + CompilerDiagnostic.create({ + category: ErrorCategory.UseMemo, + severity: ErrorSeverity.InvalidReact, + reason: `Expected the first argument to be an inline function expression`, + description: `Expected the first argument to be an inline function expression`, + suggestions: [], + }).withDetail({ + kind: 'error', + loc: fnPlace.loc, + message: `Expected the first argument to be an inline function expression`, + }), + ); + continue; } const memoDecl: Place = manualMemo.kind === 'useMemo' @@ -484,6 +587,8 @@ export function dropManualMemoization(func: HIRFunction): void { markInstructionIds(func.body); } } + + return errors.asResult(); } function findOptionalPlaces(fn: HIRFunction): Set { @@ -528,3 +633,17 @@ function findOptionalPlaces(fn: HIRFunction): Set { } return optionals; } + +function hasNonVoidReturn(func: HIRFunction): boolean { + for (const [, block] of func.body.blocks) { + if (block.terminal.kind === 'return') { + if ( + block.terminal.returnVariant === 'Explicit' || + block.terminal.returnVariant === 'Implicit' + ) { + return true; + } + } + } + return false; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/Inference/InerAliasForUncalledFunctions.ts b/compiler/packages/babel-plugin-react-compiler/src/Inference/InerAliasForUncalledFunctions.ts deleted file mode 100644 index 1dc5743b73702..0000000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/Inference/InerAliasForUncalledFunctions.ts +++ /dev/null @@ -1,134 +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 { - Effect, - HIRFunction, - Identifier, - isMutableEffect, - isRefOrRefLikeMutableType, - makeInstructionId, -} from '../HIR/HIR'; -import {eachInstructionValueOperand} from '../HIR/visitors'; -import {isMutable} from '../ReactiveScopes/InferReactiveScopeVariables'; -import DisjointSet from '../Utils/DisjointSet'; - -/** - * If a function captures a mutable value but never gets called, we don't infer a - * mutable range for that function. This means that we also don't alias the function - * with its mutable captures. - * - * This case is tricky, because we don't generally know for sure what is a mutation - * and what may just be a normal function call. For example: - * - * ``` - * hook useFoo() { - * const x = makeObject(); - * return () => { - * return readObject(x); // could be a mutation! - * } - * } - * ``` - * - * If we pessimistically assume that all such cases are mutations, we'd have to group - * lots of memo scopes together unnecessarily. However, if there is definitely a mutation: - * - * ``` - * hook useFoo(createEntryForKey) { - * const cache = new WeakMap(); - * return (key) => { - * let entry = cache.get(key); - * if (entry == null) { - * entry = createEntryForKey(key); - * cache.set(key, entry); // known mutation! - * } - * return entry; - * } - * } - * ``` - * - * Then we have to ensure that the function and its mutable captures alias together and - * end up in the same scope. However, aliasing together isn't enough if the function - * and operands all have empty mutable ranges (end = start + 1). - * - * This pass finds function expressions and object methods that have an empty mutable range - * and known-mutable operands which also don't have a mutable range, and ensures that the - * function and those operands are aliased together *and* that their ranges are updated to - * end after the function expression. This is sufficient to ensure that a reactive scope is - * created for the alias set. - */ -export function inferAliasForUncalledFunctions( - fn: HIRFunction, - aliases: DisjointSet, -): void { - for (const block of fn.body.blocks.values()) { - instrs: for (const instr of block.instructions) { - const {lvalue, value} = instr; - if ( - value.kind !== 'ObjectMethod' && - value.kind !== 'FunctionExpression' - ) { - continue; - } - /* - * If the function is known to be mutated, we will have - * already aliased any mutable operands with it - */ - const range = lvalue.identifier.mutableRange; - if (range.end > range.start + 1) { - continue; - } - /* - * If the function already has operands with an active mutable range, - * then we don't need to do anything — the function will have already - * been visited and included in some mutable alias set. This case can - * also occur due to visiting the same function in an earlier iteration - * of the outer fixpoint loop. - */ - for (const operand of eachInstructionValueOperand(value)) { - if (isMutable(instr, operand)) { - continue instrs; - } - } - const operands: Set = new Set(); - for (const effect of value.loweredFunc.func.effects ?? []) { - if (effect.kind !== 'ContextMutation') { - continue; - } - /* - * We're looking for known-mutations only, so we look at the effects - * rather than function context - */ - if (effect.effect === Effect.Store || effect.effect === Effect.Mutate) { - for (const operand of effect.places) { - /* - * It's possible that function effect analysis thinks there was a context mutation, - * but then InferReferenceEffects figures out some operands are globals and therefore - * creates a non-mutable effect for those operands. - * We should change InferReferenceEffects to swap the ContextMutation for a global - * mutation in that case, but for now we just filter them out here - */ - if ( - isMutableEffect(operand.effect, operand.loc) && - !isRefOrRefLikeMutableType(operand.identifier.type) - ) { - operands.add(operand.identifier); - } - } - } - } - if (operands.size !== 0) { - operands.add(lvalue.identifier); - aliases.union([...operands]); - // Update mutable ranges, if the ranges are empty then a reactive scope isn't created - for (const operand of operands) { - operand.mutableRange.end = makeInstructionId(instr.id + 1); - } - } - } - } -} diff --git a/compiler/packages/babel-plugin-react-compiler/src/Inference/InferAlias.ts b/compiler/packages/babel-plugin-react-compiler/src/Inference/InferAlias.ts deleted file mode 100644 index 80422c8391f46..0000000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/Inference/InferAlias.ts +++ /dev/null @@ -1,68 +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 { - HIRFunction, - Identifier, - Instruction, - isPrimitiveType, - Place, -} from '../HIR/HIR'; -import DisjointSet from '../Utils/DisjointSet'; - -export type AliasSet = Set; - -export function inferAliases(func: HIRFunction): DisjointSet { - const aliases = new DisjointSet(); - for (const [_, block] of func.body.blocks) { - for (const instr of block.instructions) { - inferInstr(instr, aliases); - } - } - - return aliases; -} - -function inferInstr( - instr: Instruction, - aliases: DisjointSet, -): void { - const {lvalue, value: instrValue} = instr; - let alias: Place | null = null; - switch (instrValue.kind) { - case 'LoadLocal': - case 'LoadContext': { - if (isPrimitiveType(instrValue.place.identifier)) { - return; - } - alias = instrValue.place; - break; - } - case 'StoreLocal': - case 'StoreContext': { - alias = instrValue.value; - break; - } - case 'Destructure': { - alias = instrValue.value; - break; - } - case 'ComputedLoad': - case 'PropertyLoad': { - alias = instrValue.object; - break; - } - case 'TypeCastExpression': { - alias = instrValue.value; - break; - } - default: - return; - } - - aliases.union([lvalue.identifier, alias.identifier]); -} diff --git a/compiler/packages/babel-plugin-react-compiler/src/Inference/InferAliasForPhis.ts b/compiler/packages/babel-plugin-react-compiler/src/Inference/InferAliasForPhis.ts deleted file mode 100644 index e81e3ebdae7a2..0000000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/Inference/InferAliasForPhis.ts +++ /dev/null @@ -1,27 +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 {HIRFunction, Identifier} from '../HIR/HIR'; -import DisjointSet from '../Utils/DisjointSet'; - -export function inferAliasForPhis( - func: HIRFunction, - aliases: DisjointSet, -): void { - for (const [_, block] of func.body.blocks) { - for (const phi of block.phis) { - const isPhiMutatedAfterCreation: boolean = - phi.place.identifier.mutableRange.end > - (block.instructions.at(0)?.id ?? block.terminal.id); - if (isPhiMutatedAfterCreation) { - for (const [, operand] of phi.operands) { - aliases.union([phi.place.identifier, operand.identifier]); - } - } - } - } -} diff --git a/compiler/packages/babel-plugin-react-compiler/src/Inference/InferAliasForStores.ts b/compiler/packages/babel-plugin-react-compiler/src/Inference/InferAliasForStores.ts deleted file mode 100644 index d8d17f6a5fa03..0000000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/Inference/InferAliasForStores.ts +++ /dev/null @@ -1,68 +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 { - Effect, - HIRFunction, - Identifier, - InstructionId, - Place, -} from '../HIR/HIR'; -import { - eachInstructionLValue, - eachInstructionValueOperand, -} from '../HIR/visitors'; -import DisjointSet from '../Utils/DisjointSet'; - -export function inferAliasForStores( - func: HIRFunction, - aliases: DisjointSet, -): void { - for (const [_, block] of func.body.blocks) { - for (const instr of block.instructions) { - const {value, lvalue} = instr; - const isStore = - lvalue.effect === Effect.Store || - /* - * Some typed functions annotate callees or arguments - * as Effect.Store. - */ - ![...eachInstructionValueOperand(value)].every( - operand => operand.effect !== Effect.Store, - ); - - if (!isStore) { - continue; - } - for (const operand of eachInstructionLValue(instr)) { - maybeAlias(aliases, lvalue, operand, instr.id); - } - for (const operand of eachInstructionValueOperand(value)) { - if ( - operand.effect === Effect.Capture || - operand.effect === Effect.Store - ) { - maybeAlias(aliases, lvalue, operand, instr.id); - } - } - } - } -} - -function maybeAlias( - aliases: DisjointSet, - lvalue: Place, - rvalue: Place, - id: InstructionId, -): void { - if ( - lvalue.identifier.mutableRange.end > id + 1 || - rvalue.identifier.mutableRange.end > id - ) { - aliases.union([lvalue.identifier, rvalue.identifier]); - } -} diff --git a/compiler/packages/babel-plugin-react-compiler/src/Inference/InferEffectDependencies.ts b/compiler/packages/babel-plugin-react-compiler/src/Inference/InferEffectDependencies.ts index f1a584341912b..2997a449dead3 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Inference/InferEffectDependencies.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Inference/InferEffectDependencies.ts @@ -10,7 +10,6 @@ import {CompilerError, SourceLocation} from '..'; import { ArrayExpression, Effect, - Environment, FunctionExpression, GeneratedSource, HIRFunction, @@ -29,6 +28,10 @@ import { isSetStateType, isFireFunctionType, makeScopeId, + HIR, + BasicBlock, + BlockId, + isEffectEventFunctionType, } from '../HIR'; import {collectHoistablePropertyLoadsInInnerFn} from '../HIR/CollectHoistablePropertyLoads'; import {collectOptionalChainSidemap} from '../HIR/CollectOptionalChainDependencies'; @@ -38,22 +41,30 @@ import { createTemporaryPlace, fixScopeAndIdentifierRanges, markInstructionIds, + markPredecessors, + reversePostorderBlocks, } from '../HIR/HIRBuilder'; import { collectTemporariesSidemap, DependencyCollectionContext, handleInstruction, } from '../HIR/PropagateScopeDependenciesHIR'; -import {eachInstructionOperand, eachTerminalOperand} from '../HIR/visitors'; +import {buildDependencyInstructions} from '../HIR/ScopeDependencyUtils'; +import { + eachInstructionOperand, + eachTerminalOperand, + terminalFallthrough, +} from '../HIR/visitors'; import {empty} from '../Utils/Stack'; import {getOrInsertWith} from '../Utils/utils'; +import {deadCodeElimination} from '../Optimization'; +import {BuiltInAutodepsId} from '../HIR/ObjectShape'; /** * Infers reactive dependencies captured by useEffect lambdas and adds them as * a second argument to the useEffect call if no dependency array is provided. */ export function inferEffectDependencies(fn: HIRFunction): void { - let hasRewrite = false; const fnExpressions = new Map< IdentifierId, TInstruction @@ -68,7 +79,7 @@ export function inferEffectDependencies(fn: HIRFunction): void { ); moduleTargets.set( effectTarget.function.importSpecifierName, - effectTarget.numRequiredArgs, + effectTarget.autodepsIndex, ); } const autodepFnLoads = new Map(); @@ -86,6 +97,7 @@ export function inferEffectDependencies(fn: HIRFunction): void { * reactive(Identifier i) = Union_{reference of i}(reactive(reference)) */ const reactiveIds = inferReactiveIdentifiers(fn); + const rewriteBlocks: Array = []; for (const [, block] of fn.body.blocks) { if (block.terminal.kind === 'scope') { @@ -101,7 +113,7 @@ export function inferEffectDependencies(fn: HIRFunction): void { ); } } - const rewriteInstrs = new Map>(); + const rewriteInstrs: Array = []; for (const instr of block.instructions) { const {value, lvalue} = instr; if (value.kind === 'FunctionExpression') { @@ -125,7 +137,6 @@ export function inferEffectDependencies(fn: HIRFunction): void { } } else if (value.kind === 'LoadGlobal') { loadGlobals.add(lvalue.identifier.id); - /* * TODO: Handle properties on default exports, like * import React from 'react'; @@ -159,13 +170,26 @@ export function inferEffectDependencies(fn: HIRFunction): void { ) { const callee = value.kind === 'CallExpression' ? value.callee : value.property; + + const autodepsArgIndex = value.args.findIndex( + arg => + arg.kind === 'Identifier' && + arg.identifier.type.kind === 'Object' && + arg.identifier.type.shapeId === BuiltInAutodepsId, + ); + const autodepsArgExpectedIndex = autodepFnLoads.get( + callee.identifier.id, + ); + if ( - value.args.length === autodepFnLoads.get(callee.identifier.id) && + value.args.length > 0 && + autodepsArgExpectedIndex != null && + autodepsArgIndex === autodepsArgExpectedIndex && + autodepFnLoads.has(callee.identifier.id) && value.args[0].kind === 'Identifier' ) { // We have a useEffect call with no deps array, so we need to infer the deps const effectDeps: Array = []; - const newInstructions: Array = []; const deps: ArrayExpression = { kind: 'ArrayExpression', elements: effectDeps, @@ -196,24 +220,29 @@ export function inferEffectDependencies(fn: HIRFunction): void { */ const usedDeps = []; - for (const dep of minimalDeps) { + for (const maybeDep of minimalDeps) { if ( - ((isUseRefType(dep.identifier) || - isSetStateType(dep.identifier)) && - !reactiveIds.has(dep.identifier.id)) || - isFireFunctionType(dep.identifier) + ((isUseRefType(maybeDep.identifier) || + isSetStateType(maybeDep.identifier)) && + !reactiveIds.has(maybeDep.identifier.id)) || + isFireFunctionType(maybeDep.identifier) || + isEffectEventFunctionType(maybeDep.identifier) ) { // exclude non-reactive hook results, which will never be in a memo block continue; } - const {place, instructions} = writeDependencyToInstructions( + const dep = truncateDepAtCurrent(maybeDep); + const {place, value, exitBlockId} = buildDependencyInstructions( dep, - reactiveIds.has(dep.identifier.id), fn.env, - fnExpr.loc, ); - newInstructions.push(...instructions); + rewriteInstrs.push({ + kind: 'block', + location: instr.id, + value, + exitBlockId: exitBlockId, + }); effectDeps.push(place); usedDeps.push(dep); } @@ -234,27 +263,40 @@ export function inferEffectDependencies(fn: HIRFunction): void { }); } - newInstructions.push({ - id: makeInstructionId(0), - loc: GeneratedSource, - lvalue: {...depsPlace, effect: Effect.Mutate}, - value: deps, - }); - // Step 2: push the inferred deps array as an argument of the useEffect - value.args.push({...depsPlace, effect: Effect.Freeze}); - rewriteInstrs.set(instr.id, newInstructions); + rewriteInstrs.push({ + kind: 'instr', + location: instr.id, + value: { + id: makeInstructionId(0), + loc: GeneratedSource, + lvalue: {...depsPlace, effect: Effect.Mutate}, + value: deps, + effects: null, + }, + }); + value.args[autodepsArgIndex] = { + ...depsPlace, + effect: Effect.Freeze, + }; fn.env.inferredEffectLocations.add(callee.loc); } else if (loadGlobals.has(value.args[0].identifier.id)) { // Global functions have no reactive dependencies, so we can insert an empty array - newInstructions.push({ - id: makeInstructionId(0), - loc: GeneratedSource, - lvalue: {...depsPlace, effect: Effect.Mutate}, - value: deps, + rewriteInstrs.push({ + kind: 'instr', + location: instr.id, + value: { + id: makeInstructionId(0), + loc: GeneratedSource, + lvalue: {...depsPlace, effect: Effect.Mutate}, + value: deps, + effects: null, + }, }); - value.args.push({...depsPlace, effect: Effect.Freeze}); - rewriteInstrs.set(instr.id, newInstructions); + value.args[autodepsArgIndex] = { + ...depsPlace, + effect: Effect.Freeze, + }; fn.env.inferredEffectLocations.add(callee.loc); } } else if ( @@ -285,85 +327,166 @@ export function inferEffectDependencies(fn: HIRFunction): void { } } } - if (rewriteInstrs.size > 0) { - hasRewrite = true; - const newInstrs = []; - for (const instr of block.instructions) { - const newInstr = rewriteInstrs.get(instr.id); - if (newInstr != null) { - newInstrs.push(...newInstr, instr); - } else { - newInstrs.push(instr); - } - } - block.instructions = newInstrs; - } + rewriteSplices(block, rewriteInstrs, rewriteBlocks); } - if (hasRewrite) { + + if (rewriteBlocks.length > 0) { + for (const block of rewriteBlocks) { + fn.body.blocks.set(block.id, block); + } + + /** + * Fixup the HIR to restore RPO, ensure correct predecessors, and renumber + * instructions. + */ + reversePostorderBlocks(fn.body); + markPredecessors(fn.body); // Renumber instructions and fix scope ranges markInstructionIds(fn.body); fixScopeAndIdentifierRanges(fn.body); + deadCodeElimination(fn); + fn.env.hasInferredEffect = true; } } -function writeDependencyToInstructions( +function truncateDepAtCurrent( dep: ReactiveScopeDependency, - reactive: boolean, - env: Environment, - loc: SourceLocation, -): {place: Place; instructions: Array} { - const instructions: Array = []; - let currValue = createTemporaryPlace(env, GeneratedSource); - currValue.reactive = reactive; - instructions.push({ - id: makeInstructionId(0), - loc: GeneratedSource, - lvalue: {...currValue, effect: Effect.Mutate}, - value: { - kind: 'LoadLocal', - place: { - kind: 'Identifier', - identifier: dep.identifier, - effect: Effect.Capture, - reactive, - loc: loc, - }, - loc: loc, - }, - }); - for (const path of dep.path) { - if (path.optional) { - /** - * TODO: instead of truncating optional paths, reuse - * instructions from hoisted dependencies block(s) - */ - break; - } - if (path.property === 'current') { - /* - * Prune ref.current accesses. This may over-capture for non-ref values with - * a current property, but that's fine. - */ - break; +): ReactiveScopeDependency { + const idx = dep.path.findIndex(path => path.property === 'current'); + if (idx === -1) { + return dep; + } else { + return {...dep, path: dep.path.slice(0, idx)}; + } +} + +type SpliceInfo = + | {kind: 'instr'; location: InstructionId; value: Instruction} + | { + kind: 'block'; + location: InstructionId; + value: HIR; + exitBlockId: BlockId; + }; + +function rewriteSplices( + originalBlock: BasicBlock, + splices: Array, + rewriteBlocks: Array, +): void { + if (splices.length === 0) { + return; + } + /** + * Splice instructions or value blocks into the original block. + * --- original block --- + * bb_original + * instr1 + * ... + * instr2 <-- splice location + * instr3 + * ... + * + * + * If there is more than one block in the splice, this means that we're + * splicing in a set of value-blocks of the following structure: + * --- blocks we're splicing in --- + * bb_entry: + * instrEntry + * ... + * fallthrough=bb_exit + * + * bb1(value): + * ... + * + * bb_exit: + * instrExit + * ... + * + * + * + * --- rewritten blocks --- + * bb_original + * instr1 + * ... (original instructions) + * instr2 + * instrEntry + * ... (spliced instructions) + * fallthrough=bb_exit + * + * bb1(value): + * ... + * + * bb_exit: + * instrExit + * ... (spliced instructions) + * instr3 + * ... (original instructions) + * + */ + const originalInstrs = originalBlock.instructions; + let currBlock: BasicBlock = {...originalBlock, instructions: []}; + rewriteBlocks.push(currBlock); + + let cursor = 0; + + for (const rewrite of splices) { + while (originalInstrs[cursor].id < rewrite.location) { + CompilerError.invariant( + originalInstrs[cursor].id < originalInstrs[cursor + 1].id, + { + reason: + '[InferEffectDependencies] Internal invariant broken: expected block instructions to be sorted', + loc: originalInstrs[cursor].loc, + }, + ); + currBlock.instructions.push(originalInstrs[cursor]); + cursor++; } - const nextValue = createTemporaryPlace(env, GeneratedSource); - nextValue.reactive = reactive; - instructions.push({ - id: makeInstructionId(0), - loc: GeneratedSource, - lvalue: {...nextValue, effect: Effect.Mutate}, - value: { - kind: 'PropertyLoad', - object: {...currValue, effect: Effect.Capture}, - property: path.property, - loc: loc, - }, + CompilerError.invariant(originalInstrs[cursor].id === rewrite.location, { + reason: + '[InferEffectDependencies] Internal invariant broken: splice location not found', + loc: originalInstrs[cursor].loc, }); - currValue = nextValue; + + if (rewrite.kind === 'instr') { + currBlock.instructions.push(rewrite.value); + } else if (rewrite.kind === 'block') { + const {entry, blocks} = rewrite.value; + const entryBlock = blocks.get(entry)!; + // splice in all instructions from the entry block + currBlock.instructions.push(...entryBlock.instructions); + if (blocks.size > 1) { + /** + * We're splicing in a set of value-blocks, which means we need + * to push new blocks and update terminals. + */ + CompilerError.invariant( + terminalFallthrough(entryBlock.terminal) === rewrite.exitBlockId, + { + reason: + '[InferEffectDependencies] Internal invariant broken: expected entry block to have a fallthrough', + loc: entryBlock.terminal.loc, + }, + ); + const originalTerminal = currBlock.terminal; + currBlock.terminal = entryBlock.terminal; + + for (const [id, block] of blocks) { + if (id === entry) { + continue; + } + if (id === rewrite.exitBlockId) { + block.terminal = originalTerminal; + currBlock = block; + } + rewriteBlocks.push(block); + } + } + } } - currValue.effect = Effect.Freeze; - return {place: currValue, instructions}; + currBlock.instructions.push(...originalInstrs.slice(cursor)); } function inferReactiveIdentifiers(fn: HIRFunction): Set { diff --git a/compiler/packages/babel-plugin-react-compiler/src/Inference/InferFunctionEffects.ts b/compiler/packages/babel-plugin-react-compiler/src/Inference/InferFunctionEffects.ts deleted file mode 100644 index a58ae440219b9..0000000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/Inference/InferFunctionEffects.ts +++ /dev/null @@ -1,345 +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, - CompilerErrorDetailOptions, - ErrorSeverity, - ValueKind, -} from '..'; -import { - AbstractValue, - BasicBlock, - Effect, - Environment, - FunctionEffect, - Instruction, - InstructionValue, - Place, - ValueReason, - getHookKind, - isRefOrRefValue, -} from '../HIR'; -import {eachInstructionOperand, eachTerminalOperand} from '../HIR/visitors'; -import {assertExhaustive} from '../Utils/utils'; - -interface State { - kind(place: Place): AbstractValue; - values(place: Place): Array; - isDefined(place: Place): boolean; -} - -function inferOperandEffect(state: State, place: Place): null | FunctionEffect { - const value = state.kind(place); - CompilerError.invariant(value != null, { - reason: 'Expected operand to have a kind', - loc: null, - }); - - switch (place.effect) { - case Effect.Store: - case Effect.Mutate: { - if (isRefOrRefValue(place.identifier)) { - break; - } else if (value.kind === ValueKind.Context) { - CompilerError.invariant(value.context.size > 0, { - reason: - "[InferFunctionEffects] Expected Context-kind value's capture list to be non-empty.", - loc: place.loc, - }); - return { - kind: 'ContextMutation', - loc: place.loc, - effect: place.effect, - places: value.context, - }; - } else if ( - value.kind !== ValueKind.Mutable && - // We ignore mutations of primitives since this is not a React-specific problem - value.kind !== ValueKind.Primitive - ) { - let reason = getWriteErrorReason(value); - return { - kind: - value.reason.size === 1 && value.reason.has(ValueReason.Global) - ? 'GlobalMutation' - : 'ReactMutation', - error: { - reason, - description: - place.identifier.name !== null && - place.identifier.name.kind === 'named' - ? `Found mutation of \`${place.identifier.name.value}\`` - : null, - loc: place.loc, - suggestions: null, - severity: ErrorSeverity.InvalidReact, - }, - }; - } - break; - } - } - return null; -} - -function inheritFunctionEffects( - state: State, - place: Place, -): Array { - const effects = inferFunctionInstrEffects(state, place); - - return effects - .flatMap(effect => { - if (effect.kind === 'GlobalMutation' || effect.kind === 'ReactMutation') { - return [effect]; - } else { - const effects: Array = []; - CompilerError.invariant(effect.kind === 'ContextMutation', { - reason: 'Expected ContextMutation', - loc: null, - }); - /** - * Contextual effects need to be replayed against the current inference - * state, which may know more about the value to which the effect applied. - * The main cases are: - * 1. The mutated context value is _still_ a context value in the current scope, - * so we have to continue propagating the original context mutation. - * 2. The mutated context value is a mutable value in the current scope, - * so the context mutation was fine and we can skip propagating the effect. - * 3. The mutated context value is an immutable value in the current scope, - * resulting in a non-ContextMutation FunctionEffect. We propagate that new, - * more detailed effect to the current function context. - */ - for (const place of effect.places) { - if (state.isDefined(place)) { - const replayedEffect = inferOperandEffect(state, { - ...place, - loc: effect.loc, - effect: effect.effect, - }); - if (replayedEffect != null) { - if (replayedEffect.kind === 'ContextMutation') { - // Case 1, still a context value so propagate the original effect - effects.push(effect); - } else { - // Case 3, immutable value so propagate the more precise effect - effects.push(replayedEffect); - } - } // else case 2, local mutable value so this effect was fine - } - } - return effects; - } - }) - .filter((effect): effect is FunctionEffect => effect != null); -} - -function inferFunctionInstrEffects( - state: State, - place: Place, -): Array { - const effects: Array = []; - const instrs = state.values(place); - CompilerError.invariant(instrs != null, { - reason: 'Expected operand to have instructions', - loc: null, - }); - - for (const instr of instrs) { - if ( - (instr.kind === 'FunctionExpression' || instr.kind === 'ObjectMethod') && - instr.loweredFunc.func.effects != null - ) { - effects.push(...instr.loweredFunc.func.effects); - } - } - - return effects; -} - -function operandEffects( - state: State, - place: Place, - filterRenderSafe: boolean, -): Array { - const functionEffects: Array = []; - const effect = inferOperandEffect(state, place); - effect && functionEffects.push(effect); - functionEffects.push(...inheritFunctionEffects(state, place)); - if (filterRenderSafe) { - return functionEffects.filter(effect => !isEffectSafeOutsideRender(effect)); - } else { - return functionEffects; - } -} - -export function inferInstructionFunctionEffects( - env: Environment, - state: State, - instr: Instruction, -): Array { - const functionEffects: Array = []; - switch (instr.value.kind) { - case 'JsxExpression': { - if (instr.value.tag.kind === 'Identifier') { - functionEffects.push(...operandEffects(state, instr.value.tag, false)); - } - instr.value.children?.forEach(child => - functionEffects.push(...operandEffects(state, child, false)), - ); - for (const attr of instr.value.props) { - if (attr.kind === 'JsxSpreadAttribute') { - functionEffects.push(...operandEffects(state, attr.argument, false)); - } else { - functionEffects.push(...operandEffects(state, attr.place, true)); - } - } - break; - } - case 'ObjectMethod': - case 'FunctionExpression': { - /** - * If this function references other functions, propagate the referenced function's - * effects to this function. - * - * ``` - * let f = () => global = true; - * let g = () => f(); - * g(); - * ``` - * - * In this example, because `g` references `f`, we propagate the GlobalMutation from - * `f` to `g`. Thus, referencing `g` in `g()` will evaluate the GlobalMutation in the outer - * function effect context and report an error. But if instead we do: - * - * ``` - * let f = () => global = true; - * let g = () => f(); - * useEffect(() => g(), [g]) - * ``` - * - * Now `g`'s effects will be discarded since they're in a useEffect. - */ - for (const operand of eachInstructionOperand(instr)) { - instr.value.loweredFunc.func.effects ??= []; - instr.value.loweredFunc.func.effects.push( - ...inferFunctionInstrEffects(state, operand), - ); - } - break; - } - case 'MethodCall': - case 'CallExpression': { - let callee; - if (instr.value.kind === 'MethodCall') { - callee = instr.value.property; - functionEffects.push( - ...operandEffects(state, instr.value.receiver, false), - ); - } else { - callee = instr.value.callee; - } - functionEffects.push(...operandEffects(state, callee, false)); - let isHook = getHookKind(env, callee.identifier) != null; - for (const arg of instr.value.args) { - const place = arg.kind === 'Identifier' ? arg : arg.place; - /* - * Join the effects of the argument with the effects of the enclosing function, - * unless the we're detecting a global mutation inside a useEffect hook - */ - functionEffects.push(...operandEffects(state, place, isHook)); - } - break; - } - case 'StartMemoize': - case 'FinishMemoize': - case 'LoadLocal': - case 'StoreLocal': { - break; - } - case 'StoreGlobal': { - functionEffects.push({ - kind: 'GlobalMutation', - error: { - reason: - 'Unexpected reassignment of a variable which was defined outside of the component. Components and hooks should be pure and side-effect free, but variable reassignment is a form of side-effect. If this variable is used in rendering, use useState instead. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#side-effects-must-run-outside-of-render)', - loc: instr.loc, - suggestions: null, - severity: ErrorSeverity.InvalidReact, - }, - }); - break; - } - default: { - for (const operand of eachInstructionOperand(instr)) { - functionEffects.push(...operandEffects(state, operand, false)); - } - } - } - return functionEffects; -} - -export function inferTerminalFunctionEffects( - state: State, - block: BasicBlock, -): Array { - const functionEffects: Array = []; - for (const operand of eachTerminalOperand(block.terminal)) { - functionEffects.push(...operandEffects(state, operand, true)); - } - return functionEffects; -} - -export function transformFunctionEffectErrors( - functionEffects: Array, -): Array { - return functionEffects.map(eff => { - switch (eff.kind) { - case 'ReactMutation': - case 'GlobalMutation': { - return eff.error; - } - case 'ContextMutation': { - return { - severity: ErrorSeverity.Invariant, - reason: `Unexpected ContextMutation in top-level function effects`, - loc: eff.loc, - }; - } - default: - assertExhaustive( - eff, - `Unexpected function effect kind \`${(eff as any).kind}\``, - ); - } - }); -} - -function isEffectSafeOutsideRender(effect: FunctionEffect): boolean { - return effect.kind === 'GlobalMutation'; -} - -function getWriteErrorReason(abstractValue: AbstractValue): string { - if (abstractValue.reason.has(ValueReason.Global)) { - return 'Writing to a variable defined outside a component or hook is not allowed. Consider using an effect'; - } else if (abstractValue.reason.has(ValueReason.JsxCaptured)) { - return 'Updating a value used previously in JSX is not allowed. Consider moving the mutation before the JSX'; - } else if (abstractValue.reason.has(ValueReason.Context)) { - return `Mutating a value returned from 'useContext()', which should not be mutated`; - } else if (abstractValue.reason.has(ValueReason.KnownReturnSignature)) { - return 'Mutating a value returned from a function whose return value should not be mutated'; - } else if (abstractValue.reason.has(ValueReason.ReactiveFunctionArgument)) { - return 'Mutating component props or hook arguments is not allowed. Consider using a local variable instead'; - } else if (abstractValue.reason.has(ValueReason.State)) { - return "Mutating a value returned from 'useState()', which should not be mutated. Use the setter function to update instead"; - } else if (abstractValue.reason.has(ValueReason.ReducerState)) { - return "Mutating a value returned from 'useReducer()', which should not be mutated. Use the dispatch function to update instead"; - } else { - return 'This mutates a variable that React considers immutable'; - } -} diff --git a/compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutableLifetimes.ts b/compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutableLifetimes.ts deleted file mode 100644 index 5057a7ac88ca3..0000000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutableLifetimes.ts +++ /dev/null @@ -1,218 +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 { - Effect, - HIRFunction, - Identifier, - InstructionId, - InstructionKind, - isArrayType, - isMapType, - isRefOrRefValue, - isSetType, - makeInstructionId, - Place, -} from '../HIR/HIR'; -import {printPlace} from '../HIR/PrintHIR'; -import { - eachInstructionLValue, - eachInstructionOperand, - eachTerminalOperand, -} from '../HIR/visitors'; -import {assertExhaustive} from '../Utils/utils'; - -/* - * For each usage of a value in the given function, determines if the usage - * may be succeeded by a mutable usage of that same value and if so updates - * the usage to be mutable. - * - * Stated differently, this inference ensures that inferred capabilities of - * each reference are as follows: - * - freeze: the value is frozen at this point - * - readonly: the value is not modified at this point *or any subsequent - * point* - * - mutable: the value is modified at this point *or some subsequent point*. - * - * Note that this refines the capabilities inferered by InferReferenceCapability, - * which looks at individual references and not the lifetime of a value's mutability. - * - * == Algorithm - * - * TODO: - * 1. Forward data-flow analysis to determine aliasing. Unlike InferReferenceCapability - * which only tracks aliasing of top-level variables (`y = x`), this analysis needs - * to know if a value is aliased anywhere (`y.x = x`). The forward data flow tracks - * all possible locations which may have aliased a value. The concrete result is - * a mapping of each Place to the set of possibly-mutable values it may alias. - * - * ``` - * const x = []; // {x: v0; v0: mutable []} - * const y = {}; // {x: v0, y: v1; v0: mutable [], v1: mutable []} - * y.x = x; // {x: v0, y: v1; v0: mutable [v1], v1: mutable [v0]} - * read(x); // {x: v0, y: v1; v0: mutable [v1], v1: mutable [v0]} - * mutate(y); // can infer that y mutates v0 and v1 - * ``` - * - * DONE: - * 2. Forward data-flow analysis to compute mutability liveness. Walk forwards over - * the CFG and track which values are mutated in a successor. - * - * ``` - * mutate(y); // mutable y => v0, v1 mutated - * read(x); // x maps to v0, v1, those are in the mutated-later set, so x is mutable here - * ... - * ``` - */ - -function infer(place: Place, instrId: InstructionId): void { - if (!isRefOrRefValue(place.identifier)) { - place.identifier.mutableRange.end = makeInstructionId(instrId + 1); - } -} - -function inferPlace( - place: Place, - instrId: InstructionId, - inferMutableRangeForStores: boolean, -): void { - switch (place.effect) { - case Effect.Unknown: { - throw new Error(`Found an unknown place ${printPlace(place)}}!`); - } - case Effect.Capture: - case Effect.Read: - case Effect.Freeze: - return; - case Effect.Store: - if (inferMutableRangeForStores) { - infer(place, instrId); - } - return; - case Effect.ConditionallyMutateIterator: { - const identifier = place.identifier; - if ( - !isArrayType(identifier) && - !isSetType(identifier) && - !isMapType(identifier) - ) { - infer(place, instrId); - } - return; - } - case Effect.ConditionallyMutate: - case Effect.Mutate: { - infer(place, instrId); - return; - } - default: - assertExhaustive(place.effect, `Unexpected ${printPlace(place)} effect`); - } -} - -export function inferMutableLifetimes( - func: HIRFunction, - inferMutableRangeForStores: boolean, -): void { - /* - * Context variables only appear to mutate where they are assigned, but we need - * to force their range to start at their declaration. Track the declaring instruction - * id so that the ranges can be extended if/when they are reassigned - */ - const contextVariableDeclarationInstructions = new Map< - Identifier, - InstructionId - >(); - for (const [_, block] of func.body.blocks) { - for (const phi of block.phis) { - const isPhiMutatedAfterCreation: boolean = - phi.place.identifier.mutableRange.end > - (block.instructions.at(0)?.id ?? block.terminal.id); - if ( - inferMutableRangeForStores && - isPhiMutatedAfterCreation && - phi.place.identifier.mutableRange.start === 0 - ) { - for (const [, operand] of phi.operands) { - if (phi.place.identifier.mutableRange.start === 0) { - phi.place.identifier.mutableRange.start = - operand.identifier.mutableRange.start; - } else { - phi.place.identifier.mutableRange.start = makeInstructionId( - Math.min( - phi.place.identifier.mutableRange.start, - operand.identifier.mutableRange.start, - ), - ); - } - } - } - } - - for (const instr of block.instructions) { - for (const operand of eachInstructionLValue(instr)) { - const lvalueId = operand.identifier; - - /* - * lvalue start being mutable when they're initially assigned a - * value. - */ - lvalueId.mutableRange.start = instr.id; - - /* - * Let's be optimistic and assume this lvalue is not mutable by - * default. - */ - lvalueId.mutableRange.end = makeInstructionId(instr.id + 1); - } - for (const operand of eachInstructionOperand(instr)) { - inferPlace(operand, instr.id, inferMutableRangeForStores); - } - - if ( - instr.value.kind === 'DeclareContext' || - (instr.value.kind === 'StoreContext' && - instr.value.lvalue.kind !== InstructionKind.Reassign && - !contextVariableDeclarationInstructions.has( - instr.value.lvalue.place.identifier, - )) - ) { - /** - * Save declarations of context variables if they hasn't already been - * declared (due to hoisted declarations). - */ - contextVariableDeclarationInstructions.set( - instr.value.lvalue.place.identifier, - instr.id, - ); - } else if (instr.value.kind === 'StoreContext') { - /* - * Else this is a reassignment, extend the range from the declaration (if present). - * Note that declarations may not be present for context variables that are reassigned - * within a function expression before (or without) a read of the same variable - */ - const declaration = contextVariableDeclarationInstructions.get( - instr.value.lvalue.place.identifier, - ); - if ( - declaration != null && - !isRefOrRefValue(instr.value.lvalue.place.identifier) - ) { - const range = instr.value.lvalue.place.identifier.mutableRange; - if (range.start === 0) { - range.start = declaration; - } else { - range.start = makeInstructionId(Math.min(range.start, declaration)); - } - } - } - } - for (const operand of eachTerminalOperand(block.terminal)) { - inferPlace(operand, block.terminal.id, inferMutableRangeForStores); - } - } -} diff --git a/compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutableRanges.ts b/compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutableRanges.ts deleted file mode 100644 index 624c302fbf7ee..0000000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutableRanges.ts +++ /dev/null @@ -1,102 +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 {HIRFunction, Identifier} from '../HIR/HIR'; -import {inferAliasForUncalledFunctions} from './InerAliasForUncalledFunctions'; -import {inferAliases} from './InferAlias'; -import {inferAliasForPhis} from './InferAliasForPhis'; -import {inferAliasForStores} from './InferAliasForStores'; -import {inferMutableLifetimes} from './InferMutableLifetimes'; -import {inferMutableRangesForAlias} from './InferMutableRangesForAlias'; -import {inferTryCatchAliases} from './InferTryCatchAliases'; - -export function inferMutableRanges(ir: HIRFunction): void { - // Infer mutable ranges for non fields - inferMutableLifetimes(ir, false); - - // Calculate aliases - const aliases = inferAliases(ir); - /* - * Calculate aliases for try/catch, where any value created - * in the try block could be aliased to the catch param - */ - inferTryCatchAliases(ir, aliases); - - /* - * Eagerly canonicalize so that if nothing changes we can bail out - * after a single iteration - */ - let prevAliases: Map = aliases.canonicalize(); - while (true) { - // Infer mutable ranges for aliases that are not fields - inferMutableRangesForAlias(ir, aliases); - - // Update aliasing information of fields - inferAliasForStores(ir, aliases); - - // Update aliasing information of phis - inferAliasForPhis(ir, aliases); - - const nextAliases = aliases.canonicalize(); - if (areEqualMaps(prevAliases, nextAliases)) { - break; - } - prevAliases = nextAliases; - } - - // Re-infer mutable ranges for all values - inferMutableLifetimes(ir, true); - - /** - * The second inferMutableLifetimes() call updates mutable ranges - * of values to account for Store effects. Now we need to update - * all aliases of such values to extend their ranges as well. Note - * that the store only mutates the the directly aliased value and - * not any of its inner captured references. For example: - * - * ``` - * let y; - * if (cond) { - * y = []; - * } else { - * y = [{}]; - * } - * y.push(z); - * ``` - * - * The Store effect from the `y.push` modifies the values that `y` - * directly aliases - the two arrays from the if/else branches - - * but does not modify values that `y` "contains" such as the - * object literal or `z`. - */ - prevAliases = aliases.canonicalize(); - while (true) { - inferMutableRangesForAlias(ir, aliases); - inferAliasForPhis(ir, aliases); - inferAliasForUncalledFunctions(ir, aliases); - const nextAliases = aliases.canonicalize(); - if (areEqualMaps(prevAliases, nextAliases)) { - break; - } - prevAliases = nextAliases; - } -} - -function areEqualMaps(a: Map, b: Map): boolean { - if (a.size !== b.size) { - return false; - } - for (const [key, value] of a) { - if (!b.has(key)) { - return false; - } - if (b.get(key) !== value) { - return false; - } - } - return true; -} diff --git a/compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutableRangesForAlias.ts b/compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutableRangesForAlias.ts deleted file mode 100644 index a7e8b5c1f7a80..0000000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutableRangesForAlias.ts +++ /dev/null @@ -1,54 +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 { - HIRFunction, - Identifier, - InstructionId, - isRefOrRefValue, -} from '../HIR/HIR'; -import DisjointSet from '../Utils/DisjointSet'; - -export function inferMutableRangesForAlias( - _fn: HIRFunction, - aliases: DisjointSet, -): void { - const aliasSets = aliases.buildSets(); - for (const aliasSet of aliasSets) { - /* - * Update mutableRange.end only if the identifiers have actually been - * mutated. - */ - const mutatingIdentifiers = [...aliasSet].filter( - id => - id.mutableRange.end - id.mutableRange.start > 1 && !isRefOrRefValue(id), - ); - - if (mutatingIdentifiers.length > 0) { - // Find final instruction which mutates this alias set. - let lastMutatingInstructionId = 0; - for (const id of mutatingIdentifiers) { - if (id.mutableRange.end > lastMutatingInstructionId) { - lastMutatingInstructionId = id.mutableRange.end; - } - } - - /* - * Update mutableRange.end for all aliases in this set ending before the - * last mutation. - */ - for (const alias of aliasSet) { - if ( - alias.mutableRange.end < lastMutatingInstructionId && - !isRefOrRefValue(alias) - ) { - alias.mutableRange.end = lastMutatingInstructionId as InstructionId; - } - } - } - } -} diff --git a/compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutationAliasingEffects.ts b/compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutationAliasingEffects.ts new file mode 100644 index 0000000000000..a0e9593268812 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutationAliasingEffects.ts @@ -0,0 +1,2800 @@ +/** + * 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 { + CompilerDiagnostic, + CompilerError, + Effect, + ErrorSeverity, + SourceLocation, + ValueKind, +} from '..'; +import { + BasicBlock, + BlockId, + DeclarationId, + Environment, + FunctionExpression, + GeneratedSource, + HIRFunction, + Hole, + IdentifierId, + Instruction, + InstructionKind, + InstructionValue, + isArrayType, + isJsxType, + isMapType, + isPrimitiveType, + isRefOrRefValue, + isSetType, + makeIdentifierId, + Phi, + Place, + SpreadPattern, + Type, + ValueReason, +} from '../HIR'; +import { + eachInstructionValueOperand, + eachPatternItem, + eachTerminalOperand, + eachTerminalSuccessor, +} from '../HIR/visitors'; +import {Ok, Result} from '../Utils/Result'; +import { + assertExhaustive, + getOrInsertDefault, + getOrInsertWith, + Set_isSuperset, +} from '../Utils/utils'; +import { + printAliasingEffect, + printAliasingSignature, + printIdentifier, + printInstruction, + printInstructionValue, + printPlace, + printSourceLocation, +} from '../HIR/PrintHIR'; +import {FunctionSignature} from '../HIR/ObjectShape'; +import prettyFormat from 'pretty-format'; +import {createTemporaryPlace} from '../HIR/HIRBuilder'; +import { + AliasingEffect, + AliasingSignature, + hashEffect, + MutationReason, +} from './AliasingEffects'; +import {ErrorCategory} from '../CompilerError'; + +const DEBUG = false; + +/** + * Infers the mutation/aliasing effects for instructions and terminals and annotates + * them on the HIR, making the effects of builtin instructions/functions as well as + * user-defined functions explicit. These effects then form the basis for subsequent + * analysis to determine the mutable range of each value in the program — the set of + * instructions over which the value is created and mutated — as well as validation + * against invalid code. + * + * At a high level the approach is: + * - Determine a set of candidate effects based purely on the syntax of the instruction + * and the types involved. These candidate effects are cached the first time each + * instruction is visited. The idea is to reason about the semantics of the instruction + * or function in isolation, separately from how those effects may interact with later + * abstract interpretation. + * - Then we do abstract interpretation over the HIR, iterating until reaching a fixpoint. + * This phase tracks the abstract kind of each value (mutable, primitive, frozen, etc) + * and the set of values pointed to by each identifier. Each candidate effect is "applied" + * to the current abtract state, and effects may be dropped or rewritten accordingly. + * For example, a "MutateConditionally " effect may be dropped if x is not a mutable + * value. A "Mutate " effect may get converted into a "MutateFrozen " effect + * if y is mutable, etc. + */ +export function inferMutationAliasingEffects( + fn: HIRFunction, + {isFunctionExpression}: {isFunctionExpression: boolean} = { + isFunctionExpression: false, + }, +): Result { + const initialState = InferenceState.empty(fn.env, isFunctionExpression); + + // Map of blocks to the last (merged) incoming state that was processed + const statesByBlock: Map = new Map(); + + for (const ref of fn.context) { + // TODO: using InstructionValue as a bit of a hack, but it's pragmatic + const value: InstructionValue = { + kind: 'ObjectExpression', + properties: [], + loc: ref.loc, + }; + initialState.initialize(value, { + kind: ValueKind.Context, + reason: new Set([ValueReason.Other]), + }); + initialState.define(ref, value); + } + + const paramKind: AbstractValue = isFunctionExpression + ? { + kind: ValueKind.Mutable, + reason: new Set([ValueReason.Other]), + } + : { + kind: ValueKind.Frozen, + reason: new Set([ValueReason.ReactiveFunctionArgument]), + }; + + if (fn.fnType === 'Component') { + CompilerError.invariant(fn.params.length <= 2, { + reason: + 'Expected React component to have not more than two parameters: one for props and for ref', + description: null, + loc: fn.loc, + suggestions: null, + }); + const [props, ref] = fn.params; + if (props != null) { + inferParam(props, initialState, paramKind); + } + if (ref != null) { + const place = ref.kind === 'Identifier' ? ref : ref.place; + const value: InstructionValue = { + kind: 'ObjectExpression', + properties: [], + loc: place.loc, + }; + initialState.initialize(value, { + kind: ValueKind.Mutable, + reason: new Set([ValueReason.Other]), + }); + initialState.define(place, value); + } + } else { + for (const param of fn.params) { + inferParam(param, initialState, paramKind); + } + } + + /* + * Multiple predecessors may be visited prior to reaching a given successor, + * so track the list of incoming state for each successor block. + * These are merged when reaching that block again. + */ + const queuedStates: Map = new Map(); + function queue(blockId: BlockId, state: InferenceState): void { + let queuedState = queuedStates.get(blockId); + if (queuedState != null) { + // merge the queued states for this block + state = queuedState.merge(state) ?? queuedState; + queuedStates.set(blockId, state); + } else { + /* + * this is the first queued state for this block, see whether + * there are changed relative to the last time it was processed. + */ + const prevState = statesByBlock.get(blockId); + const nextState = prevState != null ? prevState.merge(state) : state; + if (nextState != null) { + queuedStates.set(blockId, nextState); + } + } + } + queue(fn.body.entry, initialState); + + const hoistedContextDeclarations = findHoistedContextDeclarations(fn); + + const context = new Context( + isFunctionExpression, + fn, + hoistedContextDeclarations, + ); + + let iterationCount = 0; + while (queuedStates.size !== 0) { + iterationCount++; + if (iterationCount > 100) { + CompilerError.invariant(false, { + reason: `[InferMutationAliasingEffects] Potential infinite loop`, + description: `A value, temporary place, or effect was not cached properly`, + loc: fn.loc, + }); + } + for (const [blockId, block] of fn.body.blocks) { + const incomingState = queuedStates.get(blockId); + queuedStates.delete(blockId); + if (incomingState == null) { + continue; + } + + statesByBlock.set(blockId, incomingState); + const state = incomingState.clone(); + inferBlock(context, state, block); + + for (const nextBlockId of eachTerminalSuccessor(block.terminal)) { + queue(nextBlockId, state); + } + } + } + return Ok(undefined); +} + +function findHoistedContextDeclarations( + fn: HIRFunction, +): Map { + const hoisted = new Map(); + function visit(place: Place): void { + if ( + hoisted.has(place.identifier.declarationId) && + hoisted.get(place.identifier.declarationId) == null + ) { + // If this is the first load of the value, store the location + hoisted.set(place.identifier.declarationId, place); + } + } + for (const block of fn.body.blocks.values()) { + for (const instr of block.instructions) { + if (instr.value.kind === 'DeclareContext') { + const kind = instr.value.lvalue.kind; + if ( + kind == InstructionKind.HoistedConst || + kind == InstructionKind.HoistedFunction || + kind == InstructionKind.HoistedLet + ) { + hoisted.set(instr.value.lvalue.place.identifier.declarationId, null); + } + } else { + for (const operand of eachInstructionValueOperand(instr.value)) { + visit(operand); + } + } + } + for (const operand of eachTerminalOperand(block.terminal)) { + visit(operand); + } + } + return hoisted; +} + +class Context { + internedEffects: Map = new Map(); + instructionSignatureCache: Map = new Map(); + effectInstructionValueCache: Map = + new Map(); + applySignatureCache: Map< + AliasingSignature, + Map | null> + > = new Map(); + catchHandlers: Map = new Map(); + functionSignatureCache: Map = + new Map(); + isFuctionExpression: boolean; + fn: HIRFunction; + hoistedContextDeclarations: Map; + + constructor( + isFunctionExpression: boolean, + fn: HIRFunction, + hoistedContextDeclarations: Map, + ) { + this.isFuctionExpression = isFunctionExpression; + this.fn = fn; + this.hoistedContextDeclarations = hoistedContextDeclarations; + } + + cacheApplySignature( + signature: AliasingSignature, + effect: Extract, + f: () => Array | null, + ): Array | null { + const inner = getOrInsertDefault( + this.applySignatureCache, + signature, + new Map(), + ); + return getOrInsertWith(inner, effect, f); + } + + internEffect(effect: AliasingEffect): AliasingEffect { + const hash = hashEffect(effect); + let interned = this.internedEffects.get(hash); + if (interned == null) { + this.internedEffects.set(hash, effect); + interned = effect; + } + return interned; + } +} + +function inferParam( + param: Place | SpreadPattern, + initialState: InferenceState, + paramKind: AbstractValue, +): void { + const place = param.kind === 'Identifier' ? param : param.place; + const value: InstructionValue = { + kind: 'Primitive', + loc: place.loc, + value: undefined, + }; + initialState.initialize(value, paramKind); + initialState.define(place, value); +} + +function inferBlock( + context: Context, + state: InferenceState, + block: BasicBlock, +): void { + for (const phi of block.phis) { + state.inferPhi(phi); + } + + for (const instr of block.instructions) { + let instructionSignature = context.instructionSignatureCache.get(instr); + if (instructionSignature == null) { + instructionSignature = computeSignatureForInstruction( + context, + state.env, + instr, + ); + context.instructionSignatureCache.set(instr, instructionSignature); + } + const effects = applySignature(context, state, instructionSignature, instr); + instr.effects = effects; + } + const terminal = block.terminal; + if (terminal.kind === 'try' && terminal.handlerBinding != null) { + context.catchHandlers.set(terminal.handler, terminal.handlerBinding); + } else if (terminal.kind === 'maybe-throw') { + const handlerParam = context.catchHandlers.get(terminal.handler); + if (handlerParam != null) { + CompilerError.invariant(state.kind(handlerParam) != null, { + reason: + 'Expected catch binding to be intialized with a DeclareLocal Catch instruction', + loc: terminal.loc, + }); + const effects: Array = []; + for (const instr of block.instructions) { + if ( + instr.value.kind === 'CallExpression' || + instr.value.kind === 'MethodCall' + ) { + /** + * Many instructions can error, but only calls can throw their result as the error + * itself. For example, `c = a.b` can throw if `a` is nullish, but the thrown value + * is an error object synthesized by the JS runtime. Whereas `throwsInput(x)` can + * throw (effectively) the result of the call. + * + * TODO: call applyEffect() instead. This meant that the catch param wasn't inferred + * as a mutable value, though. See `try-catch-try-value-modified-in-catch-escaping.js` + * fixture as an example + */ + state.appendAlias(handlerParam, instr.lvalue); + const kind = state.kind(instr.lvalue).kind; + if (kind === ValueKind.Mutable || kind == ValueKind.Context) { + effects.push( + context.internEffect({ + kind: 'Alias', + from: instr.lvalue, + into: handlerParam, + }), + ); + } + } + } + terminal.effects = effects.length !== 0 ? effects : null; + } + } else if (terminal.kind === 'return') { + if (!context.isFuctionExpression) { + terminal.effects = [ + context.internEffect({ + kind: 'Freeze', + value: terminal.value, + reason: ValueReason.JsxCaptured, + }), + ]; + } + } +} + +/** + * Applies the signature to the given state to determine the precise set of effects + * that will occur in practice. This takes into account the inferred state of each + * variable. For example, the signature may have a `ConditionallyMutate x` effect. + * Here, we check the abstract type of `x` and either record a `Mutate x` if x is mutable + * or no effect if x is a primitive, global, or frozen. + * + * This phase may also emit errors, for example MutateLocal on a frozen value is invalid. + */ +function applySignature( + context: Context, + state: InferenceState, + signature: InstructionSignature, + instruction: Instruction, +): Array | null { + const effects: Array = []; + /** + * For function instructions, eagerly validate that they aren't mutating + * a known-frozen value. + * + * TODO: make sure we're also validating against global mutations somewhere, but + * account for this being allowed in effects/event handlers. + */ + if ( + instruction.value.kind === 'FunctionExpression' || + instruction.value.kind === 'ObjectMethod' + ) { + const aliasingEffects = + instruction.value.loweredFunc.func.aliasingEffects ?? []; + const context = new Set( + instruction.value.loweredFunc.func.context.map(p => p.identifier.id), + ); + for (const effect of aliasingEffects) { + if (effect.kind === 'Mutate' || effect.kind === 'MutateTransitive') { + if (!context.has(effect.value.identifier.id)) { + continue; + } + const value = state.kind(effect.value); + switch (value.kind) { + case ValueKind.Frozen: { + const reason = getWriteErrorReason({ + kind: value.kind, + reason: value.reason, + }); + const variable = + effect.value.identifier.name !== null && + effect.value.identifier.name.kind === 'named' + ? `\`${effect.value.identifier.name.value}\`` + : 'value'; + const diagnostic = CompilerDiagnostic.create({ + category: ErrorCategory.Immutability, + severity: ErrorSeverity.InvalidReact, + reason: 'This value cannot be modified', + description: `${reason}.`, + }).withDetail({ + kind: 'error', + loc: effect.value.loc, + message: `${variable} cannot be modified`, + }); + if ( + effect.kind === 'Mutate' && + effect.reason?.kind === 'AssignCurrentProperty' + ) { + diagnostic.withDetail({ + kind: 'hint', + message: `Hint: If this value is a Ref (value returned by \`useRef()\`), rename the variable to end in "Ref".`, + }); + } + effects.push({ + kind: 'MutateFrozen', + place: effect.value, + error: diagnostic, + }); + } + } + } + } + } + + /* + * Track which values we've already aliased once, so that we can switch to + * appendAlias() for subsequent aliases into the same value + */ + const initialized = new Set(); + + if (DEBUG) { + console.log(printInstruction(instruction)); + } + + for (const effect of signature.effects) { + applyEffect(context, state, effect, initialized, effects); + } + if (DEBUG) { + console.log( + prettyFormat(state.debugAbstractValue(state.kind(instruction.lvalue))), + ); + console.log( + effects.map(effect => ` ${printAliasingEffect(effect)}`).join('\n'), + ); + } + if ( + !(state.isDefined(instruction.lvalue) && state.kind(instruction.lvalue)) + ) { + CompilerError.invariant(false, { + reason: `Expected instruction lvalue to be initialized`, + loc: instruction.loc, + }); + } + return effects.length !== 0 ? effects : null; +} + +function applyEffect( + context: Context, + state: InferenceState, + _effect: AliasingEffect, + initialized: Set, + effects: Array, +): void { + const effect = context.internEffect(_effect); + if (DEBUG) { + console.log(printAliasingEffect(effect)); + } + switch (effect.kind) { + case 'Freeze': { + const didFreeze = state.freeze(effect.value, effect.reason); + if (didFreeze) { + effects.push(effect); + } + break; + } + case 'Create': { + CompilerError.invariant(!initialized.has(effect.into.identifier.id), { + reason: `Cannot re-initialize variable within an instruction`, + description: `Re-initialized ${printPlace(effect.into)} in ${printAliasingEffect(effect)}`, + loc: effect.into.loc, + }); + initialized.add(effect.into.identifier.id); + + let value = context.effectInstructionValueCache.get(effect); + if (value == null) { + value = { + kind: 'ObjectExpression', + properties: [], + loc: effect.into.loc, + }; + context.effectInstructionValueCache.set(effect, value); + } + state.initialize(value, { + kind: effect.value, + reason: new Set([effect.reason]), + }); + state.define(effect.into, value); + effects.push(effect); + break; + } + case 'ImmutableCapture': { + const kind = state.kind(effect.from).kind; + switch (kind) { + case ValueKind.Global: + case ValueKind.Primitive: { + // no-op: we don't need to track data flow for copy types + break; + } + default: { + effects.push(effect); + } + } + break; + } + case 'CreateFrom': { + CompilerError.invariant(!initialized.has(effect.into.identifier.id), { + reason: `Cannot re-initialize variable within an instruction`, + description: `Re-initialized ${printPlace(effect.into)} in ${printAliasingEffect(effect)}`, + loc: effect.into.loc, + }); + initialized.add(effect.into.identifier.id); + + const fromValue = state.kind(effect.from); + let value = context.effectInstructionValueCache.get(effect); + if (value == null) { + value = { + kind: 'ObjectExpression', + properties: [], + loc: effect.into.loc, + }; + context.effectInstructionValueCache.set(effect, value); + } + state.initialize(value, { + kind: fromValue.kind, + reason: new Set(fromValue.reason), + }); + state.define(effect.into, value); + switch (fromValue.kind) { + case ValueKind.Primitive: + case ValueKind.Global: { + effects.push({ + kind: 'Create', + value: fromValue.kind, + into: effect.into, + reason: [...fromValue.reason][0] ?? ValueReason.Other, + }); + break; + } + case ValueKind.Frozen: { + effects.push({ + kind: 'Create', + value: fromValue.kind, + into: effect.into, + reason: [...fromValue.reason][0] ?? ValueReason.Other, + }); + applyEffect( + context, + state, + { + kind: 'ImmutableCapture', + from: effect.from, + into: effect.into, + }, + initialized, + effects, + ); + break; + } + default: { + effects.push(effect); + } + } + break; + } + case 'CreateFunction': { + CompilerError.invariant(!initialized.has(effect.into.identifier.id), { + reason: `Cannot re-initialize variable within an instruction`, + description: `Re-initialized ${printPlace(effect.into)} in ${printAliasingEffect(effect)}`, + loc: effect.into.loc, + }); + initialized.add(effect.into.identifier.id); + + effects.push(effect); + /** + * We consider the function mutable if it has any mutable context variables or + * any side-effects that need to be tracked if the function is called. + */ + const hasCaptures = effect.captures.some(capture => { + switch (state.kind(capture).kind) { + case ValueKind.Context: + case ValueKind.Mutable: { + return true; + } + default: { + return false; + } + } + }); + const hasTrackedSideEffects = + effect.function.loweredFunc.func.aliasingEffects?.some( + effect => + // TODO; include "render" here? + effect.kind === 'MutateFrozen' || + effect.kind === 'MutateGlobal' || + effect.kind === 'Impure', + ); + // For legacy compatibility + const capturesRef = effect.function.loweredFunc.func.context.some( + operand => isRefOrRefValue(operand.identifier), + ); + const isMutable = hasCaptures || hasTrackedSideEffects || capturesRef; + for (const operand of effect.function.loweredFunc.func.context) { + if (operand.effect !== Effect.Capture) { + continue; + } + const kind = state.kind(operand).kind; + if ( + kind === ValueKind.Primitive || + kind == ValueKind.Frozen || + kind == ValueKind.Global + ) { + operand.effect = Effect.Read; + } + } + state.initialize(effect.function, { + kind: isMutable ? ValueKind.Mutable : ValueKind.Frozen, + reason: new Set([]), + }); + state.define(effect.into, effect.function); + for (const capture of effect.captures) { + applyEffect( + context, + state, + { + kind: 'Capture', + from: capture, + into: effect.into, + }, + initialized, + effects, + ); + } + break; + } + case 'MaybeAlias': + case 'Alias': + case 'Capture': { + CompilerError.invariant( + effect.kind === 'Capture' || initialized.has(effect.into.identifier.id), + { + reason: `Expected destination value to already be initialized within this instruction for Alias effect`, + description: `Destination ${printPlace(effect.into)} is not initialized in this instruction`, + loc: effect.into.loc, + }, + ); + /* + * Capture describes potential information flow: storing a pointer to one value + * within another. If the destination is not mutable, or the source value has + * copy-on-write semantics, then we can prune the effect + */ + const intoKind = state.kind(effect.into).kind; + let isMutableDesination: boolean; + switch (intoKind) { + case ValueKind.Context: + case ValueKind.Mutable: + case ValueKind.MaybeFrozen: { + isMutableDesination = true; + break; + } + default: { + isMutableDesination = false; + break; + } + } + const fromKind = state.kind(effect.from).kind; + let isMutableReferenceType: boolean; + switch (fromKind) { + case ValueKind.Global: + case ValueKind.Primitive: { + isMutableReferenceType = false; + break; + } + case ValueKind.Frozen: { + isMutableReferenceType = false; + applyEffect( + context, + state, + { + kind: 'ImmutableCapture', + from: effect.from, + into: effect.into, + }, + initialized, + effects, + ); + break; + } + default: { + isMutableReferenceType = true; + break; + } + } + if (isMutableDesination && isMutableReferenceType) { + effects.push(effect); + } + break; + } + case 'Assign': { + CompilerError.invariant(!initialized.has(effect.into.identifier.id), { + reason: `Cannot re-initialize variable within an instruction`, + description: `Re-initialized ${printPlace(effect.into)} in ${printAliasingEffect(effect)}`, + loc: effect.into.loc, + }); + initialized.add(effect.into.identifier.id); + + /* + * Alias represents potential pointer aliasing. If the type is a global, + * a primitive (copy-on-write semantics) then we can prune the effect + */ + const fromValue = state.kind(effect.from); + const fromKind = fromValue.kind; + switch (fromKind) { + case ValueKind.Frozen: { + applyEffect( + context, + state, + { + kind: 'ImmutableCapture', + from: effect.from, + into: effect.into, + }, + initialized, + effects, + ); + let value = context.effectInstructionValueCache.get(effect); + if (value == null) { + value = { + kind: 'Primitive', + value: undefined, + loc: effect.from.loc, + }; + context.effectInstructionValueCache.set(effect, value); + } + state.initialize(value, { + kind: fromKind, + reason: new Set(fromValue.reason), + }); + state.define(effect.into, value); + break; + } + case ValueKind.Global: + case ValueKind.Primitive: { + let value = context.effectInstructionValueCache.get(effect); + if (value == null) { + value = { + kind: 'Primitive', + value: undefined, + loc: effect.from.loc, + }; + context.effectInstructionValueCache.set(effect, value); + } + state.initialize(value, { + kind: fromKind, + reason: new Set(fromValue.reason), + }); + state.define(effect.into, value); + break; + } + default: { + state.assign(effect.into, effect.from); + effects.push(effect); + break; + } + } + break; + } + case 'Apply': { + const functionValues = state.values(effect.function); + if ( + functionValues.length === 1 && + functionValues[0].kind === 'FunctionExpression' && + functionValues[0].loweredFunc.func.aliasingEffects != null + ) { + /* + * We're calling a locally declared function, we already know it's effects! + * We just have to substitute in the args for the params + */ + const functionExpr = functionValues[0]; + let signature = context.functionSignatureCache.get(functionExpr); + if (signature == null) { + signature = buildSignatureFromFunctionExpression( + state.env, + functionExpr, + ); + context.functionSignatureCache.set(functionExpr, signature); + } + if (DEBUG) { + console.log( + `constructed alias signature:\n${printAliasingSignature(signature)}`, + ); + } + const signatureEffects = context.cacheApplySignature( + signature, + effect, + () => + computeEffectsForSignature( + state.env, + signature, + effect.into, + effect.receiver, + effect.args, + functionExpr.loweredFunc.func.context, + effect.loc, + ), + ); + if (signatureEffects != null) { + applyEffect( + context, + state, + {kind: 'MutateTransitiveConditionally', value: effect.function}, + initialized, + effects, + ); + for (const signatureEffect of signatureEffects) { + applyEffect(context, state, signatureEffect, initialized, effects); + } + break; + } + } + let signatureEffects = null; + if (effect.signature?.aliasing != null) { + const signature = effect.signature.aliasing; + signatureEffects = context.cacheApplySignature( + effect.signature.aliasing, + effect, + () => + computeEffectsForSignature( + state.env, + signature, + effect.into, + effect.receiver, + effect.args, + [], + effect.loc, + ), + ); + } + if (signatureEffects != null) { + for (const signatureEffect of signatureEffects) { + applyEffect(context, state, signatureEffect, initialized, effects); + } + } else if (effect.signature != null) { + const legacyEffects = computeEffectsForLegacySignature( + state, + effect.signature, + effect.into, + effect.receiver, + effect.args, + effect.loc, + ); + for (const legacyEffect of legacyEffects) { + applyEffect(context, state, legacyEffect, initialized, effects); + } + } else { + applyEffect( + context, + state, + { + kind: 'Create', + into: effect.into, + value: ValueKind.Mutable, + reason: ValueReason.Other, + }, + initialized, + effects, + ); + /* + * If no signature then by default: + * - All operands are conditionally mutated, except some instruction + * variants are assumed to not mutate the callee (such as `new`) + * - All operands are captured into (but not directly aliased as) + * every other argument. + */ + for (const arg of [effect.receiver, effect.function, ...effect.args]) { + if (arg.kind === 'Hole') { + continue; + } + const operand = arg.kind === 'Identifier' ? arg : arg.place; + if (operand !== effect.function || effect.mutatesFunction) { + applyEffect( + context, + state, + { + kind: 'MutateTransitiveConditionally', + value: operand, + }, + initialized, + effects, + ); + } + const mutateIterator = + arg.kind === 'Spread' ? conditionallyMutateIterator(operand) : null; + if (mutateIterator) { + applyEffect(context, state, mutateIterator, initialized, effects); + } + applyEffect( + context, + state, + // OK: recording information flow + {kind: 'MaybeAlias', from: operand, into: effect.into}, + initialized, + effects, + ); + for (const otherArg of [ + effect.receiver, + effect.function, + ...effect.args, + ]) { + if (otherArg.kind === 'Hole') { + continue; + } + const other = + otherArg.kind === 'Identifier' ? otherArg : otherArg.place; + if (other === arg) { + continue; + } + applyEffect( + context, + state, + { + /* + * OK: a function might store one operand into another, + * but it can't force one to alias another + */ + kind: 'Capture', + from: operand, + into: other, + }, + initialized, + effects, + ); + } + } + } + break; + } + case 'Mutate': + case 'MutateConditionally': + case 'MutateTransitive': + case 'MutateTransitiveConditionally': { + const mutationKind = state.mutate(effect.kind, effect.value); + if (mutationKind === 'mutate') { + effects.push(effect); + } else if (mutationKind === 'mutate-ref') { + // no-op + } else if ( + mutationKind !== 'none' && + (effect.kind === 'Mutate' || effect.kind === 'MutateTransitive') + ) { + const value = state.kind(effect.value); + if (DEBUG) { + console.log(`invalid mutation: ${printAliasingEffect(effect)}`); + console.log(prettyFormat(state.debugAbstractValue(value))); + } + + if ( + mutationKind === 'mutate-frozen' && + context.hoistedContextDeclarations.has( + effect.value.identifier.declarationId, + ) + ) { + const variable = + effect.value.identifier.name !== null && + effect.value.identifier.name.kind === 'named' + ? `\`${effect.value.identifier.name.value}\`` + : null; + const hoistedAccess = context.hoistedContextDeclarations.get( + effect.value.identifier.declarationId, + ); + const diagnostic = CompilerDiagnostic.create({ + category: ErrorCategory.Immutability, + severity: ErrorSeverity.InvalidReact, + reason: 'Cannot access variable before it is declared', + description: `${variable ?? 'This variable'} is accessed before it is declared, which prevents the earlier access from updating when this value changes over time.`, + }); + if (hoistedAccess != null && hoistedAccess.loc != effect.value.loc) { + diagnostic.withDetail({ + kind: 'error', + loc: hoistedAccess.loc, + message: `${variable ?? 'variable'} accessed before it is declared`, + }); + } + diagnostic.withDetail({ + kind: 'error', + loc: effect.value.loc, + message: `${variable ?? 'variable'} is declared here`, + }); + + applyEffect( + context, + state, + { + kind: 'MutateFrozen', + place: effect.value, + error: diagnostic, + }, + initialized, + effects, + ); + } else { + const reason = getWriteErrorReason({ + kind: value.kind, + reason: value.reason, + }); + const variable = + effect.value.identifier.name !== null && + effect.value.identifier.name.kind === 'named' + ? `\`${effect.value.identifier.name.value}\`` + : 'value'; + const diagnostic = CompilerDiagnostic.create({ + category: ErrorCategory.Immutability, + severity: ErrorSeverity.InvalidReact, + reason: 'This value cannot be modified', + description: `${reason}.`, + }).withDetail({ + kind: 'error', + loc: effect.value.loc, + message: `${variable} cannot be modified`, + }); + if ( + effect.kind === 'Mutate' && + effect.reason?.kind === 'AssignCurrentProperty' + ) { + diagnostic.withDetail({ + kind: 'hint', + message: `Hint: If this value is a Ref (value returned by \`useRef()\`), rename the variable to end in "Ref".`, + }); + } + applyEffect( + context, + state, + { + kind: + value.kind === ValueKind.Frozen + ? 'MutateFrozen' + : 'MutateGlobal', + place: effect.value, + error: diagnostic, + }, + initialized, + effects, + ); + } + } + break; + } + case 'Impure': + case 'Render': + case 'MutateFrozen': + case 'MutateGlobal': { + effects.push(effect); + break; + } + default: { + assertExhaustive( + effect, + `Unexpected effect kind '${(effect as any).kind as any}'`, + ); + } + } +} + +class InferenceState { + env: Environment; + #isFunctionExpression: boolean; + + // The kind of each value, based on its allocation site + #values: Map; + /* + * The set of values pointed to by each identifier. This is a set + * to accomodate phi points (where a variable may have different + * values from different control flow paths). + */ + #variables: Map>; + + constructor( + env: Environment, + isFunctionExpression: boolean, + values: Map, + variables: Map>, + ) { + this.env = env; + this.#isFunctionExpression = isFunctionExpression; + this.#values = values; + this.#variables = variables; + } + + static empty( + env: Environment, + isFunctionExpression: boolean, + ): InferenceState { + return new InferenceState(env, isFunctionExpression, new Map(), new Map()); + } + + get isFunctionExpression(): boolean { + return this.#isFunctionExpression; + } + + // (Re)initializes a @param value with its default @param kind. + initialize(value: InstructionValue, kind: AbstractValue): void { + CompilerError.invariant(value.kind !== 'LoadLocal', { + reason: + '[InferMutationAliasingEffects] Expected all top-level identifiers to be defined as variables, not values', + description: null, + loc: value.loc, + suggestions: null, + }); + this.#values.set(value, kind); + } + + values(place: Place): Array { + const values = this.#variables.get(place.identifier.id); + CompilerError.invariant(values != null, { + reason: `[InferMutationAliasingEffects] Expected value kind to be initialized`, + description: `${printPlace(place)}`, + loc: place.loc, + suggestions: null, + }); + return Array.from(values); + } + + // Lookup the kind of the given @param value. + kind(place: Place): AbstractValue { + const values = this.#variables.get(place.identifier.id); + CompilerError.invariant(values != null, { + reason: `[InferMutationAliasingEffects] Expected value kind to be initialized`, + description: `${printPlace(place)}`, + loc: place.loc, + suggestions: null, + }); + let mergedKind: AbstractValue | null = null; + for (const value of values) { + const kind = this.#values.get(value)!; + mergedKind = + mergedKind !== null ? mergeAbstractValues(mergedKind, kind) : kind; + } + CompilerError.invariant(mergedKind !== null, { + reason: `[InferMutationAliasingEffects] Expected at least one value`, + description: `No value found at \`${printPlace(place)}\``, + loc: place.loc, + suggestions: null, + }); + return mergedKind; + } + + // Updates the value at @param place to point to the same value as @param value. + assign(place: Place, value: Place): void { + const values = this.#variables.get(value.identifier.id); + CompilerError.invariant(values != null, { + reason: `[InferMutationAliasingEffects] Expected value for identifier to be initialized`, + description: `${printIdentifier(value.identifier)}`, + loc: value.loc, + suggestions: null, + }); + this.#variables.set(place.identifier.id, new Set(values)); + } + + appendAlias(place: Place, value: Place): void { + const values = this.#variables.get(value.identifier.id); + CompilerError.invariant(values != null, { + reason: `[InferMutationAliasingEffects] Expected value for identifier to be initialized`, + description: `${printIdentifier(value.identifier)}`, + loc: value.loc, + suggestions: null, + }); + const prevValues = this.values(place); + this.#variables.set( + place.identifier.id, + new Set([...prevValues, ...values]), + ); + } + + // Defines (initializing or updating) a variable with a specific kind of value. + define(place: Place, value: InstructionValue): void { + CompilerError.invariant(this.#values.has(value), { + reason: `[InferMutationAliasingEffects] Expected value to be initialized at '${printSourceLocation( + value.loc, + )}'`, + description: printInstructionValue(value), + loc: value.loc, + suggestions: null, + }); + this.#variables.set(place.identifier.id, new Set([value])); + } + + isDefined(place: Place): boolean { + return this.#variables.has(place.identifier.id); + } + + /** + * Marks @param place as transitively frozen. Returns true if the value was not + * already frozen, false if the value is already frozen (or already known immutable). + */ + freeze(place: Place, reason: ValueReason): boolean { + const value = this.kind(place); + switch (value.kind) { + case ValueKind.Context: + case ValueKind.Mutable: + case ValueKind.MaybeFrozen: { + const values = this.values(place); + for (const instrValue of values) { + this.freezeValue(instrValue, reason); + } + return true; + } + case ValueKind.Frozen: + case ValueKind.Global: + case ValueKind.Primitive: { + return false; + } + default: { + assertExhaustive( + value.kind, + `Unexpected value kind '${(value as any).kind}'`, + ); + } + } + } + + freezeValue(value: InstructionValue, reason: ValueReason): void { + this.#values.set(value, { + kind: ValueKind.Frozen, + reason: new Set([reason]), + }); + if ( + value.kind === 'FunctionExpression' && + (this.env.config.enablePreserveExistingMemoizationGuarantees || + this.env.config.enableTransitivelyFreezeFunctionExpressions) + ) { + for (const place of value.loweredFunc.func.context) { + this.freeze(place, reason); + } + } + } + + mutate( + variant: + | 'Mutate' + | 'MutateConditionally' + | 'MutateTransitive' + | 'MutateTransitiveConditionally', + place: Place, + ): 'none' | 'mutate' | 'mutate-frozen' | 'mutate-global' | 'mutate-ref' { + if (isRefOrRefValue(place.identifier)) { + return 'mutate-ref'; + } + const kind = this.kind(place).kind; + switch (variant) { + case 'MutateConditionally': + case 'MutateTransitiveConditionally': { + switch (kind) { + case ValueKind.Mutable: + case ValueKind.Context: { + return 'mutate'; + } + default: { + return 'none'; + } + } + } + case 'Mutate': + case 'MutateTransitive': { + switch (kind) { + case ValueKind.Mutable: + case ValueKind.Context: { + return 'mutate'; + } + case ValueKind.Primitive: { + // technically an error, but it's not React specific + return 'none'; + } + case ValueKind.Frozen: { + return 'mutate-frozen'; + } + case ValueKind.Global: { + return 'mutate-global'; + } + case ValueKind.MaybeFrozen: { + return 'mutate-frozen'; + } + default: { + assertExhaustive(kind, `Unexpected kind ${kind}`); + } + } + } + default: { + assertExhaustive(variant, `Unexpected mutation variant ${variant}`); + } + } + } + + /* + * Combine the contents of @param this and @param other, returning a new + * instance with the combined changes _if_ there are any changes, or + * returning null if no changes would occur. Changes include: + * - new entries in @param other that did not exist in @param this + * - entries whose values differ in @param this and @param other, + * and where joining the values produces a different value than + * what was in @param this. + * + * Note that values are joined using a lattice operation to ensure + * termination. + */ + merge(other: InferenceState): InferenceState | null { + let nextValues: Map | null = null; + let nextVariables: Map> | null = null; + + for (const [id, thisValue] of this.#values) { + const otherValue = other.#values.get(id); + if (otherValue !== undefined) { + const mergedValue = mergeAbstractValues(thisValue, otherValue); + if (mergedValue !== thisValue) { + nextValues = nextValues ?? new Map(this.#values); + nextValues.set(id, mergedValue); + } + } + } + for (const [id, otherValue] of other.#values) { + if (this.#values.has(id)) { + // merged above + continue; + } + nextValues = nextValues ?? new Map(this.#values); + nextValues.set(id, otherValue); + } + + for (const [id, thisValues] of this.#variables) { + const otherValues = other.#variables.get(id); + if (otherValues !== undefined) { + let mergedValues: Set | null = null; + for (const otherValue of otherValues) { + if (!thisValues.has(otherValue)) { + mergedValues = mergedValues ?? new Set(thisValues); + mergedValues.add(otherValue); + } + } + if (mergedValues !== null) { + nextVariables = nextVariables ?? new Map(this.#variables); + nextVariables.set(id, mergedValues); + } + } + } + for (const [id, otherValues] of other.#variables) { + if (this.#variables.has(id)) { + continue; + } + nextVariables = nextVariables ?? new Map(this.#variables); + nextVariables.set(id, new Set(otherValues)); + } + + if (nextVariables === null && nextValues === null) { + return null; + } else { + return new InferenceState( + this.env, + this.#isFunctionExpression, + nextValues ?? new Map(this.#values), + nextVariables ?? new Map(this.#variables), + ); + } + } + + /* + * Returns a copy of this state. + * TODO: consider using persistent data structures to make + * clone cheaper. + */ + clone(): InferenceState { + return new InferenceState( + this.env, + this.#isFunctionExpression, + new Map(this.#values), + new Map(this.#variables), + ); + } + + /* + * For debugging purposes, dumps the state to a plain + * object so that it can printed as JSON. + */ + debug(): any { + const result: any = {values: {}, variables: {}}; + const objects: Map = new Map(); + function identify(value: InstructionValue): number { + let id = objects.get(value); + if (id == null) { + id = objects.size; + objects.set(value, id); + } + return id; + } + for (const [value, kind] of this.#values) { + const id = identify(value); + result.values[id] = { + abstract: this.debugAbstractValue(kind), + value: printInstructionValue(value), + }; + } + for (const [variable, values] of this.#variables) { + result.variables[`$${variable}`] = [...values].map(identify); + } + return result; + } + + debugAbstractValue(value: AbstractValue): any { + return { + kind: value.kind, + reason: [...value.reason], + }; + } + + inferPhi(phi: Phi): void { + const values: Set = new Set(); + for (const [_, operand] of phi.operands) { + const operandValues = this.#variables.get(operand.identifier.id); + // This is a backedge that will be handled later by State.merge + if (operandValues === undefined) continue; + for (const v of operandValues) { + values.add(v); + } + } + + if (values.size > 0) { + this.#variables.set(phi.place.identifier.id, values); + } + } +} + +/** + * Returns a value that represents the combined states of the two input values. + * If the two values are semantically equivalent, it returns the first argument. + */ +function mergeAbstractValues( + a: AbstractValue, + b: AbstractValue, +): AbstractValue { + const kind = mergeValueKinds(a.kind, b.kind); + if ( + kind === a.kind && + kind === b.kind && + Set_isSuperset(a.reason, b.reason) + ) { + return a; + } + const reason = new Set(a.reason); + for (const r of b.reason) { + reason.add(r); + } + return {kind, reason}; +} + +type InstructionSignature = { + effects: ReadonlyArray; +}; + +function conditionallyMutateIterator(place: Place): AliasingEffect | null { + if ( + !( + isArrayType(place.identifier) || + isSetType(place.identifier) || + isMapType(place.identifier) + ) + ) { + return { + kind: 'MutateTransitiveConditionally', + value: place, + }; + } + return null; +} + +/** + * Computes an effect signature for the instruction _without_ looking at the inference state, + * and only using the semantics of the instructions and the inferred types. The idea is to make + * it easy to check that the semantics of each instruction are preserved by describing only the + * effects and not making decisions based on the inference state. + * + * Then in applySignature(), above, we refine this signature based on the inference state. + * + * NOTE: this function is designed to be cached so it's only computed once upon first visiting + * an instruction. + */ +function computeSignatureForInstruction( + context: Context, + env: Environment, + instr: Instruction, +): InstructionSignature { + const {lvalue, value} = instr; + const effects: Array = []; + switch (value.kind) { + case 'ArrayExpression': { + effects.push({ + kind: 'Create', + into: lvalue, + value: ValueKind.Mutable, + reason: ValueReason.Other, + }); + // All elements are captured into part of the output value + for (const element of value.elements) { + if (element.kind === 'Identifier') { + effects.push({ + kind: 'Capture', + from: element, + into: lvalue, + }); + } else if (element.kind === 'Spread') { + const mutateIterator = conditionallyMutateIterator(element.place); + if (mutateIterator != null) { + effects.push(mutateIterator); + } + effects.push({ + kind: 'Capture', + from: element.place, + into: lvalue, + }); + } else { + continue; + } + } + break; + } + case 'ObjectExpression': { + effects.push({ + kind: 'Create', + into: lvalue, + value: ValueKind.Mutable, + reason: ValueReason.Other, + }); + for (const property of value.properties) { + if (property.kind === 'ObjectProperty') { + effects.push({ + kind: 'Capture', + from: property.place, + into: lvalue, + }); + } else { + effects.push({ + kind: 'Capture', + from: property.place, + into: lvalue, + }); + } + } + break; + } + case 'Await': { + effects.push({ + kind: 'Create', + into: lvalue, + value: ValueKind.Mutable, + reason: ValueReason.Other, + }); + // Potentially mutates the receiver (awaiting it changes its state and can run side effects) + effects.push({kind: 'MutateTransitiveConditionally', value: value.value}); + /** + * Data from the promise may be returned into the result, but await does not directly return + * the promise itself + */ + effects.push({ + kind: 'Capture', + from: value.value, + into: lvalue, + }); + break; + } + case 'NewExpression': + case 'CallExpression': + case 'MethodCall': { + let callee; + let receiver; + let mutatesCallee; + if (value.kind === 'NewExpression') { + callee = value.callee; + receiver = value.callee; + mutatesCallee = false; + } else if (value.kind === 'CallExpression') { + callee = value.callee; + receiver = value.callee; + mutatesCallee = true; + } else if (value.kind === 'MethodCall') { + callee = value.property; + receiver = value.receiver; + mutatesCallee = false; + } else { + assertExhaustive( + value, + `Unexpected value kind '${(value as any).kind}'`, + ); + } + const signature = getFunctionCallSignature(env, callee.identifier.type); + effects.push({ + kind: 'Apply', + receiver, + function: callee, + mutatesFunction: mutatesCallee, + args: value.args, + into: lvalue, + signature, + loc: value.loc, + }); + break; + } + case 'PropertyDelete': + case 'ComputedDelete': { + effects.push({ + kind: 'Create', + into: lvalue, + value: ValueKind.Primitive, + reason: ValueReason.Other, + }); + // Mutates the object by removing the property, no aliasing + effects.push({kind: 'Mutate', value: value.object}); + break; + } + case 'PropertyLoad': + case 'ComputedLoad': { + if (isPrimitiveType(lvalue.identifier)) { + effects.push({ + kind: 'Create', + into: lvalue, + value: ValueKind.Primitive, + reason: ValueReason.Other, + }); + } else { + effects.push({ + kind: 'CreateFrom', + from: value.object, + into: lvalue, + }); + } + break; + } + case 'PropertyStore': + case 'ComputedStore': { + const mutationReason: MutationReason | null = + value.kind === 'PropertyStore' && value.property === 'current' + ? {kind: 'AssignCurrentProperty'} + : null; + effects.push({ + kind: 'Mutate', + value: value.object, + reason: mutationReason, + }); + effects.push({ + kind: 'Capture', + from: value.value, + into: value.object, + }); + effects.push({ + kind: 'Create', + into: lvalue, + value: ValueKind.Primitive, + reason: ValueReason.Other, + }); + break; + } + case 'ObjectMethod': + case 'FunctionExpression': { + /** + * We've already analyzed the function expression in AnalyzeFunctions. There, we assign + * a Capture effect to any context variable that appears (locally) to be aliased and/or + * mutated. The precise effects are annotated on the function expression's aliasingEffects + * property, but we don't want to execute those effects yet. We can only use those when + * we know exactly how the function is invoked — via an Apply effect from a custom signature. + * + * But in the general case, functions can be passed around and possibly called in ways where + * we don't know how to interpret their precise effects. For example: + * + * ``` + * const a = {}; + * + * // We don't want to consider a as mutating here, this just declares the function + * const f = () => { maybeMutate(a) }; + * + * // We don't want to consider a as mutating here either, it can't possibly call f yet + * const x = [f]; + * + * // Here we have to assume that f can be called (transitively), and have to consider a + * // as mutating + * callAllFunctionInArray(x); + * ``` + * + * So for any context variables that were inferred as captured or mutated, we record a + * Capture effect. If the resulting function is transitively mutated, this will mean + * that those operands are also considered mutated. If the function is never called, + * they won't be! + * + * This relies on the rule that: + * Capture a -> b and MutateTransitive(b) => Mutate(a) + * + * Substituting: + * Capture contextvar -> function and MutateTransitive(function) => Mutate(contextvar) + * + * Note that if the type of the context variables are frozen, global, or primitive, the + * Capture will either get pruned or downgraded to an ImmutableCapture. + */ + effects.push({ + kind: 'CreateFunction', + into: lvalue, + function: value, + captures: value.loweredFunc.func.context.filter( + operand => operand.effect === Effect.Capture, + ), + }); + break; + } + case 'GetIterator': { + effects.push({ + kind: 'Create', + into: lvalue, + value: ValueKind.Mutable, + reason: ValueReason.Other, + }); + if ( + isArrayType(value.collection.identifier) || + isMapType(value.collection.identifier) || + isSetType(value.collection.identifier) + ) { + /* + * Builtin collections are known to return a fresh iterator on each call, + * so the iterator does not alias the collection + */ + effects.push({ + kind: 'Capture', + from: value.collection, + into: lvalue, + }); + } else { + /* + * Otherwise, the object may return itself as the iterator, so we have to + * assume that the result directly aliases the collection. Further, the + * method to get the iterator could potentially mutate the collection + */ + effects.push({kind: 'Alias', from: value.collection, into: lvalue}); + effects.push({ + kind: 'MutateTransitiveConditionally', + value: value.collection, + }); + } + break; + } + case 'IteratorNext': { + /* + * Technically advancing an iterator will always mutate it (for any reasonable implementation) + * But because we create an alias from the collection to the iterator if we don't know the type, + * then it's possible the iterator is aliased to a frozen value and we wouldn't want to error. + * so we mark this as conditional mutation to allow iterating frozen values. + */ + effects.push({kind: 'MutateConditionally', value: value.iterator}); + // Extracts part of the original collection into the result + effects.push({ + kind: 'CreateFrom', + from: value.collection, + into: lvalue, + }); + break; + } + case 'NextPropertyOf': { + effects.push({ + kind: 'Create', + into: lvalue, + value: ValueKind.Primitive, + reason: ValueReason.Other, + }); + break; + } + case 'JsxExpression': + case 'JsxFragment': { + effects.push({ + kind: 'Create', + into: lvalue, + value: ValueKind.Frozen, + reason: ValueReason.JsxCaptured, + }); + for (const operand of eachInstructionValueOperand(value)) { + effects.push({ + kind: 'Freeze', + value: operand, + reason: ValueReason.JsxCaptured, + }); + effects.push({ + kind: 'Capture', + from: operand, + into: lvalue, + }); + } + if (value.kind === 'JsxExpression') { + if (value.tag.kind === 'Identifier') { + // Tags are render function, by definition they're called during render + effects.push({ + kind: 'Render', + place: value.tag, + }); + } + if (value.children != null) { + // Children are typically called during render, not used as an event/effect callback + for (const child of value.children) { + effects.push({ + kind: 'Render', + place: child, + }); + } + } + for (const prop of value.props) { + if ( + prop.kind === 'JsxAttribute' && + prop.place.identifier.type.kind === 'Function' && + (isJsxType(prop.place.identifier.type.return) || + (prop.place.identifier.type.return.kind === 'Phi' && + prop.place.identifier.type.return.operands.some(operand => + isJsxType(operand), + ))) + ) { + // Any props which return jsx are assumed to be called during render + effects.push({ + kind: 'Render', + place: prop.place, + }); + } + } + } + break; + } + case 'DeclareLocal': { + // TODO check this + effects.push({ + kind: 'Create', + into: value.lvalue.place, + // TODO: what kind here??? + value: ValueKind.Primitive, + reason: ValueReason.Other, + }); + effects.push({ + kind: 'Create', + into: lvalue, + // TODO: what kind here??? + value: ValueKind.Primitive, + reason: ValueReason.Other, + }); + break; + } + case 'Destructure': { + for (const patternItem of eachPatternItem(value.lvalue.pattern)) { + const place = + patternItem.kind === 'Identifier' ? patternItem : patternItem.place; + if (isPrimitiveType(place.identifier)) { + effects.push({ + kind: 'Create', + into: place, + value: ValueKind.Primitive, + reason: ValueReason.Other, + }); + } else if (patternItem.kind === 'Identifier') { + effects.push({ + kind: 'CreateFrom', + from: value.value, + into: place, + }); + } else { + // Spread creates a new object/array that captures from the RValue + effects.push({ + kind: 'Create', + into: place, + reason: ValueReason.Other, + value: ValueKind.Mutable, + }); + effects.push({ + kind: 'Capture', + from: value.value, + into: place, + }); + } + } + effects.push({kind: 'Assign', from: value.value, into: lvalue}); + break; + } + case 'LoadContext': { + /* + * Context variables are like mutable boxes. Loading from one + * is equivalent to a PropertyLoad from the box, so we model it + * with the same effect we use there (CreateFrom) + */ + effects.push({kind: 'CreateFrom', from: value.place, into: lvalue}); + break; + } + case 'DeclareContext': { + // Context variables are conceptually like mutable boxes + const kind = value.lvalue.kind; + if ( + !context.hoistedContextDeclarations.has( + value.lvalue.place.identifier.declarationId, + ) || + kind === InstructionKind.HoistedConst || + kind === InstructionKind.HoistedFunction || + kind === InstructionKind.HoistedLet + ) { + /** + * If this context variable is not hoisted, or this is the declaration doing the hoisting, + * then we create the box. + */ + effects.push({ + kind: 'Create', + into: value.lvalue.place, + value: ValueKind.Mutable, + reason: ValueReason.Other, + }); + } else { + /** + * Otherwise this may be a "declare", but there was a previous DeclareContext that + * hoisted this variable, and we're mutating it here. + */ + effects.push({kind: 'Mutate', value: value.lvalue.place}); + } + effects.push({ + kind: 'Create', + into: lvalue, + // The result can't be referenced so this value doesn't matter + value: ValueKind.Primitive, + reason: ValueReason.Other, + }); + break; + } + case 'StoreContext': { + /* + * Context variables are like mutable boxes, so semantically + * we're either creating (let/const) or mutating (reassign) a box, + * and then capturing the value into it. + */ + if ( + value.lvalue.kind === InstructionKind.Reassign || + context.hoistedContextDeclarations.has( + value.lvalue.place.identifier.declarationId, + ) + ) { + effects.push({kind: 'Mutate', value: value.lvalue.place}); + } else { + effects.push({ + kind: 'Create', + into: value.lvalue.place, + value: ValueKind.Mutable, + reason: ValueReason.Other, + }); + } + effects.push({ + kind: 'Capture', + from: value.value, + into: value.lvalue.place, + }); + effects.push({kind: 'Assign', from: value.value, into: lvalue}); + break; + } + case 'LoadLocal': { + effects.push({kind: 'Assign', from: value.place, into: lvalue}); + break; + } + case 'StoreLocal': { + effects.push({ + kind: 'Assign', + from: value.value, + into: value.lvalue.place, + }); + effects.push({kind: 'Assign', from: value.value, into: lvalue}); + break; + } + case 'PostfixUpdate': + case 'PrefixUpdate': { + effects.push({ + kind: 'Create', + into: lvalue, + value: ValueKind.Primitive, + reason: ValueReason.Other, + }); + effects.push({ + kind: 'Create', + into: value.lvalue, + value: ValueKind.Primitive, + reason: ValueReason.Other, + }); + break; + } + case 'StoreGlobal': { + const variable = `\`${value.name}\``; + effects.push({ + kind: 'MutateGlobal', + place: value.value, + error: CompilerDiagnostic.create({ + category: ErrorCategory.Globals, + severity: ErrorSeverity.InvalidReact, + reason: + 'Cannot reassign variables declared outside of the component/hook', + description: `Variable ${variable} is declared outside of the component/hook. Reassigning this value during render is a form of side effect, which can cause unpredictable behavior depending on when the component happens to re-render. If this variable is used in rendering, use useState instead. Otherwise, consider updating it in an effect. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#side-effects-must-run-outside-of-render)`, + }).withDetail({ + kind: 'error', + loc: instr.loc, + message: `${variable} cannot be reassigned`, + }), + }); + effects.push({kind: 'Assign', from: value.value, into: lvalue}); + break; + } + case 'TypeCastExpression': { + effects.push({kind: 'Assign', from: value.value, into: lvalue}); + break; + } + case 'LoadGlobal': { + effects.push({ + kind: 'Create', + into: lvalue, + value: ValueKind.Global, + reason: ValueReason.Global, + }); + break; + } + case 'StartMemoize': + case 'FinishMemoize': { + if (env.config.enablePreserveExistingMemoizationGuarantees) { + for (const operand of eachInstructionValueOperand(value)) { + effects.push({ + kind: 'Freeze', + value: operand, + reason: ValueReason.HookCaptured, + }); + } + } + effects.push({ + kind: 'Create', + into: lvalue, + value: ValueKind.Primitive, + reason: ValueReason.Other, + }); + break; + } + case 'TaggedTemplateExpression': + case 'BinaryExpression': + case 'Debugger': + case 'JSXText': + case 'MetaProperty': + case 'Primitive': + case 'RegExpLiteral': + case 'TemplateLiteral': + case 'UnaryExpression': + case 'UnsupportedNode': { + effects.push({ + kind: 'Create', + into: lvalue, + value: ValueKind.Primitive, + reason: ValueReason.Other, + }); + break; + } + } + return { + effects, + }; +} + +/** + * Creates a set of aliasing effects given a legacy FunctionSignature. This makes all of the + * old implicit behaviors from the signatures and InferReferenceEffects explicit, see comments + * in the body for details. + * + * The goal of this method is to make it easier to migrate incrementally to the new system, + * so we don't have to immediately write new signatures for all the methods to get expected + * compilation output. + */ +function computeEffectsForLegacySignature( + state: InferenceState, + signature: FunctionSignature, + lvalue: Place, + receiver: Place, + args: Array, + loc: SourceLocation, +): Array { + const returnValueReason = signature.returnValueReason ?? ValueReason.Other; + const effects: Array = []; + effects.push({ + kind: 'Create', + into: lvalue, + value: signature.returnValueKind, + reason: returnValueReason, + }); + if (signature.impure && state.env.config.validateNoImpureFunctionsInRender) { + effects.push({ + kind: 'Impure', + place: receiver, + error: CompilerDiagnostic.create({ + category: ErrorCategory.Purity, + severity: ErrorSeverity.InvalidReact, + reason: 'Cannot call impure function during render', + description: + (signature.canonicalName != null + ? `\`${signature.canonicalName}\` is an impure function. ` + : '') + + 'Calling an impure function can produce unstable results that update unpredictably when the component happens to re-render. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent)', + }).withDetail({ + kind: 'error', + loc, + message: 'Cannot call impure function', + }), + }); + } + if (signature.knownIncompatible != null && state.env.isInferredMemoEnabled) { + const errors = new CompilerError(); + errors.pushDiagnostic( + CompilerDiagnostic.create({ + category: ErrorCategory.IncompatibleLibrary, + severity: ErrorSeverity.IncompatibleLibrary, + reason: 'Use of incompatible library', + description: [ + 'This API returns functions which cannot be memoized without leading to stale UI. ' + + 'To prevent this, by default React Compiler will skip memoizing this component/hook. ' + + 'However, you may see issues if values from this API are passed to other components/hooks that are ' + + 'memoized.', + ].join(''), + }).withDetail({ + kind: 'error', + loc: receiver.loc, + message: signature.knownIncompatible, + }), + ); + throw errors; + } + const stores: Array = []; + const captures: Array = []; + function visit(place: Place, effect: Effect): void { + switch (effect) { + case Effect.Store: { + effects.push({ + kind: 'Mutate', + value: place, + }); + stores.push(place); + break; + } + case Effect.Capture: { + captures.push(place); + break; + } + case Effect.ConditionallyMutate: { + effects.push({ + kind: 'MutateTransitiveConditionally', + value: place, + }); + break; + } + case Effect.ConditionallyMutateIterator: { + const mutateIterator = conditionallyMutateIterator(place); + if (mutateIterator != null) { + effects.push(mutateIterator); + } + effects.push({ + kind: 'Capture', + from: place, + into: lvalue, + }); + break; + } + case Effect.Freeze: { + effects.push({ + kind: 'Freeze', + value: place, + reason: returnValueReason, + }); + break; + } + case Effect.Mutate: { + effects.push({kind: 'MutateTransitive', value: place}); + break; + } + case Effect.Read: { + effects.push({ + kind: 'ImmutableCapture', + from: place, + into: lvalue, + }); + break; + } + } + } + + if ( + signature.mutableOnlyIfOperandsAreMutable && + areArgumentsImmutableAndNonMutating(state, args) + ) { + effects.push({ + kind: 'Alias', + from: receiver, + into: lvalue, + }); + for (const arg of args) { + if (arg.kind === 'Hole') { + continue; + } + const place = arg.kind === 'Identifier' ? arg : arg.place; + effects.push({ + kind: 'ImmutableCapture', + from: place, + into: lvalue, + }); + } + return effects; + } + + if (signature.calleeEffect !== Effect.Capture) { + /* + * InferReferenceEffects and FunctionSignature have an implicit assumption that the receiver + * is captured into the return value. Consider for example the signature for Array.proto.pop: + * the calleeEffect is Store, since it's a known mutation but non-transitive. But the return + * of the pop() captures from the receiver! This isn't specified explicitly. So we add this + * here, and rely on applySignature() to downgrade this to ImmutableCapture (or prune) if + * the type doesn't actually need to be captured based on the input and return type. + */ + effects.push({ + kind: 'Alias', + from: receiver, + into: lvalue, + }); + } + visit(receiver, signature.calleeEffect); + for (let i = 0; i < args.length; i++) { + const arg = args[i]; + if (arg.kind === 'Hole') { + continue; + } + const place = arg.kind === 'Identifier' ? arg : arg.place; + const signatureEffect = + arg.kind === 'Identifier' && i < signature.positionalParams.length + ? signature.positionalParams[i]! + : (signature.restParam ?? Effect.ConditionallyMutate); + const effect = getArgumentEffect(signatureEffect, arg); + + visit(place, effect); + } + if (captures.length !== 0) { + if (stores.length === 0) { + // If no stores, then capture into the return value + for (const capture of captures) { + effects.push({kind: 'Alias', from: capture, into: lvalue}); + } + } else { + // Else capture into the stores + for (const capture of captures) { + for (const store of stores) { + effects.push({kind: 'Capture', from: capture, into: store}); + } + } + } + } + return effects; +} + +/** + * Returns true if all of the arguments are both non-mutable (immutable or frozen) + * _and_ are not functions which might mutate their arguments. Note that function + * expressions count as frozen so long as they do not mutate free variables: this + * function checks that such functions also don't mutate their inputs. + */ +function areArgumentsImmutableAndNonMutating( + state: InferenceState, + args: Array, +): boolean { + for (const arg of args) { + if (arg.kind === 'Hole') { + continue; + } + if (arg.kind === 'Identifier' && arg.identifier.type.kind === 'Function') { + const fnShape = state.env.getFunctionSignature(arg.identifier.type); + if (fnShape != null) { + return ( + !fnShape.positionalParams.some(isKnownMutableEffect) && + (fnShape.restParam == null || + !isKnownMutableEffect(fnShape.restParam)) + ); + } + } + const place = arg.kind === 'Identifier' ? arg : arg.place; + + const kind = state.kind(place).kind; + switch (kind) { + case ValueKind.Primitive: + case ValueKind.Frozen: { + /* + * Only immutable values, or frozen lambdas are allowed. + * A lambda may appear frozen even if it may mutate its inputs, + * so we have a second check even for frozen value types + */ + break; + } + default: { + /** + * Globals, module locals, and other locally defined functions may + * mutate their arguments. + */ + return false; + } + } + const values = state.values(place); + for (const value of values) { + if ( + value.kind === 'FunctionExpression' && + value.loweredFunc.func.params.some(param => { + const place = param.kind === 'Identifier' ? param : param.place; + const range = place.identifier.mutableRange; + return range.end > range.start + 1; + }) + ) { + // This is a function which may mutate its inputs + return false; + } + } + } + return true; +} + +function computeEffectsForSignature( + env: Environment, + signature: AliasingSignature, + lvalue: Place, + receiver: Place, + args: Array, + // Used for signatures constructed dynamically which reference context variables + context: Array = [], + loc: SourceLocation, +): Array | null { + if ( + // Not enough args + signature.params.length > args.length || + // Too many args and there is no rest param to hold them + (args.length > signature.params.length && signature.rest == null) + ) { + return null; + } + // Build substitutions + const mutableSpreads = new Set(); + const substitutions: Map> = new Map(); + substitutions.set(signature.receiver, [receiver]); + substitutions.set(signature.returns, [lvalue]); + const params = signature.params; + for (let i = 0; i < args.length; i++) { + const arg = args[i]; + if (arg.kind === 'Hole') { + continue; + } else if (params == null || i >= params.length || arg.kind === 'Spread') { + if (signature.rest == null) { + return null; + } + const place = arg.kind === 'Identifier' ? arg : arg.place; + getOrInsertWith(substitutions, signature.rest, () => []).push(place); + + if (arg.kind === 'Spread') { + const mutateIterator = conditionallyMutateIterator(arg.place); + if (mutateIterator != null) { + mutableSpreads.add(arg.place.identifier.id); + } + } + } else { + const param = params[i]; + substitutions.set(param, [arg]); + } + } + + /* + * Signatures constructed dynamically from function expressions will reference values + * other than their receiver/args/etc. We populate the substitution table with these + * values so that we can still exit for unpopulated substitutions + */ + for (const operand of context) { + substitutions.set(operand.identifier.id, [operand]); + } + + const effects: Array = []; + for (const signatureTemporary of signature.temporaries) { + const temp = createTemporaryPlace(env, receiver.loc); + substitutions.set(signatureTemporary.identifier.id, [temp]); + } + + // Apply substitutions + for (const effect of signature.effects) { + switch (effect.kind) { + case 'MaybeAlias': + case 'Assign': + case 'ImmutableCapture': + case 'Alias': + case 'CreateFrom': + case 'Capture': { + const from = substitutions.get(effect.from.identifier.id) ?? []; + const to = substitutions.get(effect.into.identifier.id) ?? []; + for (const fromId of from) { + for (const toId of to) { + effects.push({ + kind: effect.kind, + from: fromId, + into: toId, + }); + } + } + break; + } + case 'Impure': + case 'MutateFrozen': + case 'MutateGlobal': { + const values = substitutions.get(effect.place.identifier.id) ?? []; + for (const value of values) { + effects.push({kind: effect.kind, place: value, error: effect.error}); + } + break; + } + case 'Render': { + const values = substitutions.get(effect.place.identifier.id) ?? []; + for (const value of values) { + effects.push({kind: effect.kind, place: value}); + } + break; + } + case 'Mutate': + case 'MutateTransitive': + case 'MutateTransitiveConditionally': + case 'MutateConditionally': { + const values = substitutions.get(effect.value.identifier.id) ?? []; + for (const id of values) { + effects.push({kind: effect.kind, value: id}); + } + break; + } + case 'Freeze': { + const values = substitutions.get(effect.value.identifier.id) ?? []; + for (const value of values) { + if (mutableSpreads.has(value.identifier.id)) { + CompilerError.throwTodo({ + reason: 'Support spread syntax for hook arguments', + loc: value.loc, + }); + } + effects.push({kind: 'Freeze', value, reason: effect.reason}); + } + break; + } + case 'Create': { + const into = substitutions.get(effect.into.identifier.id) ?? []; + for (const value of into) { + effects.push({ + kind: 'Create', + into: value, + value: effect.value, + reason: effect.reason, + }); + } + break; + } + case 'Apply': { + const applyReceiver = substitutions.get(effect.receiver.identifier.id); + if (applyReceiver == null || applyReceiver.length !== 1) { + return null; + } + const applyFunction = substitutions.get(effect.function.identifier.id); + if (applyFunction == null || applyFunction.length !== 1) { + return null; + } + const applyInto = substitutions.get(effect.into.identifier.id); + if (applyInto == null || applyInto.length !== 1) { + return null; + } + const applyArgs: Array = []; + for (const arg of effect.args) { + if (arg.kind === 'Hole') { + applyArgs.push(arg); + } else if (arg.kind === 'Identifier') { + const applyArg = substitutions.get(arg.identifier.id); + if (applyArg == null || applyArg.length !== 1) { + return null; + } + applyArgs.push(applyArg[0]); + } else { + const applyArg = substitutions.get(arg.place.identifier.id); + if (applyArg == null || applyArg.length !== 1) { + return null; + } + applyArgs.push({kind: 'Spread', place: applyArg[0]}); + } + } + effects.push({ + kind: 'Apply', + mutatesFunction: effect.mutatesFunction, + receiver: applyReceiver[0], + args: applyArgs, + function: applyFunction[0], + into: applyInto[0], + signature: effect.signature, + loc, + }); + break; + } + case 'CreateFunction': { + CompilerError.throwTodo({ + reason: `Support CreateFrom effects in signatures`, + loc: receiver.loc, + }); + } + default: { + assertExhaustive( + effect, + `Unexpected effect kind '${(effect as any).kind}'`, + ); + } + } + } + return effects; +} + +function buildSignatureFromFunctionExpression( + env: Environment, + fn: FunctionExpression, +): AliasingSignature { + let rest: IdentifierId | null = null; + const params: Array = []; + for (const param of fn.loweredFunc.func.params) { + if (param.kind === 'Identifier') { + params.push(param.identifier.id); + } else { + rest = param.place.identifier.id; + } + } + return { + receiver: makeIdentifierId(0), + params, + rest: rest ?? createTemporaryPlace(env, fn.loc).identifier.id, + returns: fn.loweredFunc.func.returns.identifier.id, + effects: fn.loweredFunc.func.aliasingEffects ?? [], + temporaries: [], + }; +} + +export type AbstractValue = { + kind: ValueKind; + reason: ReadonlySet; +}; + +export function getWriteErrorReason(abstractValue: AbstractValue): string { + if (abstractValue.reason.has(ValueReason.Global)) { + return 'Modifying a variable defined outside a component or hook is not allowed. Consider using an effect'; + } else if (abstractValue.reason.has(ValueReason.JsxCaptured)) { + return 'Modifying a value used previously in JSX is not allowed. Consider moving the modification before the JSX'; + } else if (abstractValue.reason.has(ValueReason.Context)) { + return `Modifying a value returned from 'useContext()' is not allowed.`; + } else if (abstractValue.reason.has(ValueReason.KnownReturnSignature)) { + return 'Modifying a value returned from a function whose return value should not be mutated'; + } else if (abstractValue.reason.has(ValueReason.ReactiveFunctionArgument)) { + return 'Modifying component props or hook arguments is not allowed. Consider using a local variable instead'; + } else if (abstractValue.reason.has(ValueReason.State)) { + return "Modifying a value returned from 'useState()', which should not be modified directly. Use the setter function to update instead"; + } else if (abstractValue.reason.has(ValueReason.ReducerState)) { + return "Modifying a value returned from 'useReducer()', which should not be modified directly. Use the dispatch function to update instead"; + } else if (abstractValue.reason.has(ValueReason.Effect)) { + return 'Modifying a value used previously in an effect function or as an effect dependency is not allowed. Consider moving the modification before calling useEffect()'; + } else if (abstractValue.reason.has(ValueReason.HookCaptured)) { + return 'Modifying a value previously passed as an argument to a hook is not allowed. Consider moving the modification before calling the hook'; + } else if (abstractValue.reason.has(ValueReason.HookReturn)) { + return 'Modifying a value returned from a hook is not allowed. Consider moving the modification into the hook where the value is constructed'; + } else { + return 'This modifies a variable that React considers immutable'; + } +} + +function getArgumentEffect( + signatureEffect: Effect | null, + arg: Place | SpreadPattern, +): Effect { + if (signatureEffect != null) { + if (arg.kind === 'Identifier') { + return signatureEffect; + } else if ( + signatureEffect === Effect.Mutate || + signatureEffect === Effect.ConditionallyMutate + ) { + return signatureEffect; + } else { + // see call-spread-argument-mutable-iterator test fixture + if (signatureEffect === Effect.Freeze) { + CompilerError.throwTodo({ + reason: 'Support spread syntax for hook arguments', + loc: arg.place.loc, + }); + } + // effects[i] is Effect.Capture | Effect.Read | Effect.Store + return Effect.ConditionallyMutateIterator; + } + } else { + return Effect.ConditionallyMutate; + } +} + +export function getFunctionCallSignature( + env: Environment, + type: Type, +): FunctionSignature | null { + if (type.kind !== 'Function') { + return null; + } + return env.getFunctionSignature(type); +} + +export function isKnownMutableEffect(effect: Effect): boolean { + switch (effect) { + case Effect.Store: + case Effect.ConditionallyMutate: + case Effect.ConditionallyMutateIterator: + case Effect.Mutate: { + return true; + } + + case Effect.Unknown: { + CompilerError.invariant(false, { + reason: 'Unexpected unknown effect', + description: null, + loc: GeneratedSource, + suggestions: null, + }); + } + case Effect.Read: + case Effect.Capture: + case Effect.Freeze: { + return false; + } + default: { + assertExhaustive(effect, `Unexpected effect \`${effect}\``); + } + } +} + +/** + * Joins two values using the following rules: + * == Effect Transitions == + * + * Freezing an immutable value has not effect: + * ┌───────────────┐ + * │ │ + * ▼ │ Freeze + * ┌──────────────────────────┐ │ + * │ Immutable │──┘ + * └──────────────────────────┘ + * + * Freezing a mutable or maybe-frozen value makes it frozen. Freezing a frozen + * value has no effect: + * ┌───────────────┐ + * ┌─────────────────────────┐ Freeze │ │ + * │ MaybeFrozen │────┐ ▼ │ Freeze + * └─────────────────────────┘ │ ┌──────────────────────────┐ │ + * ├────▶│ Frozen │──┘ + * │ └──────────────────────────┘ + * ┌─────────────────────────┐ │ + * │ Mutable │────┘ + * └─────────────────────────┘ + * + * == Join Lattice == + * - immutable | mutable => mutable + * The justification is that immutable and mutable values are different types, + * and functions can introspect them to tell the difference (if the argument + * is null return early, else if its an object mutate it). + * - frozen | mutable => maybe-frozen + * Frozen values are indistinguishable from mutable values at runtime, so callers + * cannot dynamically avoid mutation of "frozen" values. If a value could be + * frozen we have to distinguish it from a mutable value. But it also isn't known + * frozen yet, so we distinguish as maybe-frozen. + * - immutable | frozen => frozen + * This is subtle and falls out of the above rules. If a value could be any of + * immutable, mutable, or frozen, then at runtime it could either be a primitive + * or a reference type, and callers can't distinguish frozen or not for reference + * types. To ensure that any sequence of joins btw those three states yields the + * correct maybe-frozen, these two have to produce a frozen value. + * - | maybe-frozen => maybe-frozen + * - immutable | context => context + * - mutable | context => context + * - frozen | context => maybe-frozen + * + * ┌──────────────────────────┐ + * │ Immutable │───┐ + * └──────────────────────────┘ │ + * │ ┌─────────────────────────┐ + * ├───▶│ Frozen │──┐ + * ┌──────────────────────────┐ │ └─────────────────────────┘ │ + * │ Frozen │───┤ │ ┌─────────────────────────┐ + * └──────────────────────────┘ │ ├─▶│ MaybeFrozen │ + * │ ┌─────────────────────────┐ │ └─────────────────────────┘ + * ├───▶│ MaybeFrozen │──┘ + * ┌──────────────────────────┐ │ └─────────────────────────┘ + * │ Mutable │───┘ + * └──────────────────────────┘ + */ +function mergeValueKinds(a: ValueKind, b: ValueKind): ValueKind { + if (a === b) { + return a; + } else if (a === ValueKind.MaybeFrozen || b === ValueKind.MaybeFrozen) { + return ValueKind.MaybeFrozen; + // after this a and b differ and neither are MaybeFrozen + } else if (a === ValueKind.Mutable || b === ValueKind.Mutable) { + if (a === ValueKind.Frozen || b === ValueKind.Frozen) { + // frozen | mutable + return ValueKind.MaybeFrozen; + } else if (a === ValueKind.Context || b === ValueKind.Context) { + // context | mutable + return ValueKind.Context; + } else { + // mutable | immutable + return ValueKind.Mutable; + } + } else if (a === ValueKind.Context || b === ValueKind.Context) { + if (a === ValueKind.Frozen || b === ValueKind.Frozen) { + // frozen | context + return ValueKind.MaybeFrozen; + } else { + // context | immutable + return ValueKind.Context; + } + } else if (a === ValueKind.Frozen || b === ValueKind.Frozen) { + return ValueKind.Frozen; + } else if (a === ValueKind.Global || b === ValueKind.Global) { + return ValueKind.Global; + } else { + CompilerError.invariant( + a === ValueKind.Primitive && b == ValueKind.Primitive, + { + reason: `Unexpected value kind in mergeValues()`, + description: `Found kinds ${a} and ${b}`, + loc: GeneratedSource, + }, + ); + return ValueKind.Primitive; + } +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutationAliasingRanges.ts b/compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutationAliasingRanges.ts new file mode 100644 index 0000000000000..b53026a4d4b87 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutationAliasingRanges.ts @@ -0,0 +1,828 @@ +/** + * 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, SourceLocation} from '..'; +import { + BlockId, + Effect, + HIRFunction, + Identifier, + IdentifierId, + InstructionId, + isJsxType, + makeInstructionId, + ValueKind, + ValueReason, + Place, + isPrimitiveType, +} from '../HIR/HIR'; +import { + eachInstructionLValue, + eachInstructionValueOperand, + eachTerminalOperand, +} from '../HIR/visitors'; +import {assertExhaustive, getOrInsertWith} from '../Utils/utils'; +import {Err, Ok, Result} from '../Utils/Result'; +import {AliasingEffect, MutationReason} from './AliasingEffects'; + +/** + * This pass builds an abstract model of the heap and interprets the effects of the + * given function in order to determine the following: + * - The mutable ranges of all identifiers in the function + * - The externally-visible effects of the function, such as mutations of params and + * context-vars, aliasing between params/context-vars/return-value, and impure side + * effects. + * - The legacy `Effect` to store on each Place. + * + * This pass builds a data flow graph using the effects, tracking an abstract notion + * of "when" each effect occurs relative to the others. It then walks each mutation + * effect against the graph, updating the range of each node that would be reachable + * at the "time" that the effect occurred. + * + * This pass also validates against invalid effects: any function that is reachable + * by being called, or via a Render effect, is validated against mutating globals + * or calling impure code. + * + * Note that this function also populates the outer function's aliasing effects with + * any mutations that apply to its params or context variables. + * + * ## Example + * A function expression such as the following: + * + * ``` + * (x) => { x.y = true } + * ``` + * + * Would populate a `Mutate x` aliasing effect on the outer function. + * + * ## Returned Function Effects + * + * The function returns (if successful) a list of externally-visible effects. + * This is determined by simulating a conditional, transitive mutation against + * each param, context variable, and return value in turn, and seeing which other + * such values are affected. If they're affected, they must be captured, so we + * record a Capture. + * + * The only tricky bit is the return value, which could _alias_ (or even assign) + * one or more of the params/context-vars rather than just capturing. So we have + * to do a bit more tracking for returns. + */ +export function inferMutationAliasingRanges( + fn: HIRFunction, + {isFunctionExpression}: {isFunctionExpression: boolean}, +): Result, CompilerError> { + // The set of externally-visible effects + const functionEffects: Array = []; + + /** + * Part 1: Infer mutable ranges for values. We build an abstract model of + * values, the alias/capture edges between them, and the set of mutations. + * Edges and mutations are ordered, with mutations processed against the + * abstract model only after it is fully constructed by visiting all blocks + * _and_ connecting phis. Phis are considered ordered at the time of the + * phi node. + * + * This should (may?) mean that mutations are able to see the full state + * of the graph and mark all the appropriate identifiers as mutated at + * the correct point, accounting for both backward and forward edges. + * Ie a mutation of x accounts for both values that flowed into x, + * and values that x flowed into. + */ + const state = new AliasingState(); + type PendingPhiOperand = {from: Place; into: Place; index: number}; + const pendingPhis = new Map>(); + const mutations: Array<{ + index: number; + id: InstructionId; + transitive: boolean; + kind: MutationKind; + place: Place; + reason: MutationReason | null; + }> = []; + const renders: Array<{index: number; place: Place}> = []; + + let index = 0; + + const errors = new CompilerError(); + + for (const param of [...fn.params, ...fn.context, fn.returns]) { + const place = param.kind === 'Identifier' ? param : param.place; + state.create(place, {kind: 'Object'}); + } + const seenBlocks = new Set(); + for (const block of fn.body.blocks.values()) { + for (const phi of block.phis) { + state.create(phi.place, {kind: 'Phi'}); + for (const [pred, operand] of phi.operands) { + if (!seenBlocks.has(pred)) { + // NOTE: annotation required to actually typecheck and not silently infer `any` + const blockPhis = getOrInsertWith>( + pendingPhis, + pred, + () => [], + ); + blockPhis.push({from: operand, into: phi.place, index: index++}); + } else { + state.assign(index++, operand, phi.place); + } + } + } + seenBlocks.add(block.id); + + for (const instr of block.instructions) { + if (instr.effects == null) continue; + for (const effect of instr.effects) { + if (effect.kind === 'Create') { + state.create(effect.into, {kind: 'Object'}); + } else if (effect.kind === 'CreateFunction') { + state.create(effect.into, { + kind: 'Function', + function: effect.function.loweredFunc.func, + }); + } else if (effect.kind === 'CreateFrom') { + state.createFrom(index++, effect.from, effect.into); + } else if (effect.kind === 'Assign') { + /** + * TODO: Invariant that the node is not initialized yet + * + * InferFunctionExpressionAliasingEffectSignatures currently infers + * Assign effects in some places that should be Alias, leading to + * Assign effects that reinitialize a value. The end result appears to + * be fine, but we should fix that inference pass so that we add the + * invariant here. + */ + if (!state.nodes.has(effect.into.identifier)) { + state.create(effect.into, {kind: 'Object'}); + } + state.assign(index++, effect.from, effect.into); + } else if (effect.kind === 'Alias') { + state.assign(index++, effect.from, effect.into); + } else if (effect.kind === 'MaybeAlias') { + state.maybeAlias(index++, effect.from, effect.into); + } else if (effect.kind === 'Capture') { + state.capture(index++, effect.from, effect.into); + } else if ( + effect.kind === 'MutateTransitive' || + effect.kind === 'MutateTransitiveConditionally' + ) { + mutations.push({ + index: index++, + id: instr.id, + transitive: true, + kind: + effect.kind === 'MutateTransitive' + ? MutationKind.Definite + : MutationKind.Conditional, + reason: null, + place: effect.value, + }); + } else if ( + effect.kind === 'Mutate' || + effect.kind === 'MutateConditionally' + ) { + mutations.push({ + index: index++, + id: instr.id, + transitive: false, + kind: + effect.kind === 'Mutate' + ? MutationKind.Definite + : MutationKind.Conditional, + reason: effect.kind === 'Mutate' ? (effect.reason ?? null) : null, + place: effect.value, + }); + } else if ( + effect.kind === 'MutateFrozen' || + effect.kind === 'MutateGlobal' || + effect.kind === 'Impure' + ) { + errors.pushDiagnostic(effect.error); + functionEffects.push(effect); + } else if (effect.kind === 'Render') { + renders.push({index: index++, place: effect.place}); + functionEffects.push(effect); + } + } + } + const blockPhis = pendingPhis.get(block.id); + if (blockPhis != null) { + for (const {from, into, index} of blockPhis) { + state.assign(index, from, into); + } + } + if (block.terminal.kind === 'return') { + state.assign(index++, block.terminal.value, fn.returns); + } + + if ( + (block.terminal.kind === 'maybe-throw' || + block.terminal.kind === 'return') && + block.terminal.effects != null + ) { + for (const effect of block.terminal.effects) { + if (effect.kind === 'Alias') { + state.assign(index++, effect.from, effect.into); + } else { + CompilerError.invariant(effect.kind === 'Freeze', { + reason: `Unexpected '${effect.kind}' effect for MaybeThrow terminal`, + loc: block.terminal.loc, + }); + } + } + } + } + + for (const mutation of mutations) { + state.mutate( + mutation.index, + mutation.place.identifier, + makeInstructionId(mutation.id + 1), + mutation.transitive, + mutation.kind, + mutation.place.loc, + mutation.reason, + errors, + ); + } + for (const render of renders) { + state.render(render.index, render.place.identifier, errors); + } + for (const param of [...fn.context, ...fn.params]) { + const place = param.kind === 'Identifier' ? param : param.place; + + const node = state.nodes.get(place.identifier); + if (node == null) { + continue; + } + let mutated = false; + if (node.local != null) { + if (node.local.kind === MutationKind.Conditional) { + mutated = true; + functionEffects.push({ + kind: 'MutateConditionally', + value: {...place, loc: node.local.loc}, + }); + } else if (node.local.kind === MutationKind.Definite) { + mutated = true; + functionEffects.push({ + kind: 'Mutate', + value: {...place, loc: node.local.loc}, + reason: node.mutationReason, + }); + } + } + if (node.transitive != null) { + if (node.transitive.kind === MutationKind.Conditional) { + mutated = true; + functionEffects.push({ + kind: 'MutateTransitiveConditionally', + value: {...place, loc: node.transitive.loc}, + }); + } else if (node.transitive.kind === MutationKind.Definite) { + mutated = true; + functionEffects.push({ + kind: 'MutateTransitive', + value: {...place, loc: node.transitive.loc}, + }); + } + } + if (mutated) { + place.effect = Effect.Capture; + } + } + + /** + * Part 2 + * Add legacy operand-specific effects based on instruction effects and mutable ranges. + * Also fixes up operand mutable ranges, making sure that start is non-zero if the value + * is mutated (depended on by later passes like InferReactiveScopeVariables which uses this + * to filter spurious mutations of globals, which we now guard against more precisely) + */ + for (const block of fn.body.blocks.values()) { + for (const phi of block.phis) { + // TODO: we don't actually set these effects today! + phi.place.effect = Effect.Store; + const isPhiMutatedAfterCreation: boolean = + phi.place.identifier.mutableRange.end > + (block.instructions.at(0)?.id ?? block.terminal.id); + for (const operand of phi.operands.values()) { + operand.effect = isPhiMutatedAfterCreation + ? Effect.Capture + : Effect.Read; + } + if ( + isPhiMutatedAfterCreation && + phi.place.identifier.mutableRange.start === 0 + ) { + /* + * TODO: ideally we'd construct a precise start range, but what really + * matters is that the phi's range appears mutable (end > start + 1) + * so we just set the start to the previous instruction before this block + */ + const firstInstructionIdOfBlock = + block.instructions.at(0)?.id ?? block.terminal.id; + phi.place.identifier.mutableRange.start = makeInstructionId( + firstInstructionIdOfBlock - 1, + ); + } + } + for (const instr of block.instructions) { + for (const lvalue of eachInstructionLValue(instr)) { + lvalue.effect = Effect.ConditionallyMutate; + if (lvalue.identifier.mutableRange.start === 0) { + lvalue.identifier.mutableRange.start = instr.id; + } + if (lvalue.identifier.mutableRange.end === 0) { + lvalue.identifier.mutableRange.end = makeInstructionId( + Math.max(instr.id + 1, lvalue.identifier.mutableRange.end), + ); + } + } + for (const operand of eachInstructionValueOperand(instr.value)) { + operand.effect = Effect.Read; + } + if (instr.effects == null) { + continue; + } + const operandEffects = new Map(); + for (const effect of instr.effects) { + switch (effect.kind) { + case 'Assign': + case 'Alias': + case 'Capture': + case 'CreateFrom': + case 'MaybeAlias': { + const isMutatedOrReassigned = + effect.into.identifier.mutableRange.end > instr.id; + if (isMutatedOrReassigned) { + operandEffects.set(effect.from.identifier.id, Effect.Capture); + operandEffects.set(effect.into.identifier.id, Effect.Store); + } else { + operandEffects.set(effect.from.identifier.id, Effect.Read); + operandEffects.set(effect.into.identifier.id, Effect.Store); + } + break; + } + case 'CreateFunction': + case 'Create': { + break; + } + case 'Mutate': { + operandEffects.set(effect.value.identifier.id, Effect.Store); + break; + } + case 'Apply': { + CompilerError.invariant(false, { + reason: `[AnalyzeFunctions] Expected Apply effects to be replaced with more precise effects`, + loc: effect.function.loc, + }); + } + case 'MutateTransitive': + case 'MutateConditionally': + case 'MutateTransitiveConditionally': { + operandEffects.set( + effect.value.identifier.id, + Effect.ConditionallyMutate, + ); + break; + } + case 'Freeze': { + operandEffects.set(effect.value.identifier.id, Effect.Freeze); + break; + } + case 'ImmutableCapture': { + // no-op, Read is the default + break; + } + case 'Impure': + case 'Render': + case 'MutateFrozen': + case 'MutateGlobal': { + // no-op + break; + } + default: { + assertExhaustive( + effect, + `Unexpected effect kind ${(effect as any).kind}`, + ); + } + } + } + for (const lvalue of eachInstructionLValue(instr)) { + const effect = + operandEffects.get(lvalue.identifier.id) ?? + Effect.ConditionallyMutate; + lvalue.effect = effect; + } + for (const operand of eachInstructionValueOperand(instr.value)) { + if ( + operand.identifier.mutableRange.end > instr.id && + operand.identifier.mutableRange.start === 0 + ) { + operand.identifier.mutableRange.start = instr.id; + } + const effect = operandEffects.get(operand.identifier.id) ?? Effect.Read; + operand.effect = effect; + } + + /** + * This case is targeted at hoisted functions like: + * + * ``` + * x(); + * function x() { ... } + * ``` + * + * Which turns into: + * + * t0 = DeclareContext HoistedFunction x + * t1 = LoadContext x + * t2 = CallExpression t1 ( ) + * t3 = FunctionExpression ... + * t4 = StoreContext Function x = t3 + * + * If the function had captured mutable values, it would already have its + * range extended to include the StoreContext. But if the function doesn't + * capture any mutable values its range won't have been extended yet. We + * want to ensure that the value is memoized along with the context variable, + * not independently of it (bc of the way we do codegen for hoisted functions). + * So here we check for StoreContext rvalues and if they haven't already had + * their range extended to at least this instruction, we extend it. + */ + if ( + instr.value.kind === 'StoreContext' && + instr.value.value.identifier.mutableRange.end <= instr.id + ) { + instr.value.value.identifier.mutableRange.end = makeInstructionId( + instr.id + 1, + ); + } + } + if (block.terminal.kind === 'return') { + block.terminal.value.effect = isFunctionExpression + ? Effect.Read + : Effect.Freeze; + } else { + for (const operand of eachTerminalOperand(block.terminal)) { + operand.effect = Effect.Read; + } + } + } + + /** + * Part 3 + * Finish populating the externally visible effects. Above we bubble-up the side effects + * (MutateFrozen/MutableGlobal/Impure/Render) as well as mutations of context variables. + * Here we populate an effect to create the return value as well as populating alias/capture + * effects for how data flows between the params, context vars, and return. + */ + const returns = fn.returns.identifier; + functionEffects.push({ + kind: 'Create', + into: fn.returns, + value: isPrimitiveType(returns) + ? ValueKind.Primitive + : isJsxType(returns.type) + ? ValueKind.Frozen + : ValueKind.Mutable, + reason: ValueReason.KnownReturnSignature, + }); + /** + * Determine precise data-flow effects by simulating transitive mutations of the params/ + * captures and seeing what other params/context variables are affected. Anything that + * would be transitively mutated needs a capture relationship. + */ + const tracked: Array = []; + const ignoredErrors = new CompilerError(); + for (const param of [...fn.params, ...fn.context, fn.returns]) { + const place = param.kind === 'Identifier' ? param : param.place; + tracked.push(place); + } + for (const into of tracked) { + const mutationIndex = index++; + state.mutate( + mutationIndex, + into.identifier, + null, + true, + MutationKind.Conditional, + into.loc, + null, + ignoredErrors, + ); + for (const from of tracked) { + if ( + from.identifier.id === into.identifier.id || + from.identifier.id === fn.returns.identifier.id + ) { + continue; + } + const fromNode = state.nodes.get(from.identifier); + CompilerError.invariant(fromNode != null, { + reason: `Expected a node to exist for all parameters and context variables`, + loc: into.loc, + }); + if (fromNode.lastMutated === mutationIndex) { + if (into.identifier.id === fn.returns.identifier.id) { + // The return value could be any of the params/context variables + functionEffects.push({ + kind: 'Alias', + from, + into, + }); + } else { + // Otherwise params/context-vars can only capture each other + functionEffects.push({ + kind: 'Capture', + from, + into, + }); + } + } + } + } + + if (errors.hasErrors() && !isFunctionExpression) { + return Err(errors); + } + return Ok(functionEffects); +} + +function appendFunctionErrors(errors: CompilerError, fn: HIRFunction): void { + for (const effect of fn.aliasingEffects ?? []) { + switch (effect.kind) { + case 'Impure': + case 'MutateFrozen': + case 'MutateGlobal': { + errors.pushDiagnostic(effect.error); + break; + } + } + } +} + +export enum MutationKind { + None = 0, + Conditional = 1, + Definite = 2, +} + +type Node = { + id: Identifier; + createdFrom: Map; + captures: Map; + aliases: Map; + maybeAliases: Map; + edges: Array<{ + index: number; + node: Identifier; + kind: 'capture' | 'alias' | 'maybeAlias'; + }>; + transitive: {kind: MutationKind; loc: SourceLocation} | null; + local: {kind: MutationKind; loc: SourceLocation} | null; + lastMutated: number; + mutationReason: MutationReason | null; + value: + | {kind: 'Object'} + | {kind: 'Phi'} + | {kind: 'Function'; function: HIRFunction}; +}; +class AliasingState { + nodes: Map = new Map(); + + create(place: Place, value: Node['value']): void { + this.nodes.set(place.identifier, { + id: place.identifier, + createdFrom: new Map(), + captures: new Map(), + aliases: new Map(), + maybeAliases: new Map(), + edges: [], + transitive: null, + local: null, + lastMutated: 0, + mutationReason: null, + value, + }); + } + + createFrom(index: number, from: Place, into: Place): void { + this.create(into, {kind: 'Object'}); + const fromNode = this.nodes.get(from.identifier); + const toNode = this.nodes.get(into.identifier); + if (fromNode == null || toNode == null) { + return; + } + fromNode.edges.push({index, node: into.identifier, kind: 'alias'}); + if (!toNode.createdFrom.has(from.identifier)) { + toNode.createdFrom.set(from.identifier, index); + } + } + + capture(index: number, from: Place, into: Place): void { + const fromNode = this.nodes.get(from.identifier); + const toNode = this.nodes.get(into.identifier); + if (fromNode == null || toNode == null) { + return; + } + fromNode.edges.push({index, node: into.identifier, kind: 'capture'}); + if (!toNode.captures.has(from.identifier)) { + toNode.captures.set(from.identifier, index); + } + } + + assign(index: number, from: Place, into: Place): void { + const fromNode = this.nodes.get(from.identifier); + const toNode = this.nodes.get(into.identifier); + if (fromNode == null || toNode == null) { + return; + } + fromNode.edges.push({index, node: into.identifier, kind: 'alias'}); + if (!toNode.aliases.has(from.identifier)) { + toNode.aliases.set(from.identifier, index); + } + } + + maybeAlias(index: number, from: Place, into: Place): void { + const fromNode = this.nodes.get(from.identifier); + const toNode = this.nodes.get(into.identifier); + if (fromNode == null || toNode == null) { + return; + } + fromNode.edges.push({index, node: into.identifier, kind: 'maybeAlias'}); + if (!toNode.maybeAliases.has(from.identifier)) { + toNode.maybeAliases.set(from.identifier, index); + } + } + + render(index: number, start: Identifier, errors: CompilerError): void { + const seen = new Set(); + const queue: Array = [start]; + while (queue.length !== 0) { + const current = queue.pop()!; + if (seen.has(current)) { + continue; + } + seen.add(current); + const node = this.nodes.get(current); + if (node == null || node.transitive != null || node.local != null) { + continue; + } + if (node.value.kind === 'Function') { + appendFunctionErrors(errors, node.value.function); + } + for (const [alias, when] of node.createdFrom) { + if (when >= index) { + continue; + } + queue.push(alias); + } + for (const [alias, when] of node.aliases) { + if (when >= index) { + continue; + } + queue.push(alias); + } + for (const [capture, when] of node.captures) { + if (when >= index) { + continue; + } + queue.push(capture); + } + } + } + + mutate( + index: number, + start: Identifier, + // Null is used for simulated mutations + end: InstructionId | null, + transitive: boolean, + startKind: MutationKind, + loc: SourceLocation, + reason: MutationReason | null, + errors: CompilerError, + ): void { + const seen = new Map(); + const queue: Array<{ + place: Identifier; + transitive: boolean; + direction: 'backwards' | 'forwards'; + kind: MutationKind; + }> = [{place: start, transitive, direction: 'backwards', kind: startKind}]; + while (queue.length !== 0) { + const {place: current, transitive, direction, kind} = queue.pop()!; + const previousKind = seen.get(current); + if (previousKind != null && previousKind >= kind) { + continue; + } + seen.set(current, kind); + const node = this.nodes.get(current); + if (node == null) { + continue; + } + node.mutationReason ??= reason; + node.lastMutated = Math.max(node.lastMutated, index); + if (end != null) { + node.id.mutableRange.end = makeInstructionId( + Math.max(node.id.mutableRange.end, end), + ); + } + if ( + node.value.kind === 'Function' && + node.transitive == null && + node.local == null + ) { + appendFunctionErrors(errors, node.value.function); + } + if (transitive) { + if (node.transitive == null || node.transitive.kind < kind) { + node.transitive = {kind, loc}; + } + } else { + if (node.local == null || node.local.kind < kind) { + node.local = {kind, loc}; + } + } + /** + * all mutations affect "forward" edges by the rules: + * - Capture a -> b, mutate(a) => mutate(b) + * - Alias a -> b, mutate(a) => mutate(b) + */ + for (const edge of node.edges) { + if (edge.index >= index) { + break; + } + queue.push({place: edge.node, transitive, direction: 'forwards', kind}); + } + for (const [alias, when] of node.createdFrom) { + if (when >= index) { + continue; + } + queue.push({ + place: alias, + transitive: true, + direction: 'backwards', + kind, + }); + } + if (direction === 'backwards' || node.value.kind !== 'Phi') { + /** + * all mutations affect backward alias edges by the rules: + * - Alias a -> b, mutate(b) => mutate(a) + * - Alias a -> b, mutateTransitive(b) => mutate(a) + * + * However, if we reached a phi because one of its inputs was mutated + * (and we're advancing "forwards" through that node's edges), then + * we know we've already processed the mutation at its source. The + * phi's other inputs can't be affected. + */ + for (const [alias, when] of node.aliases) { + if (when >= index) { + continue; + } + queue.push({place: alias, transitive, direction: 'backwards', kind}); + } + /** + * MaybeAlias indicates potential data flow from unknown function calls, + * so we downgrade mutations through these aliases to consider them + * conditional. This means we'll consider them for mutation *range* + * purposes but not report validation errors for mutations, since + * we aren't sure that the `from` value could actually be aliased. + */ + for (const [alias, when] of node.maybeAliases) { + if (when >= index) { + continue; + } + queue.push({ + place: alias, + transitive, + direction: 'backwards', + kind: MutationKind.Conditional, + }); + } + } + /** + * but only transitive mutations affect captures + */ + if (transitive) { + for (const [capture, when] of node.captures) { + if (when >= index) { + continue; + } + queue.push({ + place: capture, + transitive, + direction: 'backwards', + kind, + }); + } + } + } + } +} 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 b05b292124c72..19e220b235694 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Inference/InferReactivePlaces.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Inference/InferReactivePlaces.ts @@ -21,11 +21,11 @@ import { isStableType, isStableTypeContainer, isUseOperator, - isUseRefType, } from '../HIR'; import {PostDominator} from '../HIR/Dominator'; import { eachInstructionLValue, + eachInstructionOperand, eachInstructionValueOperand, eachTerminalOperand, } from '../HIR/visitors'; @@ -69,13 +69,6 @@ class StableSidemap { isStable: false, }); } - } else if ( - this.env.config.enableTreatRefLikeIdentifiersAsRefs && - isUseRefType(lvalue.identifier) - ) { - this.map.set(lvalue.identifier.id, { - isStable: true, - }); } break; } @@ -292,7 +285,7 @@ export function inferReactivePlaces(fn: HIRFunction): void { let hasReactiveInput = false; /* * NOTE: we want to mark all operands as reactive or not, so we - * avoid short-circuting here + * avoid short-circuiting here */ for (const operand of eachInstructionValueOperand(value)) { const reactive = reactiveIdentifiers.isReactive(operand); @@ -375,6 +368,41 @@ export function inferReactivePlaces(fn: HIRFunction): void { } } } while (reactiveIdentifiers.snapshot()); + + function propagateReactivityToInnerFunctions( + fn: HIRFunction, + isOutermost: boolean, + ): void { + for (const [, block] of fn.body.blocks) { + for (const instr of block.instructions) { + if (!isOutermost) { + for (const operand of eachInstructionOperand(instr)) { + reactiveIdentifiers.isReactive(operand); + } + } + if ( + instr.value.kind === 'ObjectMethod' || + instr.value.kind === 'FunctionExpression' + ) { + propagateReactivityToInnerFunctions( + instr.value.loweredFunc.func, + false, + ); + } + } + if (!isOutermost) { + for (const operand of eachTerminalOperand(block.terminal)) { + reactiveIdentifiers.isReactive(operand); + } + } + } + } + + /** + * Propagate reactivity for inner functions, as we eventually hoist and dedupe + * dependency instructions for scopes. + */ + propagateReactivityToInnerFunctions(fn, true); } /* diff --git a/compiler/packages/babel-plugin-react-compiler/src/Inference/InferReferenceEffects.ts b/compiler/packages/babel-plugin-react-compiler/src/Inference/InferReferenceEffects.ts deleted file mode 100644 index d1546038edcbe..0000000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/Inference/InferReferenceEffects.ts +++ /dev/null @@ -1,2137 +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, CompilerErrorDetailOptions} from '../CompilerError'; -import {Environment} from '../HIR'; -import { - AbstractValue, - BasicBlock, - BlockId, - CallExpression, - NewExpression, - Effect, - FunctionEffect, - GeneratedSource, - HIRFunction, - IdentifierId, - InstructionKind, - InstructionValue, - MethodCall, - Phi, - Place, - SpreadPattern, - TInstruction, - Type, - ValueKind, - ValueReason, - isArrayType, - isMapType, - isMutableEffect, - isObjectType, - isSetType, -} from '../HIR/HIR'; -import {FunctionSignature} from '../HIR/ObjectShape'; -import { - printIdentifier, - printMixedHIR, - printPlace, - printSourceLocation, -} from '../HIR/PrintHIR'; -import { - eachInstructionOperand, - eachInstructionValueOperand, - eachPatternOperand, - eachTerminalOperand, - eachTerminalSuccessor, -} from '../HIR/visitors'; -import {assertExhaustive} from '../Utils/utils'; -import { - inferTerminalFunctionEffects, - inferInstructionFunctionEffects, - transformFunctionEffectErrors, -} from './InferFunctionEffects'; - -const UndefinedValue: InstructionValue = { - kind: 'Primitive', - loc: GeneratedSource, - value: undefined, -}; - -/* - * For every usage of a value in the given function, infers the effect or action - * taken at that reference. Each reference is inferred as exactly one of: - * - freeze: this usage freezes the value, ie converts it to frozen. This is only inferred - * when the value *may* not already be frozen. - * - frozen: the value is known to already be "owned" by React and is therefore already - * frozen (permanently and transitively immutable). - * - immutable: the value is not owned by React, but is known to be an immutable value - * that therefore cannot ever change. - * - readonly: the value is not frozen or immutable, but this usage of the value does - * not modify it. the value may be mutated by a subsequent reference. Examples include - * referencing the operands of a binary expression, or referencing the items/properties - * of an array or object literal. - * - mutable: the value is not frozen or immutable, and this usage *may* modify it. - * Examples include passing a value to as a function argument or assigning into an object. - * - * Note that the inference follows variable assignment, so assigning a frozen value - * to a different value will infer usages of the other variable as frozen as well. - * - * The inference assumes that the code follows the rules of React: - * - React function arguments are frozen (component props, hook arguments). - * - Hook arguments are frozen at the point the hook is invoked. - * - React function return values are frozen at the point of being returned, - * thus the return value of a hook call is frozen. - * - JSX represents invocation of a React function (the component) and - * therefore all values passed to JSX become frozen at the point the JSX - * is created. - * - * Internally, the inference tracks the approximate type of value held by each variable, - * and iterates over the control flow graph. The inferred effect of reach reference is - * a combination of the operation performed (ie, assignment into an object mutably uses the - * object; an if condition reads the condition) and the type of the value. The types of values - * are: - * - frozen: can be any type so long as the value is known to be owned by React, permanently - * and transitively immutable - * - maybe-frozen: the value may or may not be frozen, conditionally depending on control flow. - * - immutable: a type with value semantics: primitives, records/tuples when standardized. - * - mutable: a type with reference semantics eg array, object, class instance, etc. - * - * When control flow paths converge the types of values are merged together, with the value - * types forming a lattice to ensure convergence. - */ -export default function inferReferenceEffects( - fn: HIRFunction, - options: {isFunctionExpression: boolean} = {isFunctionExpression: false}, -): Array { - /* - * Initial state contains function params - * TODO: include module declarations here as well - */ - const initialState = InferenceState.empty( - fn.env, - options.isFunctionExpression, - ); - const value: InstructionValue = { - kind: 'Primitive', - loc: fn.loc, - value: undefined, - }; - initialState.initialize(value, { - kind: ValueKind.Frozen, - reason: new Set([ValueReason.Other]), - context: new Set(), - }); - - for (const ref of fn.context) { - // TODO(gsn): This is a hack. - const value: InstructionValue = { - kind: 'ObjectExpression', - properties: [], - loc: ref.loc, - }; - initialState.initialize(value, { - kind: ValueKind.Context, - reason: new Set([ValueReason.Other]), - context: new Set([ref]), - }); - initialState.define(ref, value); - } - - const paramKind: AbstractValue = options.isFunctionExpression - ? { - kind: ValueKind.Mutable, - reason: new Set([ValueReason.Other]), - context: new Set(), - } - : { - kind: ValueKind.Frozen, - reason: new Set([ValueReason.ReactiveFunctionArgument]), - context: new Set(), - }; - - if (fn.fnType === 'Component') { - CompilerError.invariant(fn.params.length <= 2, { - reason: - 'Expected React component to have not more than two parameters: one for props and for ref', - description: null, - loc: fn.loc, - suggestions: null, - }); - const [props, ref] = fn.params; - let value: InstructionValue; - let place: Place; - if (props) { - inferParam(props, initialState, paramKind); - } - if (ref) { - if (ref.kind === 'Identifier') { - place = ref; - value = { - kind: 'ObjectExpression', - properties: [], - loc: ref.loc, - }; - } else { - place = ref.place; - value = { - kind: 'ObjectExpression', - properties: [], - loc: ref.place.loc, - }; - } - initialState.initialize(value, { - kind: ValueKind.Mutable, - reason: new Set([ValueReason.Other]), - context: new Set(), - }); - initialState.define(place, value); - } - } else { - for (const param of fn.params) { - inferParam(param, initialState, paramKind); - } - } - - // Map of blocks to the last (merged) incoming state that was processed - const statesByBlock: Map = new Map(); - - /* - * Multiple predecessors may be visited prior to reaching a given successor, - * so track the list of incoming state for each successor block. - * These are merged when reaching that block again. - */ - const queuedStates: Map = new Map(); - function queue(blockId: BlockId, state: InferenceState): void { - let queuedState = queuedStates.get(blockId); - if (queuedState != null) { - // merge the queued states for this block - state = queuedState.merge(state) ?? queuedState; - queuedStates.set(blockId, state); - } else { - /* - * this is the first queued state for this block, see whether - * there are changed relative to the last time it was processed. - */ - const prevState = statesByBlock.get(blockId); - const nextState = prevState != null ? prevState.merge(state) : state; - if (nextState != null) { - queuedStates.set(blockId, nextState); - } - } - } - queue(fn.body.entry, initialState); - - const functionEffects: Array = fn.effects ?? []; - - while (queuedStates.size !== 0) { - for (const [blockId, block] of fn.body.blocks) { - const incomingState = queuedStates.get(blockId); - queuedStates.delete(blockId); - if (incomingState == null) { - continue; - } - - statesByBlock.set(blockId, incomingState); - const state = incomingState.clone(); - inferBlock(fn.env, state, block, functionEffects); - - for (const nextBlockId of eachTerminalSuccessor(block.terminal)) { - queue(nextBlockId, state); - } - } - } - - if (options.isFunctionExpression) { - fn.effects = functionEffects; - return []; - } else { - return transformFunctionEffectErrors(functionEffects); - } -} - -type FreezeAction = {values: Set; reason: Set}; - -// Maintains a mapping of top-level variables to the kind of value they hold -class InferenceState { - env: Environment; - #isFunctionExpression: boolean; - - // The kind of each value, based on its allocation site - #values: Map; - /* - * The set of values pointed to by each identifier. This is a set - * to accomodate phi points (where a variable may have different - * values from different control flow paths). - */ - #variables: Map>; - - constructor( - env: Environment, - isFunctionExpression: boolean, - values: Map, - variables: Map>, - ) { - this.env = env; - this.#isFunctionExpression = isFunctionExpression; - this.#values = values; - this.#variables = variables; - } - - static empty( - env: Environment, - isFunctionExpression: boolean, - ): InferenceState { - return new InferenceState(env, isFunctionExpression, new Map(), new Map()); - } - - get isFunctionExpression(): boolean { - return this.#isFunctionExpression; - } - - // (Re)initializes a @param value with its default @param kind. - initialize(value: InstructionValue, kind: AbstractValue): void { - CompilerError.invariant(value.kind !== 'LoadLocal', { - reason: - 'Expected all top-level identifiers to be defined as variables, not values', - description: null, - loc: value.loc, - suggestions: null, - }); - this.#values.set(value, kind); - } - - values(place: Place): Array { - const values = this.#variables.get(place.identifier.id); - CompilerError.invariant(values != null, { - reason: `[hoisting] Expected value kind to be initialized`, - description: `${printPlace(place)}`, - loc: place.loc, - suggestions: null, - }); - return Array.from(values); - } - - // Lookup the kind of the given @param value. - kind(place: Place): AbstractValue { - const values = this.#variables.get(place.identifier.id); - CompilerError.invariant(values != null, { - reason: `[hoisting] Expected value kind to be initialized`, - description: `${printPlace(place)}`, - loc: place.loc, - suggestions: null, - }); - let mergedKind: AbstractValue | null = null; - for (const value of values) { - const kind = this.#values.get(value)!; - mergedKind = - mergedKind !== null ? mergeAbstractValues(mergedKind, kind) : kind; - } - CompilerError.invariant(mergedKind !== null, { - reason: `InferReferenceEffects::kind: Expected at least one value`, - description: `No value found at \`${printPlace(place)}\``, - loc: place.loc, - suggestions: null, - }); - return mergedKind; - } - - // Updates the value at @param place to point to the same value as @param value. - alias(place: Place, value: Place): void { - const values = this.#variables.get(value.identifier.id); - CompilerError.invariant(values != null, { - reason: `[hoisting] Expected value for identifier to be initialized`, - description: `${printIdentifier(value.identifier)}`, - loc: value.loc, - suggestions: null, - }); - this.#variables.set(place.identifier.id, new Set(values)); - } - - // Defines (initializing or updating) a variable with a specific kind of value. - define(place: Place, value: InstructionValue): void { - CompilerError.invariant(this.#values.has(value), { - reason: `Expected value to be initialized at '${printSourceLocation( - value.loc, - )}'`, - description: null, - loc: value.loc, - suggestions: null, - }); - this.#variables.set(place.identifier.id, new Set([value])); - } - - isDefined(place: Place): boolean { - return this.#variables.has(place.identifier.id); - } - - /* - * Records that a given Place was accessed with the given kind and: - * - Updates the effect of @param place based on the kind of value - * and the kind of reference (@param effectKind). - * - Updates the value kind to reflect the effect of the reference. - * - * Notably, a mutable reference is downgraded to readonly if the - * value unless the value is known to be mutable. - * - * Similarly, a freeze reference is converted to readonly if the - * value is already frozen or is immutable. - */ - referenceAndRecordEffects( - freezeActions: Array, - place: Place, - effectKind: Effect, - reason: ValueReason, - ): void { - const values = this.#variables.get(place.identifier.id); - if (values === undefined) { - CompilerError.invariant(effectKind !== Effect.Store, { - reason: '[InferReferenceEffects] Unhandled store reference effect', - description: null, - loc: place.loc, - suggestions: null, - }); - place.effect = - effectKind === Effect.ConditionallyMutate - ? Effect.ConditionallyMutate - : Effect.Read; - return; - } - - const action = this.reference(place, effectKind, reason); - action && freezeActions.push(action); - } - - freezeValues(values: Set, reason: Set): void { - for (const value of values) { - if ( - value.kind === 'DeclareContext' || - (value.kind === 'StoreContext' && - (value.lvalue.kind === InstructionKind.Let || - value.lvalue.kind === InstructionKind.Const)) - ) { - /** - * Avoid freezing context variable declarations, hoisted or otherwise - * function Component() { - * const cb = useBar(() => foo(2)); // produces a hoisted context declaration - * const foo = useFoo(); // reassigns to the context variable - * return ; - * } - */ - continue; - } - this.#values.set(value, { - kind: ValueKind.Frozen, - reason, - context: new Set(), - }); - if ( - value.kind === 'FunctionExpression' && - (this.env.config.enablePreserveExistingMemoizationGuarantees || - this.env.config.enableTransitivelyFreezeFunctionExpressions) - ) { - for (const operand of value.loweredFunc.func.context) { - const operandValues = this.#variables.get(operand.identifier.id); - if (operandValues !== undefined) { - this.freezeValues(operandValues, reason); - } - } - } - } - } - - reference( - place: Place, - effectKind: Effect, - reason: ValueReason, - ): null | FreezeAction { - const values = this.#variables.get(place.identifier.id); - CompilerError.invariant(values !== undefined, { - reason: '[InferReferenceEffects] Expected value to be initialized', - description: null, - loc: place.loc, - suggestions: null, - }); - let valueKind: AbstractValue | null = this.kind(place); - let effect: Effect | null = null; - let freeze: null | FreezeAction = null; - switch (effectKind) { - case Effect.Freeze: { - if ( - valueKind.kind === ValueKind.Mutable || - valueKind.kind === ValueKind.Context || - valueKind.kind === ValueKind.MaybeFrozen - ) { - const reasonSet = new Set([reason]); - effect = Effect.Freeze; - valueKind = { - kind: ValueKind.Frozen, - reason: reasonSet, - context: new Set(), - }; - freeze = {values, reason: reasonSet}; - } else { - effect = Effect.Read; - } - break; - } - case Effect.ConditionallyMutate: { - if ( - valueKind.kind === ValueKind.Mutable || - valueKind.kind === ValueKind.Context - ) { - effect = Effect.ConditionallyMutate; - } else { - effect = Effect.Read; - } - break; - } - case Effect.ConditionallyMutateIterator: { - if ( - valueKind.kind === ValueKind.Mutable || - valueKind.kind === ValueKind.Context - ) { - if ( - isArrayType(place.identifier) || - isSetType(place.identifier) || - isMapType(place.identifier) - ) { - effect = Effect.Capture; - } else { - effect = Effect.ConditionallyMutate; - } - } else { - effect = Effect.Read; - } - break; - } - case Effect.Mutate: { - effect = Effect.Mutate; - break; - } - case Effect.Store: { - /* - * TODO(gsn): This should be bailout once we add bailout infra. - * - * invariant( - * valueKind.kind === ValueKindKind.Mutable, - * `expected valueKind to be 'Mutable' but found to be \`${valueKind}\`` - * ); - */ - effect = isObjectType(place.identifier) ? Effect.Store : Effect.Mutate; - break; - } - case Effect.Capture: { - if ( - valueKind.kind === ValueKind.Primitive || - valueKind.kind === ValueKind.Global || - valueKind.kind === ValueKind.Frozen || - valueKind.kind === ValueKind.MaybeFrozen - ) { - effect = Effect.Read; - } else { - effect = Effect.Capture; - } - break; - } - case Effect.Read: { - effect = Effect.Read; - break; - } - case Effect.Unknown: { - CompilerError.invariant(false, { - reason: - 'Unexpected unknown effect, expected to infer a precise effect kind', - description: null, - loc: place.loc, - suggestions: null, - }); - } - default: { - assertExhaustive( - effectKind, - `Unexpected reference kind \`${effectKind as any as string}\``, - ); - } - } - CompilerError.invariant(effect !== null, { - reason: 'Expected effect to be set', - description: null, - loc: place.loc, - suggestions: null, - }); - place.effect = effect; - return freeze; - } - - /* - * Combine the contents of @param this and @param other, returning a new - * instance with the combined changes _if_ there are any changes, or - * returning null if no changes would occur. Changes include: - * - new entries in @param other that did not exist in @param this - * - entries whose values differ in @param this and @param other, - * and where joining the values produces a different value than - * what was in @param this. - * - * Note that values are joined using a lattice operation to ensure - * termination. - */ - merge(other: InferenceState): InferenceState | null { - let nextValues: Map | null = null; - let nextVariables: Map> | null = null; - - for (const [id, thisValue] of this.#values) { - const otherValue = other.#values.get(id); - if (otherValue !== undefined) { - const mergedValue = mergeAbstractValues(thisValue, otherValue); - if (mergedValue !== thisValue) { - nextValues = nextValues ?? new Map(this.#values); - nextValues.set(id, mergedValue); - } - } - } - for (const [id, otherValue] of other.#values) { - if (this.#values.has(id)) { - // merged above - continue; - } - nextValues = nextValues ?? new Map(this.#values); - nextValues.set(id, otherValue); - } - - for (const [id, thisValues] of this.#variables) { - const otherValues = other.#variables.get(id); - if (otherValues !== undefined) { - let mergedValues: Set | null = null; - for (const otherValue of otherValues) { - if (!thisValues.has(otherValue)) { - mergedValues = mergedValues ?? new Set(thisValues); - mergedValues.add(otherValue); - } - } - if (mergedValues !== null) { - nextVariables = nextVariables ?? new Map(this.#variables); - nextVariables.set(id, mergedValues); - } - } - } - for (const [id, otherValues] of other.#variables) { - if (this.#variables.has(id)) { - continue; - } - nextVariables = nextVariables ?? new Map(this.#variables); - nextVariables.set(id, new Set(otherValues)); - } - - if (nextVariables === null && nextValues === null) { - return null; - } else { - return new InferenceState( - this.env, - this.#isFunctionExpression, - nextValues ?? new Map(this.#values), - nextVariables ?? new Map(this.#variables), - ); - } - } - - /* - * Returns a copy of this state. - * TODO: consider using persistent data structures to make - * clone cheaper. - */ - clone(): InferenceState { - return new InferenceState( - this.env, - this.#isFunctionExpression, - new Map(this.#values), - new Map(this.#variables), - ); - } - - /* - * For debugging purposes, dumps the state to a plain - * object so that it can printed as JSON. - */ - debug(): any { - const result: any = {values: {}, variables: {}}; - const objects: Map = new Map(); - function identify(value: InstructionValue): number { - let id = objects.get(value); - if (id == null) { - id = objects.size; - objects.set(value, id); - } - return id; - } - for (const [value, kind] of this.#values) { - const id = identify(value); - result.values[id] = {kind, value: printMixedHIR(value)}; - } - for (const [variable, values] of this.#variables) { - result.variables[`$${variable}`] = [...values].map(identify); - } - return result; - } - - inferPhi(phi: Phi): void { - const values: Set = new Set(); - for (const [_, operand] of phi.operands) { - const operandValues = this.#variables.get(operand.identifier.id); - // This is a backedge that will be handled later by State.merge - if (operandValues === undefined) continue; - for (const v of operandValues) { - values.add(v); - } - } - - if (values.size > 0) { - this.#variables.set(phi.place.identifier.id, values); - } - } -} - -function inferParam( - param: Place | SpreadPattern, - initialState: InferenceState, - paramKind: AbstractValue, -): void { - let value: InstructionValue; - let place: Place; - if (param.kind === 'Identifier') { - place = param; - value = { - kind: 'Primitive', - loc: param.loc, - value: undefined, - }; - } else { - place = param.place; - value = { - kind: 'Primitive', - loc: param.place.loc, - value: undefined, - }; - } - initialState.initialize(value, paramKind); - initialState.define(place, value); -} - -/* - * Joins two values using the following rules: - * == Effect Transitions == - * - * Freezing an immutable value has not effect: - * ┌───────────────┐ - * │ │ - * ▼ │ Freeze - * ┌──────────────────────────┐ │ - * │ Immutable │──┘ - * └──────────────────────────┘ - * - * Freezing a mutable or maybe-frozen value makes it frozen. Freezing a frozen - * value has no effect: - * ┌───────────────┐ - * ┌─────────────────────────┐ Freeze │ │ - * │ MaybeFrozen │────┐ ▼ │ Freeze - * └─────────────────────────┘ │ ┌──────────────────────────┐ │ - * ├────▶│ Frozen │──┘ - * │ └──────────────────────────┘ - * ┌─────────────────────────┐ │ - * │ Mutable │────┘ - * └─────────────────────────┘ - * - * == Join Lattice == - * - immutable | mutable => mutable - * The justification is that immutable and mutable values are different types, - * and functions can introspect them to tell the difference (if the argument - * is null return early, else if its an object mutate it). - * - frozen | mutable => maybe-frozen - * Frozen values are indistinguishable from mutable values at runtime, so callers - * cannot dynamically avoid mutation of "frozen" values. If a value could be - * frozen we have to distinguish it from a mutable value. But it also isn't known - * frozen yet, so we distinguish as maybe-frozen. - * - immutable | frozen => frozen - * This is subtle and falls out of the above rules. If a value could be any of - * immutable, mutable, or frozen, then at runtime it could either be a primitive - * or a reference type, and callers can't distinguish frozen or not for reference - * types. To ensure that any sequence of joins btw those three states yields the - * correct maybe-frozen, these two have to produce a frozen value. - * - | maybe-frozen => maybe-frozen - * - immutable | context => context - * - mutable | context => context - * - frozen | context => maybe-frozen - * - * ┌──────────────────────────┐ - * │ Immutable │───┐ - * └──────────────────────────┘ │ - * │ ┌─────────────────────────┐ - * ├───▶│ Frozen │──┐ - * ┌──────────────────────────┐ │ └─────────────────────────┘ │ - * │ Frozen │───┤ │ ┌─────────────────────────┐ - * └──────────────────────────┘ │ ├─▶│ MaybeFrozen │ - * │ ┌─────────────────────────┐ │ └─────────────────────────┘ - * ├───▶│ MaybeFrozen │──┘ - * ┌──────────────────────────┐ │ └─────────────────────────┘ - * │ Mutable │───┘ - * └──────────────────────────┘ - */ -function mergeValues(a: ValueKind, b: ValueKind): ValueKind { - if (a === b) { - return a; - } else if (a === ValueKind.MaybeFrozen || b === ValueKind.MaybeFrozen) { - return ValueKind.MaybeFrozen; - // after this a and b differ and neither are MaybeFrozen - } else if (a === ValueKind.Mutable || b === ValueKind.Mutable) { - if (a === ValueKind.Frozen || b === ValueKind.Frozen) { - // frozen | mutable - return ValueKind.MaybeFrozen; - } else if (a === ValueKind.Context || b === ValueKind.Context) { - // context | mutable - return ValueKind.Context; - } else { - // mutable | immutable - return ValueKind.Mutable; - } - } else if (a === ValueKind.Context || b === ValueKind.Context) { - if (a === ValueKind.Frozen || b === ValueKind.Frozen) { - // frozen | context - return ValueKind.MaybeFrozen; - } else { - // context | immutable - return ValueKind.Context; - } - } else if (a === ValueKind.Frozen || b === ValueKind.Frozen) { - return ValueKind.Frozen; - } else if (a === ValueKind.Global || b === ValueKind.Global) { - return ValueKind.Global; - } else { - CompilerError.invariant( - a === ValueKind.Primitive && b == ValueKind.Primitive, - { - reason: `Unexpected value kind in mergeValues()`, - description: `Found kinds ${a} and ${b}`, - loc: GeneratedSource, - }, - ); - return ValueKind.Primitive; - } -} - -/** - * @returns `true` if `a` is a superset of `b`. - */ -function isSuperset(a: ReadonlySet, b: ReadonlySet): boolean { - for (const v of b) { - if (!a.has(v)) { - return false; - } - } - return true; -} - -function mergeAbstractValues( - a: AbstractValue, - b: AbstractValue, -): AbstractValue { - const kind = mergeValues(a.kind, b.kind); - if ( - kind === a.kind && - kind === b.kind && - isSuperset(a.reason, b.reason) && - isSuperset(a.context, b.context) - ) { - return a; - } - const reason = new Set(a.reason); - for (const r of b.reason) { - reason.add(r); - } - const context = new Set(a.context); - for (const c of b.context) { - context.add(c); - } - return {kind, reason, context}; -} - -type Continuation = - | { - kind: 'initialize'; - valueKind: AbstractValue; - effect: {kind: Effect; reason: ValueReason} | null; - lvalueEffect?: Effect; - } - | {kind: 'funeffects'}; - -/* - * Iterates over the given @param block, defining variables and - * recording references on the @param state according to JS semantics. - */ -function inferBlock( - env: Environment, - state: InferenceState, - block: BasicBlock, - functionEffects: Array, -): void { - for (const phi of block.phis) { - state.inferPhi(phi); - } - - for (const instr of block.instructions) { - const instrValue = instr.value; - const defaultLvalueEffect = Effect.ConditionallyMutate; - let continuation: Continuation; - const freezeActions: Array = []; - switch (instrValue.kind) { - case 'BinaryExpression': { - continuation = { - kind: 'initialize', - valueKind: { - kind: ValueKind.Primitive, - reason: new Set([ValueReason.Other]), - context: new Set(), - }, - effect: { - kind: Effect.Read, - reason: ValueReason.Other, - }, - }; - break; - } - case 'ArrayExpression': { - const contextRefOperands = getContextRefOperand(state, instrValue); - const valueKind: AbstractValue = - contextRefOperands.length > 0 - ? { - kind: ValueKind.Context, - reason: new Set([ValueReason.Other]), - context: new Set(contextRefOperands), - } - : { - kind: ValueKind.Mutable, - reason: new Set([ValueReason.Other]), - context: new Set(), - }; - - for (const element of instrValue.elements) { - if (element.kind === 'Spread') { - state.referenceAndRecordEffects( - freezeActions, - element.place, - Effect.ConditionallyMutateIterator, - ValueReason.Other, - ); - } else if (element.kind === 'Identifier') { - state.referenceAndRecordEffects( - freezeActions, - element, - Effect.Capture, - ValueReason.Other, - ); - } else { - let _: 'Hole' = element.kind; - } - } - state.initialize(instrValue, valueKind); - state.define(instr.lvalue, instrValue); - instr.lvalue.effect = Effect.Store; - continuation = { - kind: 'funeffects', - }; - break; - } - case 'NewExpression': { - inferCallEffects( - state, - instr as TInstruction, - freezeActions, - getFunctionCallSignature(env, instrValue.callee.identifier.type), - ); - continuation = {kind: 'funeffects'}; - break; - } - case 'ObjectExpression': { - const contextRefOperands = getContextRefOperand(state, instrValue); - const valueKind: AbstractValue = - contextRefOperands.length > 0 - ? { - kind: ValueKind.Context, - reason: new Set([ValueReason.Other]), - context: new Set(contextRefOperands), - } - : { - kind: ValueKind.Mutable, - reason: new Set([ValueReason.Other]), - context: new Set(), - }; - - for (const property of instrValue.properties) { - switch (property.kind) { - case 'ObjectProperty': { - if (property.key.kind === 'computed') { - // Object keys must be primitives, so we know they're frozen at this point - state.referenceAndRecordEffects( - freezeActions, - property.key.name, - Effect.Freeze, - ValueReason.Other, - ); - } - // Object construction captures but does not modify the key/property values - state.referenceAndRecordEffects( - freezeActions, - property.place, - Effect.Capture, - ValueReason.Other, - ); - break; - } - case 'Spread': { - // Object construction captures but does not modify the key/property values - state.referenceAndRecordEffects( - freezeActions, - property.place, - Effect.Capture, - ValueReason.Other, - ); - break; - } - default: { - assertExhaustive( - property, - `Unexpected property kind \`${(property as any).kind}\``, - ); - } - } - } - - state.initialize(instrValue, valueKind); - state.define(instr.lvalue, instrValue); - instr.lvalue.effect = Effect.Store; - continuation = {kind: 'funeffects'}; - break; - } - case 'UnaryExpression': { - continuation = { - kind: 'initialize', - valueKind: { - kind: ValueKind.Primitive, - reason: new Set([ValueReason.Other]), - context: new Set(), - }, - effect: {kind: Effect.Read, reason: ValueReason.Other}, - }; - break; - } - case 'UnsupportedNode': { - // TODO: handle other statement kinds - continuation = { - kind: 'initialize', - valueKind: { - kind: ValueKind.Mutable, - reason: new Set([ValueReason.Other]), - context: new Set(), - }, - effect: null, - }; - break; - } - case 'JsxExpression': { - if (instrValue.tag.kind === 'Identifier') { - state.referenceAndRecordEffects( - freezeActions, - instrValue.tag, - Effect.Freeze, - ValueReason.JsxCaptured, - ); - } - if (instrValue.children !== null) { - for (const child of instrValue.children) { - state.referenceAndRecordEffects( - freezeActions, - child, - Effect.Freeze, - ValueReason.JsxCaptured, - ); - } - } - for (const attr of instrValue.props) { - if (attr.kind === 'JsxSpreadAttribute') { - state.referenceAndRecordEffects( - freezeActions, - attr.argument, - Effect.Freeze, - ValueReason.JsxCaptured, - ); - } else { - state.referenceAndRecordEffects( - freezeActions, - attr.place, - Effect.Freeze, - ValueReason.JsxCaptured, - ); - } - } - - state.initialize(instrValue, { - kind: ValueKind.Frozen, - reason: new Set([ValueReason.Other]), - context: new Set(), - }); - state.define(instr.lvalue, instrValue); - instr.lvalue.effect = Effect.ConditionallyMutate; - continuation = {kind: 'funeffects'}; - break; - } - case 'JsxFragment': { - continuation = { - kind: 'initialize', - valueKind: { - kind: ValueKind.Frozen, - reason: new Set([ValueReason.Other]), - context: new Set(), - }, - effect: { - kind: Effect.Freeze, - reason: ValueReason.Other, - }, - }; - break; - } - case 'TemplateLiteral': { - /* - * template literal (with no tag function) always produces - * an immutable string - */ - continuation = { - kind: 'initialize', - valueKind: { - kind: ValueKind.Primitive, - reason: new Set([ValueReason.Other]), - context: new Set(), - }, - effect: {kind: Effect.Read, reason: ValueReason.Other}, - }; - break; - } - case 'RegExpLiteral': { - // RegExp instances are mutable objects - continuation = { - kind: 'initialize', - valueKind: { - kind: ValueKind.Mutable, - reason: new Set([ValueReason.Other]), - context: new Set(), - }, - effect: { - kind: Effect.ConditionallyMutate, - reason: ValueReason.Other, - }, - }; - break; - } - case 'MetaProperty': { - if (instrValue.meta !== 'import' || instrValue.property !== 'meta') { - continuation = {kind: 'funeffects'}; - break; - } - continuation = { - kind: 'initialize', - valueKind: { - kind: ValueKind.Global, - reason: new Set([ValueReason.Global]), - context: new Set(), - }, - effect: null, - }; - break; - } - case 'LoadGlobal': - continuation = { - kind: 'initialize', - valueKind: { - kind: ValueKind.Global, - reason: new Set([ValueReason.Global]), - context: new Set(), - }, - effect: null, - }; - break; - case 'Debugger': - case 'JSXText': - case 'Primitive': { - continuation = { - kind: 'initialize', - valueKind: { - kind: ValueKind.Primitive, - reason: new Set([ValueReason.Other]), - context: new Set(), - }, - effect: null, - }; - break; - } - case 'ObjectMethod': - case 'FunctionExpression': { - let hasMutableOperand = false; - for (const operand of eachInstructionOperand(instr)) { - CompilerError.invariant(operand.effect !== Effect.Unknown, { - reason: 'Expected fn effects to be populated', - loc: operand.loc, - }); - state.referenceAndRecordEffects( - freezeActions, - operand, - operand.effect, - ValueReason.Other, - ); - hasMutableOperand ||= isMutableEffect(operand.effect, operand.loc); - } - /* - * If a closure did not capture any mutable values, then we can consider it to be - * frozen, which allows it to be independently memoized. - */ - state.initialize(instrValue, { - kind: hasMutableOperand ? ValueKind.Mutable : ValueKind.Frozen, - reason: new Set([ValueReason.Other]), - context: new Set(), - }); - state.define(instr.lvalue, instrValue); - instr.lvalue.effect = Effect.Store; - continuation = {kind: 'funeffects'}; - break; - } - case 'TaggedTemplateExpression': { - const operands = [...eachInstructionValueOperand(instrValue)]; - if (operands.length !== 1) { - // future-proofing to make sure we update this case when we support interpolation - CompilerError.throwTodo({ - reason: 'Support tagged template expressions with interpolations', - loc: instrValue.loc, - }); - } - const signature = getFunctionCallSignature( - env, - instrValue.tag.identifier.type, - ); - let calleeEffect = - signature?.calleeEffect ?? Effect.ConditionallyMutate; - const returnValueKind: AbstractValue = - signature !== null - ? { - kind: signature.returnValueKind, - reason: new Set([ - signature.returnValueReason ?? - ValueReason.KnownReturnSignature, - ]), - context: new Set(), - } - : { - kind: ValueKind.Mutable, - reason: new Set([ValueReason.Other]), - context: new Set(), - }; - state.referenceAndRecordEffects( - freezeActions, - instrValue.tag, - calleeEffect, - ValueReason.Other, - ); - state.initialize(instrValue, returnValueKind); - state.define(instr.lvalue, instrValue); - instr.lvalue.effect = Effect.ConditionallyMutate; - continuation = {kind: 'funeffects'}; - break; - } - case 'CallExpression': { - inferCallEffects( - state, - instr as TInstruction, - freezeActions, - getFunctionCallSignature(env, instrValue.callee.identifier.type), - ); - continuation = {kind: 'funeffects'}; - break; - } - case 'MethodCall': { - CompilerError.invariant(state.isDefined(instrValue.receiver), { - reason: - '[InferReferenceEffects] Internal error: receiver of PropertyCall should have been defined by corresponding PropertyLoad', - description: null, - loc: instrValue.loc, - suggestions: null, - }); - state.referenceAndRecordEffects( - freezeActions, - instrValue.property, - Effect.Read, - ValueReason.Other, - ); - inferCallEffects( - state, - instr as TInstruction, - freezeActions, - getFunctionCallSignature(env, instrValue.property.identifier.type), - ); - continuation = {kind: 'funeffects'}; - break; - } - case 'PropertyStore': { - const effect = - state.kind(instrValue.object).kind === ValueKind.Context - ? Effect.ConditionallyMutate - : Effect.Capture; - state.referenceAndRecordEffects( - freezeActions, - instrValue.value, - effect, - ValueReason.Other, - ); - state.referenceAndRecordEffects( - freezeActions, - instrValue.object, - Effect.Store, - ValueReason.Other, - ); - - const lvalue = instr.lvalue; - state.alias(lvalue, instrValue.value); - lvalue.effect = Effect.Store; - continuation = {kind: 'funeffects'}; - break; - } - case 'PropertyDelete': { - // `delete` returns a boolean (immutable) and modifies the object - continuation = { - kind: 'initialize', - valueKind: { - kind: ValueKind.Primitive, - reason: new Set([ValueReason.Other]), - context: new Set(), - }, - effect: {kind: Effect.Mutate, reason: ValueReason.Other}, - }; - break; - } - case 'PropertyLoad': { - state.referenceAndRecordEffects( - freezeActions, - instrValue.object, - Effect.Read, - ValueReason.Other, - ); - const lvalue = instr.lvalue; - lvalue.effect = Effect.ConditionallyMutate; - state.initialize(instrValue, state.kind(instrValue.object)); - state.define(lvalue, instrValue); - continuation = {kind: 'funeffects'}; - break; - } - case 'ComputedStore': { - const effect = - state.kind(instrValue.object).kind === ValueKind.Context - ? Effect.ConditionallyMutate - : Effect.Capture; - state.referenceAndRecordEffects( - freezeActions, - instrValue.value, - effect, - ValueReason.Other, - ); - state.referenceAndRecordEffects( - freezeActions, - instrValue.property, - Effect.Capture, - ValueReason.Other, - ); - state.referenceAndRecordEffects( - freezeActions, - instrValue.object, - Effect.Store, - ValueReason.Other, - ); - - const lvalue = instr.lvalue; - state.alias(lvalue, instrValue.value); - lvalue.effect = Effect.Store; - continuation = {kind: 'funeffects'}; - break; - } - case 'ComputedDelete': { - state.referenceAndRecordEffects( - freezeActions, - instrValue.object, - Effect.Mutate, - ValueReason.Other, - ); - state.referenceAndRecordEffects( - freezeActions, - instrValue.property, - Effect.Read, - ValueReason.Other, - ); - state.initialize(instrValue, { - kind: ValueKind.Primitive, - reason: new Set([ValueReason.Other]), - context: new Set(), - }); - state.define(instr.lvalue, instrValue); - instr.lvalue.effect = Effect.Mutate; - continuation = {kind: 'funeffects'}; - break; - } - case 'ComputedLoad': { - state.referenceAndRecordEffects( - freezeActions, - instrValue.object, - Effect.Read, - ValueReason.Other, - ); - state.referenceAndRecordEffects( - freezeActions, - instrValue.property, - Effect.Read, - ValueReason.Other, - ); - const lvalue = instr.lvalue; - lvalue.effect = Effect.ConditionallyMutate; - state.initialize(instrValue, state.kind(instrValue.object)); - state.define(lvalue, instrValue); - continuation = {kind: 'funeffects'}; - break; - } - case 'Await': { - state.initialize(instrValue, state.kind(instrValue.value)); - /* - * Awaiting a value causes it to change state (go from unresolved to resolved or error) - * It also means that any side-effects which would occur as part of the promise evaluation - * will occur. - */ - state.referenceAndRecordEffects( - freezeActions, - instrValue.value, - Effect.ConditionallyMutate, - ValueReason.Other, - ); - const lvalue = instr.lvalue; - lvalue.effect = Effect.ConditionallyMutate; - state.alias(lvalue, instrValue.value); - continuation = {kind: 'funeffects'}; - break; - } - case 'TypeCastExpression': { - /* - * A type cast expression has no effect at runtime, so it's equivalent to a raw - * identifier: - * ``` - * x = (y: type) // is equivalent to... - * x = y - * ``` - */ - state.initialize(instrValue, state.kind(instrValue.value)); - state.referenceAndRecordEffects( - freezeActions, - instrValue.value, - Effect.Read, - ValueReason.Other, - ); - const lvalue = instr.lvalue; - lvalue.effect = Effect.ConditionallyMutate; - state.alias(lvalue, instrValue.value); - continuation = {kind: 'funeffects'}; - break; - } - case 'StartMemoize': - case 'FinishMemoize': { - for (const val of eachInstructionValueOperand(instrValue)) { - if (env.config.enablePreserveExistingMemoizationGuarantees) { - state.referenceAndRecordEffects( - freezeActions, - val, - Effect.Freeze, - ValueReason.Other, - ); - } else { - state.referenceAndRecordEffects( - freezeActions, - val, - Effect.Read, - ValueReason.Other, - ); - } - } - const lvalue = instr.lvalue; - lvalue.effect = Effect.ConditionallyMutate; - state.initialize(instrValue, { - kind: ValueKind.Frozen, - reason: new Set([ValueReason.Other]), - context: new Set(), - }); - state.define(lvalue, instrValue); - continuation = {kind: 'funeffects'}; - break; - } - case 'LoadLocal': { - /** - * Due to backedges in the CFG, we may revisit LoadLocal lvalues - * multiple times. Unlike StoreLocal which may reassign to existing - * identifiers, LoadLocal always evaluates to store a new temporary. - * This means that we should always model LoadLocal as a Capture effect - * on the rvalue. - */ - const lvalue = instr.lvalue; - state.referenceAndRecordEffects( - freezeActions, - instrValue.place, - Effect.Capture, - ValueReason.Other, - ); - lvalue.effect = Effect.ConditionallyMutate; - // direct aliasing: `a = b`; - state.alias(lvalue, instrValue.place); - continuation = {kind: 'funeffects'}; - break; - } - case 'LoadContext': { - state.referenceAndRecordEffects( - freezeActions, - instrValue.place, - Effect.Capture, - ValueReason.Other, - ); - const lvalue = instr.lvalue; - lvalue.effect = Effect.ConditionallyMutate; - const valueKind = state.kind(instrValue.place); - state.initialize(instrValue, valueKind); - state.define(lvalue, instrValue); - continuation = {kind: 'funeffects'}; - break; - } - case 'DeclareLocal': { - const value = UndefinedValue; - state.initialize( - value, - // Catch params may be aliased to mutable values - instrValue.lvalue.kind === InstructionKind.Catch - ? { - kind: ValueKind.Mutable, - reason: new Set([ValueReason.Other]), - context: new Set(), - } - : { - kind: ValueKind.Primitive, - reason: new Set([ValueReason.Other]), - context: new Set(), - }, - ); - state.define(instrValue.lvalue.place, value); - continuation = {kind: 'funeffects'}; - break; - } - case 'DeclareContext': { - state.initialize(instrValue, { - kind: ValueKind.Mutable, - reason: new Set([ValueReason.Other]), - context: new Set(), - }); - state.define(instrValue.lvalue.place, instrValue); - continuation = {kind: 'funeffects'}; - break; - } - case 'PostfixUpdate': - case 'PrefixUpdate': { - const effect = - state.isDefined(instrValue.lvalue) && - state.kind(instrValue.lvalue).kind === ValueKind.Context - ? Effect.ConditionallyMutate - : Effect.Capture; - state.referenceAndRecordEffects( - freezeActions, - instrValue.value, - effect, - ValueReason.Other, - ); - - const lvalue = instr.lvalue; - state.alias(lvalue, instrValue.value); - lvalue.effect = Effect.Store; - state.alias(instrValue.lvalue, instrValue.value); - /* - * NOTE: *not* using state.reference since this is an assignment. - * reference() checks if the effect is valid given the value kind, - * but here the previous value kind doesn't matter since we are - * replacing it - */ - instrValue.lvalue.effect = Effect.Store; - continuation = {kind: 'funeffects'}; - break; - } - case 'StoreLocal': { - const effect = - state.isDefined(instrValue.lvalue.place) && - state.kind(instrValue.lvalue.place).kind === ValueKind.Context - ? Effect.ConditionallyMutate - : Effect.Capture; - state.referenceAndRecordEffects( - freezeActions, - instrValue.value, - effect, - ValueReason.Other, - ); - - const lvalue = instr.lvalue; - state.alias(lvalue, instrValue.value); - lvalue.effect = Effect.Store; - state.alias(instrValue.lvalue.place, instrValue.value); - /* - * NOTE: *not* using state.reference since this is an assignment. - * reference() checks if the effect is valid given the value kind, - * but here the previous value kind doesn't matter since we are - * replacing it - */ - instrValue.lvalue.place.effect = Effect.Store; - continuation = {kind: 'funeffects'}; - break; - } - case 'StoreContext': { - state.referenceAndRecordEffects( - freezeActions, - instrValue.value, - Effect.ConditionallyMutate, - ValueReason.Other, - ); - state.referenceAndRecordEffects( - freezeActions, - instrValue.lvalue.place, - Effect.Mutate, - ValueReason.Other, - ); - - const lvalue = instr.lvalue; - if (instrValue.lvalue.kind !== InstructionKind.Reassign) { - state.initialize(instrValue, { - kind: ValueKind.Mutable, - reason: new Set([ValueReason.Other]), - context: new Set(), - }); - state.define(instrValue.lvalue.place, instrValue); - } - state.alias(lvalue, instrValue.value); - lvalue.effect = Effect.Store; - continuation = {kind: 'funeffects'}; - break; - } - case 'StoreGlobal': { - state.referenceAndRecordEffects( - freezeActions, - instrValue.value, - Effect.Capture, - ValueReason.Other, - ); - const lvalue = instr.lvalue; - lvalue.effect = Effect.Store; - continuation = {kind: 'funeffects'}; - break; - } - case 'Destructure': { - let effect: Effect = Effect.Capture; - for (const place of eachPatternOperand(instrValue.lvalue.pattern)) { - if ( - state.isDefined(place) && - state.kind(place).kind === ValueKind.Context - ) { - effect = Effect.ConditionallyMutate; - break; - } - } - state.referenceAndRecordEffects( - freezeActions, - instrValue.value, - effect, - ValueReason.Other, - ); - - const lvalue = instr.lvalue; - state.alias(lvalue, instrValue.value); - lvalue.effect = Effect.Store; - for (const place of eachPatternOperand(instrValue.lvalue.pattern)) { - state.alias(place, instrValue.value); - /* - * NOTE: *not* using state.reference since this is an assignment. - * reference() checks if the effect is valid given the value kind, - * but here the previous value kind doesn't matter since we are - * replacing it - */ - place.effect = Effect.Store; - } - continuation = {kind: 'funeffects'}; - break; - } - case 'GetIterator': { - /** - * This instruction represents the step of retrieving an iterator from the collection - * in `for (... of )` syntax. We model two cases: - * - * 1. The collection is immutable or a known collection type (e.g. Array). In this case - * we infer that the iterator produced won't be the same as the collection itself. - * If the collection is an Array, this is because it will produce a native Array - * iterator. If the collection is already frozen, we assume it must be of some - * type that returns a separate iterator. In theory you could pass an Iterator - * as props to a component and then for..of over that in the component body, but - * this already violates React's rules so we assume you're not doing this. - * 2. The collection could be an Iterator itself, such that advancing the iterator - * (modeled with IteratorNext) mutates the collection itself. - */ - const kind = state.kind(instrValue.collection).kind; - const isMutable = - kind === ValueKind.Mutable || kind === ValueKind.Context; - let effect; - let valueKind: AbstractValue; - const iterator = instrValue.collection.identifier; - if ( - !isMutable || - isArrayType(iterator) || - isMapType(iterator) || - isSetType(iterator) - ) { - // Case 1, assume iterator is a separate mutable object - effect = { - kind: Effect.Read, - reason: ValueReason.Other, - }; - valueKind = { - kind: ValueKind.Mutable, - reason: new Set([ValueReason.Other]), - context: new Set(), - }; - } else { - // Case 2, assume that the iterator could be the (mutable) collection itself - effect = { - kind: Effect.Capture, - reason: ValueReason.Other, - }; - valueKind = state.kind(instrValue.collection); - } - continuation = { - kind: 'initialize', - effect, - valueKind, - lvalueEffect: Effect.Store, - }; - break; - } - case 'IteratorNext': { - /** - * This instruction represents advancing an iterator with .next(). We use a - * conditional mutate to model the two cases for GetIterator: - * - If the collection is a mutable iterator, we want to model the fact that - * advancing the iterator will mutate it - * - If the iterator may be different from the collection and the collection - * is frozen, we don't want to report a false positive "cannot mutate" error. - * - * ConditionallyMutate reflects this "mutate if mutable" semantic. - */ - state.referenceAndRecordEffects( - freezeActions, - instrValue.iterator, - Effect.ConditionallyMutateIterator, - ValueReason.Other, - ); - /** - * Regardless of the effect on the iterator, the *result* of advancing the iterator - * is to extract a value from the collection. We use a Capture effect to reflect this - * aliasing, and then initialize() the lvalue to the same kind as the colleciton to - * ensure that the item is mutable or frozen if the collection is mutable/frozen. - */ - state.referenceAndRecordEffects( - freezeActions, - instrValue.collection, - Effect.Capture, - ValueReason.Other, - ); - state.initialize(instrValue, state.kind(instrValue.collection)); - state.define(instr.lvalue, instrValue); - instr.lvalue.effect = Effect.Store; - continuation = {kind: 'funeffects'}; - break; - } - case 'NextPropertyOf': { - continuation = { - kind: 'initialize', - effect: {kind: Effect.Read, reason: ValueReason.Other}, - lvalueEffect: Effect.Store, - valueKind: { - kind: ValueKind.Primitive, - reason: new Set([ValueReason.Other]), - context: new Set(), - }, - }; - break; - } - default: { - assertExhaustive(instrValue, 'Unexpected instruction kind'); - } - } - - if (continuation.kind === 'initialize') { - for (const operand of eachInstructionOperand(instr)) { - CompilerError.invariant(continuation.effect != null, { - reason: `effectKind must be set for instruction value \`${instrValue.kind}\``, - description: null, - loc: instrValue.loc, - suggestions: null, - }); - state.referenceAndRecordEffects( - freezeActions, - operand, - continuation.effect.kind, - continuation.effect.reason, - ); - } - - state.initialize(instrValue, continuation.valueKind); - state.define(instr.lvalue, instrValue); - instr.lvalue.effect = continuation.lvalueEffect ?? defaultLvalueEffect; - } - - functionEffects.push(...inferInstructionFunctionEffects(env, state, instr)); - freezeActions.forEach(({values, reason}) => - state.freezeValues(values, reason), - ); - } - - const terminalFreezeActions: Array = []; - for (const operand of eachTerminalOperand(block.terminal)) { - let effect; - if (block.terminal.kind === 'return' || block.terminal.kind === 'throw') { - if ( - state.isDefined(operand) && - ((operand.identifier.type.kind === 'Function' && - state.isFunctionExpression) || - state.kind(operand).kind === ValueKind.Context) - ) { - /** - * Returned values should only be typed as 'frozen' if they are both (1) - * local and (2) not a function expression which may capture and mutate - * this function's outer context. - */ - effect = Effect.ConditionallyMutate; - } else { - effect = Effect.Freeze; - } - } else { - effect = Effect.Read; - } - state.referenceAndRecordEffects( - terminalFreezeActions, - operand, - effect, - ValueReason.Other, - ); - } - functionEffects.push(...inferTerminalFunctionEffects(state, block)); - terminalFreezeActions.forEach(({values, reason}) => - state.freezeValues(values, reason), - ); -} - -function getContextRefOperand( - state: InferenceState, - instrValue: InstructionValue, -): Array { - const result = []; - for (const place of eachInstructionValueOperand(instrValue)) { - if ( - state.isDefined(place) && - state.kind(place).kind === ValueKind.Context - ) { - result.push(place); - } - } - return result; -} - -export function getFunctionCallSignature( - env: Environment, - type: Type, -): FunctionSignature | null { - if (type.kind !== 'Function') { - return null; - } - return env.getFunctionSignature(type); -} - -/* - * Make a best attempt at matching arguments of a {@link MethodCall} to parameter effects. - * defined in its {@link FunctionSignature}. - * - * @param fn - * @param sig - * @returns Inferred effects of function arguments, or null if inference fails. - */ -export function getFunctionEffects( - fn: MethodCall | CallExpression | NewExpression, - sig: FunctionSignature, -): Array | null { - const results = []; - for (let i = 0; i < fn.args.length; i++) { - const arg = fn.args[i]; - if (i < sig.positionalParams.length) { - /* - * Only infer effects when there is a direct mapping positional arg --> positional param - * Otherwise, return null to indicate inference failed - */ - if (arg.kind === 'Identifier') { - results.push(sig.positionalParams[i]); - } else { - return null; - } - } else if (sig.restParam !== null) { - results.push(sig.restParam); - } else { - /* - * If there are more arguments than positional arguments and a rest parameter is not - * defined, we'll also assume that inference failed - */ - return null; - } - } - return results; -} - -export function isKnownMutableEffect(effect: Effect): boolean { - switch (effect) { - case Effect.Store: - case Effect.ConditionallyMutate: - case Effect.ConditionallyMutateIterator: - case Effect.Mutate: { - return true; - } - - case Effect.Unknown: { - CompilerError.invariant(false, { - reason: 'Unexpected unknown effect', - description: null, - loc: GeneratedSource, - suggestions: null, - }); - } - case Effect.Read: - case Effect.Capture: - case Effect.Freeze: { - return false; - } - default: { - assertExhaustive(effect, `Unexpected effect \`${effect}\``); - } - } -} -/** - * Returns true if all of the arguments are both non-mutable (immutable or frozen) - * _and_ are not functions which might mutate their arguments. Note that function - * expressions count as frozen so long as they do not mutate free variables: this - * function checks that such functions also don't mutate their inputs. - */ -function areArgumentsImmutableAndNonMutating( - state: InferenceState, - args: MethodCall['args'], -): boolean { - for (const arg of args) { - if (arg.kind === 'Identifier' && arg.identifier.type.kind === 'Function') { - const fnShape = state.env.getFunctionSignature(arg.identifier.type); - if (fnShape != null) { - return ( - !fnShape.positionalParams.some(isKnownMutableEffect) && - (fnShape.restParam == null || - !isKnownMutableEffect(fnShape.restParam)) - ); - } - } - const place = arg.kind === 'Identifier' ? arg : arg.place; - - const kind = state.kind(place).kind; - switch (kind) { - case ValueKind.Primitive: - case ValueKind.Frozen: { - /* - * Only immutable values, or frozen lambdas are allowed. - * A lambda may appear frozen even if it may mutate its inputs, - * so we have a second check even for frozen value types - */ - break; - } - default: { - /** - * Globals, module locals, and other locally defined functions may - * mutate their arguments. - */ - return false; - } - } - const values = state.values(place); - for (const value of values) { - if ( - value.kind === 'FunctionExpression' && - value.loweredFunc.func.params.some(param => { - const place = param.kind === 'Identifier' ? param : param.place; - const range = place.identifier.mutableRange; - return range.end > range.start + 1; - }) - ) { - // This is a function which may mutate its inputs - return false; - } - } - } - return true; -} - -function getArgumentEffect( - signatureEffect: Effect | null, - arg: Place | SpreadPattern, -): Effect { - if (signatureEffect != null) { - if (arg.kind === 'Identifier') { - return signatureEffect; - } else if ( - signatureEffect === Effect.Mutate || - signatureEffect === Effect.ConditionallyMutate - ) { - return signatureEffect; - } else { - // see call-spread-argument-mutable-iterator test fixture - if (signatureEffect === Effect.Freeze) { - CompilerError.throwTodo({ - reason: 'Support spread syntax for hook arguments', - loc: arg.place.loc, - }); - } - // effects[i] is Effect.Capture | Effect.Read | Effect.Store - return Effect.ConditionallyMutateIterator; - } - } else { - return Effect.ConditionallyMutate; - } -} - -function inferCallEffects( - state: InferenceState, - instr: - | TInstruction - | TInstruction - | TInstruction, - freezeActions: Array, - signature: FunctionSignature | null, -): void { - const instrValue = instr.value; - const returnValueKind: AbstractValue = - signature !== null - ? { - kind: signature.returnValueKind, - reason: new Set([ - signature.returnValueReason ?? ValueReason.KnownReturnSignature, - ]), - context: new Set(), - } - : { - kind: ValueKind.Mutable, - reason: new Set([ValueReason.Other]), - context: new Set(), - }; - - if ( - instrValue.kind === 'MethodCall' && - signature !== null && - signature.mutableOnlyIfOperandsAreMutable && - areArgumentsImmutableAndNonMutating(state, instrValue.args) - ) { - /* - * None of the args are mutable or mutate their params, we can downgrade to - * treating as all reads (except that the receiver may be captured) - */ - for (const arg of instrValue.args) { - const place = arg.kind === 'Identifier' ? arg : arg.place; - state.referenceAndRecordEffects( - freezeActions, - place, - Effect.Read, - ValueReason.Other, - ); - } - state.referenceAndRecordEffects( - freezeActions, - instrValue.receiver, - Effect.Capture, - ValueReason.Other, - ); - state.initialize(instrValue, returnValueKind); - state.define(instr.lvalue, instrValue); - instr.lvalue.effect = - instrValue.receiver.effect === Effect.Capture - ? Effect.Store - : Effect.ConditionallyMutate; - return; - } - - const effects = - signature !== null ? getFunctionEffects(instrValue, signature) : null; - let hasCaptureArgument = false; - for (let i = 0; i < instrValue.args.length; i++) { - const arg = instrValue.args[i]; - const place = arg.kind === 'Identifier' ? arg : arg.place; - /* - * If effects are inferred for an argument, we should fail invalid - * mutating effects - */ - state.referenceAndRecordEffects( - freezeActions, - place, - getArgumentEffect(effects != null ? effects[i] : null, arg), - ValueReason.Other, - ); - hasCaptureArgument ||= place.effect === Effect.Capture; - } - const callee = - instrValue.kind === 'MethodCall' ? instrValue.receiver : instrValue.callee; - if (signature !== null) { - state.referenceAndRecordEffects( - freezeActions, - callee, - signature.calleeEffect, - ValueReason.Other, - ); - } else { - /** - * For new expressions, we infer a `read` effect on the Class / Function type - * to avoid extending mutable ranges of locally created classes, e.g. - * ```js - * const MyClass = getClass(); - * const value = new MyClass(val1, val2) - * ^ (read) ^ (conditionally mutate) - * ``` - * - * Risks: - * Classes / functions created during render could technically capture and - * mutate their enclosing scope, which we currently do not detect. - */ - - state.referenceAndRecordEffects( - freezeActions, - callee, - instrValue.kind === 'NewExpression' - ? Effect.Read - : Effect.ConditionallyMutate, - ValueReason.Other, - ); - } - hasCaptureArgument ||= callee.effect === Effect.Capture; - - state.initialize(instrValue, returnValueKind); - state.define(instr.lvalue, instrValue); - instr.lvalue.effect = hasCaptureArgument - ? Effect.Store - : Effect.ConditionallyMutate; -} diff --git a/compiler/packages/babel-plugin-react-compiler/src/Inference/InferTryCatchAliases.ts b/compiler/packages/babel-plugin-react-compiler/src/Inference/InferTryCatchAliases.ts deleted file mode 100644 index 3b33160820c68..0000000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/Inference/InferTryCatchAliases.ts +++ /dev/null @@ -1,49 +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 {BlockId, HIRFunction, Identifier} from '../HIR'; -import DisjointSet from '../Utils/DisjointSet'; - -/* - * Any values created within a try/catch block could be aliased to the try handler. - * Our lowering ensures that every instruction within a try block will be lowered into a - * basic block ending in a maybe-throw terminal that points to its catch block, so we can - * iterate such blocks and alias their instruction lvalues to the handler's param (if present). - */ -export function inferTryCatchAliases( - fn: HIRFunction, - aliases: DisjointSet, -): void { - const handlerParams: Map = new Map(); - for (const [_, block] of fn.body.blocks) { - if ( - block.terminal.kind === 'try' && - block.terminal.handlerBinding !== null - ) { - handlerParams.set( - block.terminal.handler, - block.terminal.handlerBinding.identifier, - ); - } else if (block.terminal.kind === 'maybe-throw') { - const handlerParam = handlerParams.get(block.terminal.handler); - if (handlerParam === undefined) { - /* - * There's no catch clause param, nothing to alias to so - * skip this block - */ - continue; - } - /* - * Otherwise alias all values created in this block to the - * catch clause param - */ - for (const instr of block.instructions) { - aliases.union([handlerParam, instr.lvalue.identifier]); - } - } - } -} diff --git a/compiler/packages/babel-plugin-react-compiler/src/Inference/InlineImmediatelyInvokedFunctionExpressions.ts b/compiler/packages/babel-plugin-react-compiler/src/Inference/InlineImmediatelyInvokedFunctionExpressions.ts index c6c6f2f54fbef..d71f6ebc8a063 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Inference/InlineImmediatelyInvokedFunctionExpressions.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Inference/InlineImmediatelyInvokedFunctionExpressions.ts @@ -11,13 +11,16 @@ import { Environment, FunctionExpression, GeneratedSource, + GotoTerminal, GotoVariant, HIRFunction, IdentifierId, InstructionKind, LabelTerminal, Place, + isStatementBlockKind, makeInstructionId, + mergeConsecutiveBlocks, promoteTemporary, reversePostorderBlocks, } from '../HIR'; @@ -72,6 +75,10 @@ import {retainWhere} from '../Utils/utils'; * - All return statements in the original function expression are replaced with a * StoreLocal to the temporary we allocated before plus a Goto to the fallthrough * block (code following the CallExpression). + * + * Note that if the inliined function has only one return, we avoid the labeled block + * and fully inline the code. The original return is replaced with an assignmen to the + * IIFE's call expression lvalue. */ export function inlineImmediatelyInvokedFunctionExpressions( fn: HIRFunction, @@ -90,100 +97,144 @@ export function inlineImmediatelyInvokedFunctionExpressions( */ const queue = Array.from(fn.body.blocks.values()); queue: for (const block of queue) { - for (let ii = 0; ii < block.instructions.length; ii++) { - const instr = block.instructions[ii]!; - switch (instr.value.kind) { - case 'FunctionExpression': { - if (instr.lvalue.identifier.name === null) { - functions.set(instr.lvalue.identifier.id, instr.value); - } - break; - } - case 'CallExpression': { - if (instr.value.args.length !== 0) { - // We don't support inlining when there are arguments - continue; - } - const body = functions.get(instr.value.callee.identifier.id); - if (body === undefined) { - // Not invoking a local function expression, can't inline - continue; + /* + * We can't handle labels inside expressions yet, so we don't inline IIFEs if they are in an + * expression block. + */ + if (isStatementBlockKind(block.kind)) { + for (let ii = 0; ii < block.instructions.length; ii++) { + const instr = block.instructions[ii]!; + switch (instr.value.kind) { + case 'FunctionExpression': { + if (instr.lvalue.identifier.name === null) { + functions.set(instr.lvalue.identifier.id, instr.value); + } + break; } + case 'CallExpression': { + if (instr.value.args.length !== 0) { + // We don't support inlining when there are arguments + continue; + } + const body = functions.get(instr.value.callee.identifier.id); + if (body === undefined) { + // Not invoking a local function expression, can't inline + continue; + } - if ( - body.loweredFunc.func.params.length > 0 || - body.loweredFunc.func.async || - body.loweredFunc.func.generator - ) { - // Can't inline functions with params, or async/generator functions - continue; - } + if ( + body.loweredFunc.func.params.length > 0 || + body.loweredFunc.func.async || + body.loweredFunc.func.generator + ) { + // Can't inline functions with params, or async/generator functions + continue; + } - // We know this function is used for an IIFE and can prune it later - inlinedFunctions.add(instr.value.callee.identifier.id); + // We know this function is used for an IIFE and can prune it later + inlinedFunctions.add(instr.value.callee.identifier.id); - // Create a new block which will contain code following the IIFE call - const continuationBlockId = fn.env.nextBlockId; - const continuationBlock: BasicBlock = { - id: continuationBlockId, - instructions: block.instructions.slice(ii + 1), - kind: block.kind, - phis: new Set(), - preds: new Set(), - terminal: block.terminal, - }; - fn.body.blocks.set(continuationBlockId, continuationBlock); + // Create a new block which will contain code following the IIFE call + const continuationBlockId = fn.env.nextBlockId; + const continuationBlock: BasicBlock = { + id: continuationBlockId, + instructions: block.instructions.slice(ii + 1), + kind: block.kind, + phis: new Set(), + preds: new Set(), + terminal: block.terminal, + }; + fn.body.blocks.set(continuationBlockId, continuationBlock); - /* - * Trim the original block to contain instructions up to (but not including) - * the IIFE - */ - block.instructions.length = ii; + /* + * Trim the original block to contain instructions up to (but not including) + * the IIFE + */ + block.instructions.length = ii; - /* - * To account for complex control flow within the lambda, we treat the lambda - * as if it were a single labeled statement, and replace all returns with gotos - * to the label fallthrough. - */ - const newTerminal: LabelTerminal = { - block: body.loweredFunc.func.body.entry, - id: makeInstructionId(0), - kind: 'label', - fallthrough: continuationBlockId, - loc: block.terminal.loc, - }; - block.terminal = newTerminal; + if (hasSingleExitReturnTerminal(body.loweredFunc.func)) { + block.terminal = { + kind: 'goto', + block: body.loweredFunc.func.body.entry, + id: block.terminal.id, + loc: block.terminal.loc, + variant: GotoVariant.Break, + } as GotoTerminal; + for (const block of body.loweredFunc.func.body.blocks.values()) { + if (block.terminal.kind === 'return') { + block.instructions.push({ + id: makeInstructionId(0), + loc: block.terminal.loc, + lvalue: instr.lvalue, + value: { + kind: 'LoadLocal', + loc: block.terminal.loc, + place: block.terminal.value, + }, + effects: null, + }); + block.terminal = { + kind: 'goto', + block: continuationBlockId, + id: block.terminal.id, + loc: block.terminal.loc, + variant: GotoVariant.Break, + } as GotoTerminal; + } + } + for (const [id, block] of body.loweredFunc.func.body.blocks) { + block.preds.clear(); + fn.body.blocks.set(id, block); + } + } else { + /* + * To account for multiple returns within the lambda, we treat the lambda + * as if it were a single labeled statement, and replace all returns with gotos + * to the label fallthrough. + */ + const newTerminal: LabelTerminal = { + block: body.loweredFunc.func.body.entry, + id: makeInstructionId(0), + kind: 'label', + fallthrough: continuationBlockId, + loc: block.terminal.loc, + }; + block.terminal = newTerminal; - // We store the result in the IIFE temporary - const result = instr.lvalue; + // We store the result in the IIFE temporary + const result = instr.lvalue; - // Declare the IIFE temporary - declareTemporary(fn.env, block, result); + // Declare the IIFE temporary + declareTemporary(fn.env, block, result); - // Promote the temporary with a name as we require this to persist - promoteTemporary(result.identifier); + // Promote the temporary with a name as we require this to persist + if (result.identifier.name == null) { + promoteTemporary(result.identifier); + } - /* - * Rewrite blocks from the lambda to replace any `return` with a - * store to the result and `goto` the continuation block - */ - for (const [id, block] of body.loweredFunc.func.body.blocks) { - block.preds.clear(); - rewriteBlock(fn.env, block, continuationBlockId, result); - fn.body.blocks.set(id, block); - } + /* + * Rewrite blocks from the lambda to replace any `return` with a + * store to the result and `goto` the continuation block + */ + for (const [id, block] of body.loweredFunc.func.body.blocks) { + block.preds.clear(); + rewriteBlock(fn.env, block, continuationBlockId, result); + fn.body.blocks.set(id, block); + } + } - /* - * Ensure we visit the continuation block, since there may have been - * sequential IIFEs that need to be visited. - */ - queue.push(continuationBlock); - continue queue; - } - default: { - for (const place of eachInstructionValueOperand(instr.value)) { - // Any other use of a function expression means it isn't an IIFE - functions.delete(place.identifier.id); + /* + * Ensure we visit the continuation block, since there may have been + * sequential IIFEs that need to be visited. + */ + queue.push(continuationBlock); + continue queue; + } + default: { + for (const place of eachInstructionValueOperand(instr.value)) { + // Any other use of a function expression means it isn't an IIFE + functions.delete(place.identifier.id); + } } } } @@ -192,7 +243,7 @@ export function inlineImmediatelyInvokedFunctionExpressions( if (inlinedFunctions.size !== 0) { // Remove instructions that define lambdas which we inlined - for (const [, block] of fn.body.blocks) { + for (const block of fn.body.blocks.values()) { retainWhere( block.instructions, instr => !inlinedFunctions.has(instr.lvalue.identifier.id), @@ -206,7 +257,23 @@ export function inlineImmediatelyInvokedFunctionExpressions( reversePostorderBlocks(fn.body); markInstructionIds(fn.body); markPredecessors(fn.body); + mergeConsecutiveBlocks(fn); + } +} + +/** + * Returns true if the function has a single exit terminal (throw/return) which is a return + */ +function hasSingleExitReturnTerminal(fn: HIRFunction): boolean { + let hasReturn = false; + let exitCount = 0; + for (const [, block] of fn.body.blocks) { + if (block.terminal.kind === 'return' || block.terminal.kind === 'throw') { + hasReturn ||= block.terminal.kind === 'return'; + exitCount++; + } } + return exitCount === 1 && hasReturn; } /* @@ -235,6 +302,7 @@ function rewriteBlock( type: null, loc: terminal.loc, }, + effects: null, }); block.terminal = { kind: 'goto', @@ -263,5 +331,6 @@ function declareTemporary( type: null, loc: result.loc, }, + effects: null, }); } diff --git a/compiler/packages/babel-plugin-react-compiler/src/Inference/MUTABILITY_ALIASING_MODEL.md b/compiler/packages/babel-plugin-react-compiler/src/Inference/MUTABILITY_ALIASING_MODEL.md new file mode 100644 index 0000000000000..ab327c255b109 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/Inference/MUTABILITY_ALIASING_MODEL.md @@ -0,0 +1,559 @@ +# The Mutability & Aliasing Model + +This document describes the new (as of June 2025) mutability and aliasing model powering React Compiler. The mutability and aliasing system is a conceptual subcomponent whose primary role is to determine minimal sets of values that mutate together, and the range of instructions over which those mutations occur. These minimal sets of values that mutate together, and the corresponding instructions doing those mutations, are ultimately grouped into reactive scopes, which then translate into memoization blocks in the output (after substantial additional processing described in the comments of those passes). + +To build an intuition, consider the following example: + +```js +function Component() { + // a is created and mutated over the course of these two instructions: + const a = {}; + mutate(a); + + // b and c are created and mutated together — mutate might modify b via c + const b = {}; + const c = {b}; + mutate(c); + + // does not modify a/b/c + return +} +``` + +The goal of mutability and aliasing inference is to understand the set of instructions that create/modify a, b, and c. + +In code, the mutability and aliasing model is compromised of the following phases: + +* `InferMutationAliasingEffects`. Infers a set of mutation and aliasing effects for each instruction. The approach is to generate a set of candidate effects based purely on the semantics of each instruction and the types of the operands, then use abstract interpretation to determine the actual effects (or errros) that would apply. For example, an instruction that by default has a Capture effect might downgrade to an ImmutableCapture effect if the value is known to be frozen. +* `InferMutationAliasingRanges`. Infers a mutable range (start:end instruction ids) for each value in the program, and annotates each Place with its effect type for usage in later passes. This builds a graph of data flow through the program over time in order to understand which mutations effect which values. +* `InferReactiveScopeVariables`. Given the per-Place effects, determines disjoint sets of values that mutate together and assigns all identifiers in each set to a unique scope, and updates the range to include the ranges of all constituent values. + +Finally, `AnalyzeFunctions` needs to understand the mutation and aliasing semantics of nested FunctionExpression and ObjectMethod values. `AnalyzeFunctions` calls `InferFunctionExpressionAliasingEffectsSignature` to determine the publicly observable set of mutation/aliasing effects for nested functions. + +## Mutation and Aliasing Effects + +The inference model is based on a set of "effects" that describe subtle aspects of mutation, aliasing, and other changes to the state of values over time + +### Creation Effects + +#### Create + +```js +{ + kind: 'Create'; + into: Place; + value: ValueKind; + reason: ValueReason; +} +``` + +Describes the creation of a new value with the given kind, and reason for having that kind. For example, `x = 10` might have an effect like `Create x = ValueKind.Primitive [ValueReason.Other]`. + +#### CreateFunction + +```js +{ + kind: 'CreateFunction'; + captures: Array; + function: FunctionExpression | ObjectMethod; + into: Place; +} +``` + +Describes the creation of new function value, capturing the given set of mutable values. CreateFunction is used to specifically track function types so that we can precisely model calls to those functions with `Apply`. + +#### Apply + +```js +{ + kind: 'Apply'; + receiver: Place; + function: Place; // same as receiver for function calls + mutatesFunction: boolean; // indicates if this is a type that we consdier to mutate the function itself by default + args: Array; + into: Place; // where result is stored + signature: FunctionSignature | null; +} +``` + +Describes the potential creation of a value by calling a function. This models `new`, function calls, and method calls. The inference algorithm uses the most precise signature it can determine: + +* If the function is a locally created function expression, we use a signature inferred from the behavior of that function to interpret the effects of calling it with the given arguments. +* Else if the function has a known aliasing signature (new style precise effects signature), we apply the arguments to that signature to get a precise set of effects. +* Else if the function has a legacy style signature (with per-param effects) we convert the legacy per-Place effects into aliasing effects (described in this doc) and apply those. +* Else fall back to inferring a generic set of effects. + +The generic fallback is to assume: +- The return value may alias any of the arguments (Alias param -> return) +- Any arguments *may* be transitively mutated (MutateTransitiveConditionally param) +- Any argument may be captured into any other argument (Capture paramN -> paramM for all N,M where N != M) + +### Aliasing Effects + +These effects describe data-flow only, separately from mutation or other state-changing semantics. + +#### Assign + +```js +{ + kind: 'Assign'; + from: Place; + into: Place; +} +``` + +Describes an `x = y` assignment, where the receiving (into) value is overwritten with a new (from) value. After this effect, any previous assignments/aliases to the receiving value are dropped. Note that `Alias` initializes the receiving value. + +> TODO: InferMutationAliasingRanges may not fully reset aliases on encountering this effect + +#### Alias + +```js +{ + kind: 'Alias'; + from: Place; + into: Place; +} +``` + +Describes that an assignment _may_ occur, but that the possible assignment is non-exclusive. The canonical use-case for `Alias` is a function that may return more than one of its arguments, such as `(x, y, z) => x ? y : z`. Here, the result of this function may be `y` or `z`, but neither one overwrites the other. Note that `Alias` does _not_ initialize the receiving value: it should always be paired with an effect to create the receiving value. + +#### Capture + +```js +{ + kind: 'Capture'; + from: Place; + into: Place; +} +``` + +Describes that a reference to one variable (from) is stored within another value (into). Examples include: +- An array expression captures the items of the array (`array = [capturedValue]`) +- Array.prototype.push captures the pushed values into the array (`array.push(capturedValue)`) +- Property assignment captures the value onto the object (`object.property = capturedValue`) + +#### CreateFrom + +```js +{ + kind: 'CreateFrom'; + from: Place; + into: Place; +} +``` + +This is somewhat the inverse of `Capture`. The `CreateFrom` effect describes that a variable is initialized by extracting _part_ of another value, without taking a direct alias to the full other value. Examples include: + +- Indexing into an array (`createdFrom = array[0]`) +- Reading an object property (`createdFrom = object.property`) +- Getting a Map key (`createdFrom = map.get(key)`) + +#### ImmutableCapture + +Describes immutable data flow from one value to another. This is not currently used for anything, but is intended to eventually power a more sophisticated escape analysis. + +### MaybeAlias + +Describes potential data flow that the compiler knows may occur behind a function call, but cannot be sure about. For example, `foo(x)` _may_ be the identity function and return `x`, or `cond(a, b, c)` may conditionally return `b` or `c` depending on the value of `a`, but those functions could just as easily return new mutable values and not capture any information from their arguments. MaybeAlias represents that we have to consider the potential for data flow when deciding mutable ranges, but should be conservative about reporting errors. For example, `foo(someFrozenValue).property = true` should not error since we don't know for certain that foo returns its input. + +### State-Changing Effects + +The following effects describe state changes to specific values, not data flow. In many cases, JavaScript semantics will involve a combination of both data-flow effects *and* state-change effects. For example, `object.property = value` has data flow (`Capture object <- value`) and mutation (`Mutate object`). + +#### Freeze + +```js +{ + kind: 'Freeze', + // The reference being frozen + value: Place; + // The reason the value is frozen (passed to a hook, passed to jsx, etc) + reason: ValueReason; +} +``` + +Once a reference to a value has been passed to React, that value is generally not safe to mutate further. This is not a strictly required property of React, but is a natural consequence of making components and hooks composable without leaking implementation details. Concretely, once a value has been passed as a JSX prop, passed as argument to a hook, or returned from a hook, it must be assumed that the other "side" — receiver of the prop/argument/return value — will use that value as an input to an effect or memoization unit. Mutating that value (instead of creating a new value) will fail to cause the consuming computation to update: + +```js +// INVALID DO NOT DO THIS +function Component(props) { + const array = useArray(props.value); + // OOPS! this value is memoized, the array won't get re-created + // when `props.value` changes, so we might just keep pushing new + // values to the same array on every render! + array.push(props.otherValue); +} + +function useArray(a) { + return useMemo(() => [a], [a]); +} +``` + +The **Freeze** effect accepts a variable reference and a reason that the value is being frozen. Note: _freeze only applies to the reference, not the underlying value_. Our inference is conservative, and assumes that there may still be other references to the same underlying value which are mutated later. For example: + +```js +const x = {}; +const y = []; +x.y = y; +freeze(y); // y _reference_ is frozen +x.y.push(props.value); // but y is still considered mutable bc of this +``` + +#### Mutate (and MutateConditionally) + +```js +{ + kind: 'Mutate'; + value: Place; +} +``` + +Mutate indicates that a value is mutated, without modifying any of the values that it may transitively have captured. Canonical examples include: + +- Pushing an item onto an array modifies the array, but does not modify any items stored _within_ the array (unless the array has a reference to itself!) +- Assigning a value to an object property modifies the object, but not any values stored in the object's other properties. + +This helps explain the distinction between Assign/Alias and Capture: Mutate only affects assign/alias but not captures. + +`MutateConditionally` is an alternative in which the mutation _may_ happen depending on the type of the value. The conditional variant is not generally used and included for completeness. + + + +#### MutateTransitiveConditionally (and MutateTransitive) + +`MutateTransitiveConditionally` represents an operation that may mutate _any_ aspect of a value, including reaching arbitrarily deep into nested values to mutate them. This is the default semantic for unknown functions — we have no idea what they do, so we assume that they are idempotent but may mutate any aspect of the mutable values that are passed to them. + +There is also `MutateTransitive` for completeness, but this is not generally used. + +### Side Effects + +Finally, there are a few effects that describe error, or potential error, conditions: + +- `MutateFrozen` is always an error, because it indicates known mutation of a value that should not be mutated. +- `MutateGlobal` indicates known mutation of a global value, which is not safe during render. This effect is an error if reachable during render, but allowed if only reachable via an event handler or useEffect. +- `Impure` indicates calling some other logic that is impure/side-effecting. This is an error if reachable during render, but allowed if only reachable via an event handler or useEffect. + - TODO: we could probably merge this and MutateGlobal +- `Render` indicates a value that is not mutated, but is known to be called during render. It's used for a few particular places like JSX tags and JSX children, which we assume are accessed during render (while other props may be event handlers etc). This helps to detect more MutateGlobal/Impure effects and reject more invalid programs. + + +## Rules + +### Mutation of Alias Mutates the Source Value + +``` +Alias a <- b +Mutate a +=> +Mutate b +``` + +Example: + +```js +const a = maybeIdentity(b); // Alias a <- b +a.property = value; // a could be b, so this mutates b +``` + +### Mutation of Assignment Mutates the Source Value + +``` +Assign a <- b +Mutate a +=> +Mutate b +``` + +Example: + +```js +const a = b; +a.property = value // a _is_ b, this mutates b +``` + +### Mutation of CreateFrom Mutates the Source Value + +``` +CreateFrom a <- b +Mutate a +=> +Mutate b +``` + +Example: + +```js +const a = b[index]; +a.property = value // the contents of b are transitively mutated +``` + + +### Mutation of Capture Does *Not* Mutate the Source Value + +``` +Capture a <- b +Mutate a +!=> +~Mutate b~ +``` + +Example: + +```js +const a = {}; +a.b = b; +a.property = value; // mutates a, not b +``` + +### Mutation of Source Affects Alias, Assignment, CreateFrom, and Capture + +``` +Alias a <- b OR Assign a <- b OR CreateFrom a <- b OR Capture a <- b +Mutate b +=> +Mutate a +``` + +A derived value changes when it's source value is mutated. + +Example: + +```js +const x = {}; +const y = [x]; +x.y = true; // this changes the value within `y` ie mutates y +``` + + +### TransitiveMutation of Alias, Assignment, CreateFrom, or Capture Mutates the Source + +``` +Alias a <- b OR Assign a <- b OR CreateFrom a <- b OR Capture a <- b +MutateTransitive a +=> +MutateTransitive b +``` + +Remember, the intuition for a transitive mutation is that it's something that could traverse arbitrarily deep into an object and mutate whatever it finds. Imagine something that recurses into every nested object/array and sets `.field = value`. Given a function `mutate()` that does this, then: + +```js +const a = b; // assign +mutate(a); // clearly can transitively mutate b + +const a = maybeIdentity(b); // alias +mutate(a); // clearly can transitively mutate b + +const a = b[index]; // createfrom +mutate(a); // clearly can transitively mutate b + +const a = {}; +a.b = b; // capture +mutate(a); // can transitively mutate b +``` + +### MaybeAlias makes mutation conditional + +Because we don't know for certain that the aliasing occurs, we consider the mutation conditional against the source. + +``` +MaybeAlias a <- b +Mutate a +=> +MutateConditional b +``` + +### Freeze Does Not Freeze the Value + +Freeze does not freeze the value itself: + +``` +Create x +Assign y <- x OR Alias y <- x OR CreateFrom y <- x OR Capture y <- x +Freeze y +!=> +~Freeze x~ +``` + +This means that subsequent mutations of the original value are valid: + +``` +Create x +Assign y <- x OR Alias y <- x OR CreateFrom y <- x OR Capture y <- x +Freeze y +Mutate x +=> +Mutate x (mutation is ok) +``` + +As well as mutations through other assignments/aliases/captures/createfroms of the original value: + +``` +Create x +Assign y <- x OR Alias y <- x OR CreateFrom y <- x OR Capture y <- x +Freeze y +Alias z <- x OR Capture z <- x OR CreateFrom z <- x OR Assign z <- x +Mutate z +=> +Mutate x (mutation is ok) +``` + +### Freeze Freezes The Reference + +Although freeze doesn't freeze the value, it does affect the reference. The reference cannot be used to mutate. + +Conditional mutations of the reference are no-ops: + +``` +Create x +Assign y <- x OR Alias y <- x OR CreateFrom y <- x OR Capture y <- x +Freeze y +MutateConditional y +=> +(no mutation) +``` + +And known mutations of the reference are errors: + +``` +Create x +Assign y <- x OR Alias y <- x OR CreateFrom y <- x OR Capture y <- x +Freeze y +MutateConditional y +=> +MutateFrozen y error=... +``` + +### Corollary: Transitivity of Assign/Alias/CreateFrom/Capture + +A key part of the inference model is inferring a signature for function expressions. The signature is a minimal set of effects that describes the publicly observable behavior of the function. This can include "global" effects like side effects (MutateGlobal/Impure) as well as mutations/aliasing of parameters and free variables. + +In order to determine the aliasing of params and free variables into each other and/or the return value, we may encounter chains of assign, alias, createfrom, and capture effects. For example: + +```js +const f = (x) => { + const y = [x]; // capture y <- x + const z = y[0]; // createfrom z <- y + return z; // assign return <- z +} +// return <- x +``` + +In this example we can see that there should be some effect on `f` that tracks the flow of data from `x` into the return value. The key constraint is preserving the semantics around how local/transitive mutations of the destination would affect the source. + +#### Each of the effects is transitive with itself + +``` +Assign b <- a +Assign c <- b +=> +Assign c <- a +``` + +``` +Alias b <- a +Alias c <- b +=> +Alias c <- a +``` + +``` +CreateFrom b <- a +CreateFrom c <- b +=> +CreateFrom c <- a +``` + +``` +Capture b <- a +Capture c <- b +=> +Capture c <- a +``` + +#### Alias > Assign + +``` +Assign b <- a +Alias c <- b +=> +Alias c <- a +``` + +``` +Alias b <- a +Assign c <- b +=> +Alias c <- a +``` + +### CreateFrom > Assign/Alias + +Intuition: + +``` +CreateFrom b <- a +Alias c <- b OR Assign c <- b +=> +CreateFrom c <- a +``` + +``` +Alias b <- a OR Assign b <- a +CreateFrom c <- b +=> +CreateFrom c <- a +``` + +### Capture > Assign/Alias + +Intuition: capturing means that a local mutation of the destination will not affect the source, so we preserve the capture. + +``` +Capture b <- a +Alias c <- b OR Assign c <- b +=> +Capture c <- a +``` + +``` +Alias b <- a OR Assign b <- a +Capture c <- b +=> +Capture c <- a +``` + +### Capture And CreateFrom + +Intuition: these effects are inverses of each other (capturing into an object, extracting from an object). The result is based on the order of operations: + +Capture then CreatFrom is equivalent to Alias: we have to assume that the result _is_ the original value and that a local mutation of the result could mutate the original. + +```js +const b = [a]; // capture +const c = b[0]; // createfrom +mutate(c); // this clearly can mutate a, so the result must be one of Assign/Alias/CreateFrom +``` + +We use Alias as the return type because the mutability kind of the result is not derived from the source value (there's a fresh object in between due to the capture), so the full set of effects in practice would be a Create+Alias. + +``` +Capture b <- a +CreateFrom c <- b +=> +Alias c <- a +``` + +Meanwhile the opposite direction preserves the capture, because the result is not the same as the source: + +```js +const b = a[0]; // createfrom +const c = [b]; // capture +mutate(c); // does not mutate a, so the result must be Capture +``` + +``` +CreateFrom b <- a +Capture c <- b +=> +Capture c <- a +``` \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/Inference/index.ts b/compiler/packages/babel-plugin-react-compiler/src/Inference/index.ts index 93b99fb385262..eb645cc218dc3 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Inference/index.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Inference/index.ts @@ -7,8 +7,6 @@ export {default as analyseFunctions} from './AnalyseFunctions'; export {dropManualMemoization} from './DropManualMemoization'; -export {inferMutableRanges} from './InferMutableRanges'; export {inferReactivePlaces} from './InferReactivePlaces'; -export {default as inferReferenceEffects} from './InferReferenceEffects'; export {inlineImmediatelyInvokedFunctionExpressions} from './InlineImmediatelyInvokedFunctionExpressions'; export {inferEffectDependencies} from './InferEffectDependencies'; diff --git a/compiler/packages/babel-plugin-react-compiler/src/Optimization/InlineJsxTransform.ts b/compiler/packages/babel-plugin-react-compiler/src/Optimization/InlineJsxTransform.ts index 29c59c7b3644a..3588cf32f9f66 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Optimization/InlineJsxTransform.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Optimization/InlineJsxTransform.ts @@ -42,6 +42,7 @@ import { mapInstructionValueOperands, mapTerminalOperands, } from '../HIR/visitors'; +import {ErrorCategory} from '../CompilerError'; type InlinedJsxDeclarationMap = Map< DeclarationId, @@ -83,6 +84,7 @@ export function inlineJsxTransform( kind: 'CompileDiagnostic', fnLoc: null, detail: { + category: ErrorCategory.Todo, reason: 'JSX Inlining is not supported on value blocks', loc: instr.loc, }, @@ -151,6 +153,7 @@ export function inlineJsxTransform( type: null, loc: instr.value.loc, }, + effects: null, loc: instr.loc, }; currentBlockInstructions.push(varInstruction); @@ -167,6 +170,7 @@ export function inlineJsxTransform( }, loc: instr.value.loc, }, + effects: null, loc: instr.loc, }; currentBlockInstructions.push(devGlobalInstruction); @@ -220,6 +224,7 @@ export function inlineJsxTransform( type: null, loc: instr.value.loc, }, + effects: null, loc: instr.loc, }; thenBlockInstructions.push(reassignElseInstruction); @@ -292,6 +297,7 @@ export function inlineJsxTransform( ], loc: instr.value.loc, }, + effects: null, loc: instr.loc, }; elseBlockInstructions.push(reactElementInstruction); @@ -309,6 +315,7 @@ export function inlineJsxTransform( type: null, loc: instr.value.loc, }, + effects: null, loc: instr.loc, }; elseBlockInstructions.push(reassignConditionalInstruction); @@ -436,6 +443,7 @@ function createSymbolProperty( binding: {kind: 'Global', name: 'Symbol'}, loc: instr.value.loc, }, + effects: null, loc: instr.loc, }; nextInstructions.push(symbolInstruction); @@ -450,6 +458,7 @@ function createSymbolProperty( property: makePropertyLiteral('for'), loc: instr.value.loc, }, + effects: null, loc: instr.loc, }; nextInstructions.push(symbolForInstruction); @@ -463,6 +472,7 @@ function createSymbolProperty( value: symbolName, loc: instr.value.loc, }, + effects: null, loc: instr.loc, }; nextInstructions.push(symbolValueInstruction); @@ -478,6 +488,7 @@ function createSymbolProperty( args: [symbolValueInstruction.lvalue], loc: instr.value.loc, }, + effects: null, loc: instr.loc, }; const $$typeofProperty: ObjectProperty = { @@ -508,6 +519,7 @@ function createTagProperty( value: componentTag.name, loc: instr.value.loc, }, + effects: null, loc: instr.loc, }; tagProperty = { @@ -634,6 +646,7 @@ function createPropsProperties( elements: [...children], loc: instr.value.loc, }, + effects: null, loc: instr.loc, }; nextInstructions.push(childrenPropInstruction); @@ -657,6 +670,7 @@ function createPropsProperties( value: null, loc: instr.value.loc, }, + effects: null, loc: instr.loc, }; refProperty = { @@ -678,6 +692,7 @@ function createPropsProperties( value: null, loc: instr.value.loc, }, + effects: null, loc: instr.loc, }; keyProperty = { @@ -711,6 +726,7 @@ function createPropsProperties( properties: props, loc: instr.value.loc, }, + effects: null, loc: instr.loc, }; propsProperty = { diff --git a/compiler/packages/babel-plugin-react-compiler/src/Optimization/LowerContextAccess.ts b/compiler/packages/babel-plugin-react-compiler/src/Optimization/LowerContextAccess.ts index 834f60195af29..62845934c16f4 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Optimization/LowerContextAccess.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Optimization/LowerContextAccess.ts @@ -25,7 +25,6 @@ import { makeBlockId, makeInstructionId, makePropertyLiteral, - makeType, markInstructionIds, promoteTemporary, reversePostorderBlocks, @@ -146,6 +145,7 @@ function emitLoadLoweredContextCallee( id: makeInstructionId(0), loc: GeneratedSource, lvalue: createTemporaryPlace(env, GeneratedSource), + effects: null, value: loadGlobal, }; } @@ -192,6 +192,7 @@ function emitPropertyLoad( lvalue: object, value: loadObj, id: makeInstructionId(0), + effects: null, loc: GeneratedSource, }; @@ -206,6 +207,7 @@ function emitPropertyLoad( lvalue: element, value: loadProp, id: makeInstructionId(0), + effects: null, loc: GeneratedSource, }; return { @@ -235,8 +237,10 @@ function emitSelectorFn(env: Environment, keys: Array): Instruction { terminal: { id: makeInstructionId(0), kind: 'return', + returnVariant: 'Explicit', loc: GeneratedSource, value: arrayInstr.lvalue, + effects: null, }, preds: new Set(), phis: new Set(), @@ -249,9 +253,8 @@ function emitSelectorFn(env: Environment, keys: Array): Instruction { env, params: [obj], returnTypeAnnotation: null, - returnType: makeType(), + returns: createTemporaryPlace(env, GeneratedSource), context: [], - effects: null, body: { entry: block.id, blocks: new Map([[block.id, block]]), @@ -259,6 +262,7 @@ function emitSelectorFn(env: Environment, keys: Array): Instruction { generator: false, async: false, directives: [], + aliasingEffects: [], }; reversePostorderBlocks(fn.body); @@ -278,6 +282,7 @@ function emitSelectorFn(env: Environment, keys: Array): Instruction { loc: GeneratedSource, }, lvalue: createTemporaryPlace(env, GeneratedSource), + effects: null, loc: GeneratedSource, }; return fnInstr; @@ -294,6 +299,7 @@ function emitArrayInstr(elements: Array, env: Environment): Instruction { id: makeInstructionId(0), value: array, lvalue: arrayLvalue, + effects: null, loc: GeneratedSource, }; return arrayInstr; diff --git a/compiler/packages/babel-plugin-react-compiler/src/Optimization/OutlineJsx.ts b/compiler/packages/babel-plugin-react-compiler/src/Optimization/OutlineJsx.ts index d35c4d77362db..6232456e620c3 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Optimization/OutlineJsx.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Optimization/OutlineJsx.ts @@ -21,7 +21,6 @@ import { makeBlockId, makeIdentifierName, makeInstructionId, - makeType, ObjectProperty, Place, promoteTemporary, @@ -297,6 +296,7 @@ function emitOutlinedJsx( }, loc: GeneratedSource, }, + effects: null, }; promoteTemporaryJsxTag(loadJsx.lvalue.identifier); const jsxExpr: Instruction = { @@ -312,6 +312,7 @@ function emitOutlinedJsx( openingLoc: GeneratedSource, closingLoc: GeneratedSource, }, + effects: null, }; return [loadJsx, jsxExpr]; @@ -351,8 +352,10 @@ function emitOutlinedFn( terminal: { id: makeInstructionId(0), kind: 'return', + returnVariant: 'Explicit', loc: GeneratedSource, value: instructions.at(-1)!.lvalue, + effects: null, }, preds: new Set(), phis: new Set(), @@ -365,9 +368,8 @@ function emitOutlinedFn( env, params: [propsObj], returnTypeAnnotation: null, - returnType: makeType(), + returns: createTemporaryPlace(env, GeneratedSource), context: [], - effects: null, body: { entry: block.id, blocks: new Map([[block.id, block]]), @@ -375,6 +377,7 @@ function emitOutlinedFn( generator: false, async: false, directives: [], + aliasingEffects: [], }; return fn; } @@ -517,6 +520,7 @@ function emitDestructureProps( loc: GeneratedSource, value: propsObj, }, + effects: null, }; return destructurePropsInstr; } diff --git a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/AlignReactiveScopesToBlockScopesHIR.ts b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/AlignReactiveScopesToBlockScopesHIR.ts index 2b4e890a40da8..e440340bd29f3 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/AlignReactiveScopesToBlockScopesHIR.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/AlignReactiveScopesToBlockScopesHIR.ts @@ -175,6 +175,41 @@ export function alignReactiveScopesToBlockScopesHIR(fn: HIRFunction): void { if (node != null) { valueBlockNodes.set(fallthrough, node); } + } else if (terminal.kind === 'goto') { + /** + * If we encounter a goto that is not to the natural fallthrough of the current + * block (not the topmost fallthrough on the stack), then this is a goto to a + * label. Any scopes that extend beyond the goto must be extended to include + * the labeled range, so that the break statement doesn't accidentally jump + * out of the scope. We do this by extending the start and end of the scope's + * range to the label and its fallthrough respectively. + */ + const start = activeBlockFallthroughRanges.find( + range => range.fallthrough === terminal.block, + ); + if (start != null && start !== activeBlockFallthroughRanges.at(-1)) { + const fallthroughBlock = fn.body.blocks.get(start.fallthrough)!; + const firstId = + fallthroughBlock.instructions[0]?.id ?? fallthroughBlock.terminal.id; + for (const scope of activeScopes) { + /** + * activeScopes is only filtered at block start points, so some of the + * scopes may not actually be active anymore, ie we've past their end + * instruction. Only extend ranges for scopes that are actually active. + * + * TODO: consider pruning activeScopes per instruction + */ + if (scope.range.end <= terminal.id) { + continue; + } + scope.range.start = makeInstructionId( + Math.min(start.range.start, scope.range.start), + ); + scope.range.end = makeInstructionId( + Math.max(firstId, scope.range.end), + ); + } + } } /* 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 33a124dcec6e2..c02a41f8f0aaf 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/CodegenReactiveFunction.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/CodegenReactiveFunction.ts @@ -13,7 +13,7 @@ import { pruneUnusedLabels, renameVariables, } from '.'; -import {CompilerError, ErrorSeverity} from '../CompilerError'; +import {CompilerError, ErrorCategory, ErrorSeverity} from '../CompilerError'; import {Environment, ExternalFunction} from '../HIR'; import { ArrayPattern, @@ -44,7 +44,7 @@ import { getHookKind, makeIdentifierName, } from '../HIR/HIR'; -import {printIdentifier, printPlace} from '../HIR/PrintHIR'; +import {printIdentifier, printInstruction, printPlace} from '../HIR/PrintHIR'; import {eachPatternOperand} from '../HIR/visitors'; import {Err, Ok, Result} from '../Utils/Result'; import {GuardKind} from '../Utils/RuntimeDiagnosticConstants'; @@ -349,11 +349,9 @@ function codegenReactiveFunction( fn: ReactiveFunction, ): Result { for (const param of fn.params) { - if (param.kind === 'Identifier') { - cx.temp.set(param.identifier.declarationId, null); - } else { - cx.temp.set(param.place.identifier.declarationId, null); - } + const place = param.kind === 'Identifier' ? param : param.place; + cx.temp.set(place.identifier.declarationId, null); + cx.declare(place.identifier); } const params = fn.params.map(param => convertParameter(param)); @@ -1183,7 +1181,7 @@ function codegenTerminal( ? codegenPlaceToExpression(cx, case_.test) : null; const block = codegenBlock(cx, case_.block!); - return t.switchCase(test, [block]); + return t.switchCase(test, block.body.length === 0 ? [] : [block]); }), ); } @@ -1310,7 +1308,7 @@ function codegenInstructionNullable( }); CompilerError.invariant(value?.type === 'FunctionExpression', { reason: 'Expected a function as a function declaration value', - description: null, + description: `Got ${value == null ? String(value) : value.type} at ${printInstruction(instr)}`, loc: instr.value.loc, suggestions: null, }); @@ -1726,7 +1724,7 @@ function codegenInstructionValue( } case 'UnaryExpression': { value = t.unaryExpression( - instrValue.operator as 'throw', // todo + instrValue.operator, codegenPlaceToExpression(cx, instrValue.value), ); break; @@ -2187,6 +2185,7 @@ function codegenInstructionValue( (declarator.id as t.Identifier).name }'`, severity: ErrorSeverity.Todo, + category: ErrorCategory.Todo, loc: declarator.loc ?? null, suggestions: null, }); @@ -2195,6 +2194,7 @@ function codegenInstructionValue( cx.errors.push({ reason: `(CodegenReactiveFunction::codegenInstructionValue) Handle conversion of ${stmt.type} to expression`, severity: ErrorSeverity.Todo, + category: ErrorCategory.Todo, loc: stmt.loc ?? null, suggestions: null, }); @@ -2582,7 +2582,16 @@ function codegenValue( value: boolean | number | string | null | undefined, ): t.Expression { if (typeof value === 'number') { - return t.numericLiteral(value); + if (value < 0) { + /** + * Babel's code generator produces invalid JS for negative numbers when + * run with { compact: true }. + * See repro https://codesandbox.io/p/devbox/5d47fr + */ + return t.unaryExpression('-', t.numericLiteral(-value), false); + } else { + return t.numericLiteral(value); + } } else if (typeof value === 'boolean') { return t.booleanLiteral(value); } else if (typeof value === 'string') { diff --git a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/ExtractScopeDeclarationsFromDestructuring.ts b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/ExtractScopeDeclarationsFromDestructuring.ts index eb2caa424e417..642b89747e6ea 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/ExtractScopeDeclarationsFromDestructuring.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/ExtractScopeDeclarationsFromDestructuring.ts @@ -79,6 +79,10 @@ export function extractScopeDeclarationsFromDestructuring( fn: ReactiveFunction, ): void { const state = new State(fn.env); + for (const param of fn.params) { + const place = param.kind === 'Identifier' ? param : param.place; + state.declared.add(place.identifier.declarationId); + } visitReactiveFunction(fn, new Visitor(), state); } 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 08d2212d86b95..a7acf5d66250f 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/MergeReactiveScopesThatInvalidateTogether.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/MergeReactiveScopesThatInvalidateTogether.ts @@ -119,6 +119,7 @@ class FindLastUsageVisitor extends ReactiveFunctionVisitor { class Transform extends ReactiveFunctionTransform { lastUsage: Map; + temporaries: Map = new Map(); constructor(lastUsage: Map) { super(); @@ -215,6 +216,12 @@ class Transform extends ReactiveFunctionTransform, ): boolean { // Don't merge scopes with reassignments if ( @@ -456,6 +471,7 @@ function canMergeScopes( new Set( [...current.scope.declarations.values()].map(declaration => ({ identifier: declaration.identifier, + reactive: true, path: [], })), ), @@ -464,11 +480,14 @@ function canMergeScopes( (next.scope.dependencies.size !== 0 && [...next.scope.dependencies].every( dep => + dep.path.length === 0 && isAlwaysInvalidatingType(dep.identifier.type) && Iterable_some( current.scope.declarations.values(), decl => - decl.identifier.declarationId === dep.identifier.declarationId, + decl.identifier.declarationId === dep.identifier.declarationId || + decl.identifier.declarationId === + temporaries.get(dep.identifier.declarationId), ), )) ) { @@ -476,12 +495,16 @@ function canMergeScopes( return true; } log(` cannot merge scopes:`); - log(` ${printReactiveScopeSummary(current.scope)}`); - log(` ${printReactiveScopeSummary(next.scope)}`); + log( + ` ${printReactiveScopeSummary(current.scope)} ${[...current.scope.declarations.values()].map(decl => decl.identifier.declarationId)}`, + ); + log( + ` ${printReactiveScopeSummary(next.scope)} ${[...next.scope.dependencies].map(dep => `${dep.identifier.declarationId} ${temporaries.get(dep.identifier.declarationId) ?? dep.identifier.declarationId}`)}`, + ); return false; } -function isAlwaysInvalidatingType(type: Type): boolean { +export function isAlwaysInvalidatingType(type: Type): boolean { switch (type.kind) { case 'Object': { switch (type.shapeId) { 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 5ae4c7dfc72f9..5735f7e80115b 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/PruneNonEscapingScopes.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/PruneNonEscapingScopes.ts @@ -24,7 +24,6 @@ import { getHookKind, isMutableEffect, } from '../HIR'; -import {getFunctionCallSignature} from '../Inference/InferReferenceEffects'; import {assertExhaustive, getOrInsertDefault} from '../Utils/utils'; import {getPlaceScope, ReactiveScope} from '../HIR/HIR'; import { @@ -35,6 +34,7 @@ import { visitReactiveFunction, } from './visitors'; import {printPlace} from '../HIR/PrintHIR'; +import {getFunctionCallSignature} from '../Inference/InferMutationAliasingEffects'; /* * This pass prunes reactive scopes that are not necessary to bound downstream computation. @@ -411,7 +411,9 @@ class CollectDependenciesVisitor extends ReactiveFunctionVisitor< this.state = state; this.options = { memoizeJsxElements: !this.env.config.enableForest, - forceMemoizePrimitives: this.env.config.enableForest, + forceMemoizePrimitives: + this.env.config.enableForest || + this.env.config.enablePreserveExistingMemoizationGuarantees, }; } @@ -534,9 +536,23 @@ class CollectDependenciesVisitor extends ReactiveFunctionVisitor< case 'JSXText': case 'BinaryExpression': case 'UnaryExpression': { - const level = options.forceMemoizePrimitives - ? MemoizationLevel.Memoized - : MemoizationLevel.Never; + if (options.forceMemoizePrimitives) { + /** + * Because these instructions produce primitives we usually don't consider + * them as escape points: they are known to copy, not return references. + * However if we're forcing memoization of primitives then we mark these + * instructions as needing memoization and walk their rvalues to ensure + * any scopes transitively reachable from the rvalues are considered for + * memoization. Note: we may still prune primitive-producing scopes if + * they don't ultimately escape at all. + */ + const level = MemoizationLevel.Conditional; + return { + lvalues: lvalue !== null ? [{place: lvalue, level}] : [], + rvalues: [...eachReactiveValueOperand(value)], + }; + } + const level = MemoizationLevel.Never; return { // All of these instructions return a primitive value and never need to be memoized lvalues: lvalue !== null ? [{place: lvalue, level}] : [], @@ -685,9 +701,7 @@ class CollectDependenciesVisitor extends ReactiveFunctionVisitor< } case 'ComputedLoad': case 'PropertyLoad': { - const level = options.forceMemoizePrimitives - ? MemoizationLevel.Memoized - : MemoizationLevel.Conditional; + const level = MemoizationLevel.Conditional; return { // Indirection for the inner value, memoized if the value is lvalues: lvalue !== null ? [{place: lvalue, level}] : [], @@ -829,12 +843,14 @@ class CollectDependenciesVisitor extends ReactiveFunctionVisitor< }; } case 'UnsupportedNode': { - CompilerError.invariant(false, { - reason: `Unexpected unsupported node`, - description: null, - loc: value.loc, - suggestions: null, - }); + const lvalues = []; + if (lvalue !== null) { + lvalues.push({place: lvalue, level: MemoizationLevel.Never}); + } + return { + lvalues, + rvalues: [], + }; } default: { assertExhaustive( @@ -1064,12 +1080,29 @@ class PruneScopesTransform extends ReactiveFunctionTransform< const value = instruction.value; if (value.kind === 'StoreLocal' && value.lvalue.kind === 'Reassign') { + // Complex cases of useMemo inlining result in a temporary that is reassigned const ids = getOrInsertDefault( this.reassignments, value.lvalue.place.identifier.declarationId, new Set(), ); ids.add(value.value.identifier); + } else if ( + value.kind === 'LoadLocal' && + value.place.identifier.scope != null && + instruction.lvalue != null && + instruction.lvalue.identifier.scope == null + ) { + /* + * Simpler cases result in a direct assignment to the original lvalue, with a + * LoadLocal + */ + const ids = getOrInsertDefault( + this.reassignments, + instruction.lvalue.identifier.declarationId, + new Set(), + ); + ids.add(value.place.identifier); } else if (value.kind === 'FinishMemoize') { let decls; if (value.decl.identifier.scope == null) { diff --git a/compiler/packages/babel-plugin-react-compiler/src/Transform/TransformFire.ts b/compiler/packages/babel-plugin-react-compiler/src/Transform/TransformFire.ts index b033af6750c37..393f95d94c46b 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Transform/TransformFire.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Transform/TransformFire.ts @@ -42,6 +42,7 @@ import { import {eachInstructionOperand} from '../HIR/visitors'; import {printSourceLocationLine} from '../HIR/PrintHIR'; import {USE_FIRE_FUNCTION_NAME} from '../HIR/Environment'; +import {ErrorCategory} from '../CompilerError'; /* * TODO(jmbrown): @@ -133,6 +134,7 @@ function replaceFireFunctions(fn: HIRFunction, context: Context): void { loc: value.loc, description: null, severity: ErrorSeverity.Invariant, + category: ErrorCategory.Invariant, reason: '[InsertFire] No LoadGlobal found for useEffect call', suggestions: null, }); @@ -179,6 +181,7 @@ function replaceFireFunctions(fn: HIRFunction, context: Context): void { description: 'You must use an array literal for an effect dependency array when that effect uses `fire()`', severity: ErrorSeverity.Invariant, + category: ErrorCategory.Fire, reason: CANNOT_COMPILE_FIRE, suggestions: null, }); @@ -189,6 +192,7 @@ function replaceFireFunctions(fn: HIRFunction, context: Context): void { description: 'You must use an array literal for an effect dependency array when that effect uses `fire()`', severity: ErrorSeverity.Invariant, + category: ErrorCategory.Fire, reason: CANNOT_COMPILE_FIRE, suggestions: null, }); @@ -223,6 +227,7 @@ function replaceFireFunctions(fn: HIRFunction, context: Context): void { loc: value.loc, description: null, severity: ErrorSeverity.Invariant, + category: ErrorCategory.Invariant, reason: '[InsertFire] No loadLocal found for fire call argument', suggestions: null, @@ -246,6 +251,7 @@ function replaceFireFunctions(fn: HIRFunction, context: Context): void { description: '`fire()` can only receive a function call such as `fire(fn(a,b)). Method calls and other expressions are not allowed', severity: ErrorSeverity.InvalidReact, + category: ErrorCategory.Fire, reason: CANNOT_COMPILE_FIRE, suggestions: null, }); @@ -264,6 +270,7 @@ function replaceFireFunctions(fn: HIRFunction, context: Context): void { loc: value.loc, description, severity: ErrorSeverity.InvalidReact, + category: ErrorCategory.Fire, reason: CANNOT_COMPILE_FIRE, suggestions: null, }); @@ -395,6 +402,7 @@ function ensureNoRemainingCalleeCaptures( this effect or not used with a fire() call at all. ${calleeName} was used with fire() on line \ ${printSourceLocationLine(calleeInfo.fireLoc)} in this effect`, severity: ErrorSeverity.InvalidReact, + category: ErrorCategory.Fire, reason: CANNOT_COMPILE_FIRE, suggestions: null, }); @@ -411,6 +419,7 @@ function ensureNoMoreFireUses(fn: HIRFunction, context: Context): void { context.pushError({ loc: place.identifier.loc, description: 'Cannot use `fire` outside of a useEffect function', + category: ErrorCategory.Fire, severity: ErrorSeverity.Invariant, reason: CANNOT_COMPILE_FIRE, suggestions: null, @@ -436,6 +445,7 @@ function makeLoadUseFireInstruction( value: instrValue, lvalue: {...useFirePlace}, loc: GeneratedSource, + effects: null, }; } @@ -460,6 +470,7 @@ function makeLoadFireCalleeInstruction( }, lvalue: {...loadedFireCallee}, loc: GeneratedSource, + effects: null, }; } @@ -483,6 +494,7 @@ function makeCallUseFireInstruction( value: useFireCall, lvalue: {...useFireCallResultPlace}, loc: GeneratedSource, + effects: null, }; } @@ -511,6 +523,7 @@ function makeStoreUseFireInstruction( }, lvalue: fireFunctionBindingLValuePlace, loc: GeneratedSource, + effects: null, }; } diff --git a/compiler/packages/babel-plugin-react-compiler/src/TypeInference/InferTypes.ts b/compiler/packages/babel-plugin-react-compiler/src/TypeInference/InferTypes.ts index 69812fc130ded..d3a297e2e51c2 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/TypeInference/InferTypes.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/TypeInference/InferTypes.ts @@ -14,6 +14,7 @@ import { Identifier, IdentifierId, Instruction, + InstructionKind, makePropertyLiteral, makeType, PropType, @@ -90,7 +91,8 @@ function apply(func: HIRFunction, unifier: Unifier): void { } } } - func.returnType = unifier.get(func.returnType); + const returns = func.returns.identifier; + returns.type = unifier.get(returns.type); } type TypeEquation = { @@ -143,12 +145,12 @@ function* generate( } } if (returnTypes.length > 1) { - yield equation(func.returnType, { + yield equation(func.returns.identifier.type, { kind: 'Phi', operands: returnTypes, }); } else if (returnTypes.length === 1) { - yield equation(func.returnType, returnTypes[0]!); + yield equation(func.returns.identifier.type, returnTypes[0]!); } } @@ -193,12 +195,29 @@ function* generateInstructionTypes( break; } - // We intentionally do not infer types for context variables + // We intentionally do not infer types for most context variables case 'DeclareContext': - case 'StoreContext': case 'LoadContext': { break; } + case 'StoreContext': { + /** + * The caveat is StoreContext const, where we know the value is + * assigned once such that everywhere the value is accessed, it + * must have the same type from the rvalue. + * + * A concrete example where this is useful is `const ref = useRef()` + * where the ref is referenced before its declaration in a function + * expression, causing it to be converted to a const context variable. + */ + if (value.lvalue.kind === InstructionKind.Const) { + yield equation( + value.lvalue.place.identifier.type, + value.value.identifier.type, + ); + } + break; + } case 'StoreLocal': { if (env.config.enableUseTypeAnnotations) { @@ -359,6 +378,12 @@ function* generateInstructionTypes( value: makePropertyLiteral(propertyName), }, }); + } else if (item.kind === 'Spread') { + // Array pattern spread always creates an array + yield equation(item.place.identifier.type, { + kind: 'Object', + shapeId: BuiltInArrayId, + }); } else { break; } @@ -407,7 +432,7 @@ function* generateInstructionTypes( yield equation(left, { kind: 'Function', shapeId: BuiltInFunctionId, - return: value.loweredFunc.func.returnType, + return: value.loweredFunc.func.returns.identifier.type, isConstructor: false, }); break; @@ -426,6 +451,18 @@ function* generateInstructionTypes( case 'JsxExpression': case 'JsxFragment': { + if (env.config.enableTreatRefLikeIdentifiersAsRefs) { + if (value.kind === 'JsxExpression') { + for (const prop of value.props) { + if (prop.kind === 'JsxAttribute' && prop.name === 'ref') { + yield equation(prop.place.identifier.type, { + kind: 'Object', + shapeId: BuiltInUseRefId, + }); + } + } + } + } yield equation(left, {kind: 'Object', shapeId: BuiltInJsxId}); break; } @@ -441,7 +478,36 @@ function* generateInstructionTypes( yield equation(left, returnType); break; } - case 'PropertyStore': + case 'PropertyStore': { + /** + * Infer types based on assignments to known object properties + * This is important for refs, where assignment to `.current` + * can help us infer that an object itself is a ref + */ + yield equation( + /** + * Our property type declarations are best-effort and we haven't tested + * using them to drive inference of rvalues from lvalues. We want to emit + * a Property type in order to infer refs from `.current` accesses, but + * stay conservative by not otherwise inferring anything about rvalues. + * So we use a dummy type here. + * + * TODO: consider using the rvalue type here + */ + makeType(), + // unify() only handles properties in the second position + { + kind: 'Property', + objectType: value.object.identifier.type, + objectName: getName(names, value.object.identifier.id), + propertyName: { + kind: 'literal', + value: value.property, + }, + }, + ); + break; + } case 'DeclareLocal': case 'RegExpLiteral': case 'MetaProperty': @@ -711,6 +777,15 @@ class Unifier { return {kind: 'Phi', operands: type.operands.map(o => this.get(o))}; } + if (type.kind === 'Function') { + return { + kind: 'Function', + isConstructor: type.isConstructor, + shapeId: type.shapeId, + return: this.get(type.return), + }; + } + return type; } } diff --git a/compiler/packages/babel-plugin-react-compiler/src/TypeInference/PropagatePhiTypes.ts b/compiler/packages/babel-plugin-react-compiler/src/TypeInference/PropagatePhiTypes.ts deleted file mode 100644 index 0369945525bcf..0000000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/TypeInference/PropagatePhiTypes.ts +++ /dev/null @@ -1,110 +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 {HIRFunction, IdentifierId, Type, typeEquals} from '../HIR'; - -/** - * Temporary workaround for InferTypes not propagating the types of phis. - * Previously, LeaveSSA would replace all the identifiers for each phi (operands and - * the phi itself) with a single "canonical" identifier, generally chosen as the first - * operand to flow into the phi. In case of a phi whose operand was a phi, this could - * sometimes be an operand from the earlier phi. - * - * As a result, even though InferTypes did not propagate types for phis, LeaveSSA - * could end up replacing the phi Identifier with another identifer from an operand, - * which _did_ have a type inferred. - * - * This didn't affect the initial construction of mutable ranges because InferMutableRanges - * runs before LeaveSSA - thus, the types propagated by LeaveSSA only affected later optimizations, - * notably MergeScopesThatInvalidateTogether which uses type to determine if a scope's output - * will always invalidate with its input. - * - * The long-term correct approach is to update InferTypes to infer the types of phis, - * but this is complicated because InferMutableRanges inadvertently depends on phis - * never having a known type, such that a Store effect cannot occur on a phi value. - * Once we fix InferTypes to infer phi types, then we'll also have to update InferMutableRanges - * to handle this case. - * - * As a temporary workaround, this pass propagates the type of phis and can be called - * safely *after* InferMutableRanges. Unlike LeaveSSA, this pass only propagates the - * type if all operands have the same type, it's its more correct. - */ -export function propagatePhiTypes(fn: HIRFunction): void { - /** - * We track which SSA ids have had their types propagated to handle nested ternaries, - * see the StoreLocal handling below - */ - const propagated = new Set(); - for (const [, block] of fn.body.blocks) { - for (const phi of block.phis) { - /* - * We replicate the previous LeaveSSA behavior and only propagate types for - * unnamed variables. LeaveSSA would have chosen one of the operands as the - * canonical id and taken its type as the type of all identifiers. We're - * more conservative and only propagate if the types are the same and the - * phi didn't have a type inferred. - * - * Note that this can change output slightly in cases such as - * `cond ?
: null`. - * - * Previously the first operand's type (BuiltInJsx) would have been propagated, - * and this expression may have been merged with subsequent reactive scopes - * since it appears (based on that type) to always invalidate. - * - * But the correct type is `BuiltInJsx | null`, which we can't express and - * so leave as a generic `Type`, which does not always invalidate and therefore - * does not merge with subsequent scopes. - * - * We also don't propagate scopes for named variables, to preserve compatibility - * with previous LeaveSSA behavior. - */ - if ( - phi.place.identifier.type.kind !== 'Type' || - phi.place.identifier.name !== null - ) { - continue; - } - let type: Type | null = null; - for (const [, operand] of phi.operands) { - if (type === null) { - type = operand.identifier.type; - } else if (!typeEquals(type, operand.identifier.type)) { - type = null; - break; - } - } - if (type !== null) { - phi.place.identifier.type = type; - propagated.add(phi.place.identifier.id); - } - } - for (const instr of block.instructions) { - const {value} = instr; - switch (value.kind) { - case 'StoreLocal': { - /** - * Nested ternaries can lower to a form with an intermediate StoreLocal where - * the value.lvalue is the temporary of the outer ternary, and the value.value - * is the result of the inner ternary. - * - * This is a common pattern in practice and easy enough to support. Again, the - * long-term approach is to update InferTypes and InferMutableRanges. - */ - const lvalue = value.lvalue.place; - if ( - propagated.has(value.value.identifier.id) && - lvalue.identifier.type.kind === 'Type' && - lvalue.identifier.name === null - ) { - lvalue.identifier.type = value.value.identifier.type; - propagated.add(lvalue.identifier.id); - } - } - } - } - } -} diff --git a/compiler/packages/babel-plugin-react-compiler/src/Utils/DisjointSet.ts b/compiler/packages/babel-plugin-react-compiler/src/Utils/DisjointSet.ts index 449064ef3280a..566732c2bb9fb 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Utils/DisjointSet.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Utils/DisjointSet.ts @@ -78,6 +78,10 @@ export default class DisjointSet { return root; } + has(item: T): boolean { + return this.#entries.has(item); + } + /* * Forces the set into canonical form, ie with all items pointing directly to * their root, and returns a Map representing the mapping of items to their roots. diff --git a/compiler/packages/babel-plugin-react-compiler/src/Utils/Keyword.ts b/compiler/packages/babel-plugin-react-compiler/src/Utils/Keyword.ts new file mode 100644 index 0000000000000..3964f4accc85f --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/Utils/Keyword.ts @@ -0,0 +1,87 @@ +/** + * 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. + */ + +/** + * https://tc39.es/ecma262/multipage/ecmascript-language-lexical-grammar.html#sec-keywords-and-reserved-words + */ + +/** + * Note: `await` and `yield` are contextually allowed as identifiers. + * await: reserved inside async functions and modules + * yield: reserved inside generator functions + * + * Note: `async` is not reserved. + */ +const RESERVED_WORDS = new Set([ + 'break', + 'case', + 'catch', + 'class', + 'const', + 'continue', + 'debugger', + 'default', + 'delete', + 'do', + 'else', + 'enum', + 'export', + 'extends', + 'false', + 'finally', + 'for', + 'function', + 'if', + 'import', + 'in', + 'instanceof', + 'new', + 'null', + 'return', + 'super', + 'switch', + 'this', + 'throw', + 'true', + 'try', + 'typeof', + 'var', + 'void', + 'while', + 'with', +]); + +/** + * Reserved when a module has a 'use strict' directive. + */ +const STRICT_MODE_RESERVED_WORDS = new Set([ + 'let', + 'static', + 'implements', + 'interface', + 'package', + 'private', + 'protected', + 'public', +]); +/** + * The names arguments and eval are not keywords, but they are subject to some restrictions in + * strict mode code. + */ +const STRICT_MODE_RESTRICTED_WORDS = new Set(['eval', 'arguments']); + +/** + * Conservative check for whether an identifer name is reserved or not. We assume that code is + * written with strict mode. + */ +export function isReservedWord(identifierName: string): boolean { + return ( + RESERVED_WORDS.has(identifierName) || + STRICT_MODE_RESERVED_WORDS.has(identifierName) || + STRICT_MODE_RESTRICTED_WORDS.has(identifierName) + ); +} 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 85fb3922d66a6..32e3ec82c5925 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Utils/Result.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Utils/Result.ts @@ -90,10 +90,13 @@ export function Ok(val: T): OkImpl { } class OkImpl implements Result { - constructor(private val: T) {} + #val: T; + constructor(val: T) { + this.#val = val; + } map(fn: (val: T) => U): Result { - return new OkImpl(fn(this.val)); + return new OkImpl(fn(this.#val)); } mapErr(_fn: (val: never) => F): Result { @@ -101,15 +104,15 @@ class OkImpl implements Result { } mapOr(_fallback: U, fn: (val: T) => U): U { - return fn(this.val); + return fn(this.#val); } mapOrElse(_fallback: () => U, fn: (val: T) => U): U { - return fn(this.val); + return fn(this.#val); } andThen(fn: (val: T) => Result): Result { - return fn(this.val); + return fn(this.#val); } and(res: Result): Result { @@ -133,30 +136,30 @@ class OkImpl implements Result { } expect(_msg: string): T { - return this.val; + return this.#val; } expectErr(msg: string): never { - throw new Error(`${msg}: ${this.val}`); + throw new Error(`${msg}: ${this.#val}`); } unwrap(): T { - return this.val; + return this.#val; } unwrapOr(_fallback: T): T { - return this.val; + return this.#val; } unwrapOrElse(_fallback: (val: never) => T): T { - return this.val; + return this.#val; } unwrapErr(): never { - if (this.val instanceof Error) { - throw this.val; + if (this.#val instanceof Error) { + throw this.#val; } - throw new Error(`Can't unwrap \`Ok\` to \`Err\`: ${this.val}`); + throw new Error(`Can't unwrap \`Ok\` to \`Err\`: ${this.#val}`); } } @@ -165,14 +168,17 @@ export function Err(val: E): ErrImpl { } class ErrImpl implements Result { - constructor(private val: E) {} + #val: E; + constructor(val: E) { + this.#val = val; + } map(_fn: (val: never) => U): Result { return this; } mapErr(fn: (val: E) => F): Result { - return new ErrImpl(fn(this.val)); + return new ErrImpl(fn(this.#val)); } mapOr(fallback: U, _fn: (val: never) => U): U { @@ -196,7 +202,7 @@ class ErrImpl implements Result { } orElse(fn: (val: E) => ErrImpl): Result { - return fn(this.val); + return fn(this.#val); } isOk(): this is OkImpl { @@ -208,18 +214,18 @@ class ErrImpl implements Result { } expect(msg: string): never { - throw new Error(`${msg}: ${this.val}`); + throw new Error(`${msg}: ${this.#val}`); } expectErr(_msg: string): E { - return this.val; + return this.#val; } unwrap(): never { - if (this.val instanceof Error) { - throw this.val; + if (this.#val instanceof Error) { + throw this.#val; } - throw new Error(`Can't unwrap \`Err\` to \`Ok\`: ${this.val}`); + throw new Error(`Can't unwrap \`Err\` to \`Ok\`: ${this.#val}`); } unwrapOr(fallback: T): T { @@ -227,10 +233,10 @@ class ErrImpl implements Result { } unwrapOrElse(fallback: (val: E) => T): T { - return fallback(this.val); + return fallback(this.#val); } unwrapErr(): E { - return this.val; + return this.#val; } } diff --git a/compiler/packages/babel-plugin-react-compiler/src/Utils/TestUtils.ts b/compiler/packages/babel-plugin-react-compiler/src/Utils/TestUtils.ts index b4484331dec80..c384165c31260 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Utils/TestUtils.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Utils/TestUtils.ts @@ -75,45 +75,57 @@ const testComplexConfigDefaults: PartialEnvironmentConfig = { source: 'react', importSpecifierName: 'useEffect', }, - numRequiredArgs: 1, + autodepsIndex: 1, }, { function: { source: 'shared-runtime', importSpecifierName: 'useSpecialEffect', }, - numRequiredArgs: 2, + autodepsIndex: 2, }, { function: { source: 'useEffectWrapper', importSpecifierName: 'default', }, - numRequiredArgs: 1, + autodepsIndex: 1, }, ], }; + +function* splitPragma( + pragma: string, +): Generator<{key: string; value: string | null}> { + for (const entry of pragma.split('@')) { + const keyVal = entry.trim(); + const valIdx = keyVal.indexOf(':'); + if (valIdx === -1) { + yield {key: keyVal.split(' ', 1)[0], value: null}; + } else { + yield {key: keyVal.slice(0, valIdx), value: keyVal.slice(valIdx + 1)}; + } + } +} + /** * For snap test fixtures and playground only. */ function parseConfigPragmaEnvironmentForTest( pragma: string, + defaultConfig: PartialEnvironmentConfig, ): EnvironmentConfig { - const maybeConfig: Partial> = {}; + // throw early if the defaults are invalid + EnvironmentConfigSchema.parse(defaultConfig); - for (const token of pragma.split(' ')) { - if (!token.startsWith('@')) { - continue; - } - const keyVal = token.slice(1); - const valIdx = keyVal.indexOf(':'); - const key = valIdx === -1 ? keyVal : keyVal.slice(0, valIdx); - const val = valIdx === -1 ? undefined : keyVal.slice(valIdx + 1); - const isSet = val === undefined || val === 'true'; + const maybeConfig: Partial> = + defaultConfig; + + for (const {key, value: val} of splitPragma(pragma)) { if (!hasOwnProperty(EnvironmentConfigSchema.shape, key)) { continue; } - + const isSet = val == null || val === 'true'; if (isSet && key in testComplexConfigDefaults) { maybeConfig[key] = testComplexConfigDefaults[key]; } else if (isSet) { @@ -167,27 +179,29 @@ export function parseConfigPragmaForTests( pragma: string, defaults: { compilationMode: CompilationMode; + environment?: PartialEnvironmentConfig; }, ): PluginOptions { - const environment = parseConfigPragmaEnvironmentForTest(pragma); + const overridePragma = parseConfigPragmaAsString(pragma); + if (overridePragma !== '') { + return parseConfigStringAsJS(overridePragma, defaults); + } + + const environment = parseConfigPragmaEnvironmentForTest( + pragma, + defaults.environment ?? {}, + ); const options: Record = { ...defaultOptions, panicThreshold: 'all_errors', compilationMode: defaults.compilationMode, environment, }; - for (const token of pragma.split(' ')) { - if (!token.startsWith('@')) { - continue; - } - const keyVal = token.slice(1); - const idx = keyVal.indexOf(':'); - const key = idx === -1 ? keyVal : keyVal.slice(0, idx); - const val = idx === -1 ? undefined : keyVal.slice(idx + 1); + for (const {key, value: val} of splitPragma(pragma)) { if (!hasOwnProperty(defaultOptions, key)) { continue; } - const isSet = val === undefined || val === 'true'; + const isSet = val == null || val === 'true'; if (isSet && key in testComplexPluginOptionDefaults) { options[key] = testComplexPluginOptionDefaults[key]; } else if (isSet) { @@ -208,3 +222,90 @@ export function parseConfigPragmaForTests( } return parsePluginOptions(options); } + +export function parseConfigPragmaAsString(pragma: string): string { + // Check if it's in JS override format + for (const {key, value: val} of splitPragma(pragma)) { + if (key === 'OVERRIDE' && val != null) { + return val; + } + } + return ''; +} + +function parseConfigStringAsJS( + configString: string, + defaults: { + compilationMode: CompilationMode; + environment?: PartialEnvironmentConfig; + }, +): PluginOptions { + let parsedConfig: any; + try { + // Parse the JavaScript object literal + parsedConfig = new Function(`return ${configString}`)(); + } catch (error) { + CompilerError.invariant(false, { + reason: 'Failed to parse config pragma as JavaScript object', + description: `Could not parse: ${configString}. Error: ${error}`, + loc: null, + suggestions: null, + }); + } + + console.log('OVERRIDE:', parsedConfig); + + const environment = parseConfigPragmaEnvironmentForTest( + '', + defaults.environment ?? {}, + ); + + const options: Record = { + ...defaultOptions, + panicThreshold: 'all_errors', + compilationMode: defaults.compilationMode, + environment, + }; + + // Apply parsed config, merging environment if it exists + if (parsedConfig.environment) { + const mergedEnvironment = { + ...(options.environment as Record), + ...parsedConfig.environment, + }; + + // Validate environment config + const validatedEnvironment = + EnvironmentConfigSchema.safeParse(mergedEnvironment); + if (!validatedEnvironment.success) { + CompilerError.invariant(false, { + reason: 'Invalid environment configuration in config pragma', + description: `${fromZodError(validatedEnvironment.error)}`, + loc: null, + suggestions: null, + }); + } + + options.environment = validatedEnvironment.data; + } + + // Apply other config options + for (const [key, value] of Object.entries(parsedConfig)) { + if (key === 'environment') { + continue; + } + + if (hasOwnProperty(defaultOptions, key)) { + if (key === 'target' && value === 'donotuse_meta_internal') { + options[key] = { + kind: value, + runtimeModule: 'react', + }; + } else { + options[key] = value; + } + } + } + + return parsePluginOptions(options); +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/Utils/utils.ts b/compiler/packages/babel-plugin-react-compiler/src/Utils/utils.ts index aa91c48b1b0db..897614015f544 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Utils/utils.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Utils/utils.ts @@ -33,12 +33,12 @@ export function assertExhaustive(_: never, errorMsg: string): never { // Modifies @param array in place, retaining only the items where the predicate returns true. export function retainWhere( array: Array, - predicate: (item: T) => boolean, + predicate: (item: T, index: number) => boolean, ): void { let writeIndex = 0; for (let readIndex = 0; readIndex < array.length; readIndex++) { const item = array[readIndex]; - if (predicate(item) === true) { + if (predicate(item, readIndex) === true) { array[writeIndex++] = item; } } @@ -121,6 +121,21 @@ export function Set_intersect(sets: Array>): Set { return result; } +/** + * @returns `true` if `a` is a superset of `b`. + */ +export function Set_isSuperset( + a: ReadonlySet, + b: ReadonlySet, +): boolean { + for (const v of b) { + if (!a.has(v)) { + return false; + } + } + return true; +} + export function Iterable_some( iter: Iterable, pred: (item: T) => boolean, diff --git a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateHooksUsage.ts b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateHooksUsage.ts index e90f33c74076c..af596155253cd 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateHooksUsage.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateHooksUsage.ts @@ -9,6 +9,7 @@ import * as t from '@babel/types'; import { CompilerError, CompilerErrorDetail, + ErrorCategory, ErrorSeverity, } from '../CompilerError'; import {computeUnconditionalBlocks} from '../HIR/ComputeUnconditionalBlocks'; @@ -124,6 +125,7 @@ export function validateHooksUsage( recordError( place.loc, new CompilerErrorDetail({ + category: ErrorCategory.Hooks, description: null, reason, loc: place.loc, @@ -140,6 +142,7 @@ export function validateHooksUsage( recordError( place.loc, new CompilerErrorDetail({ + category: ErrorCategory.Hooks, description: null, reason: 'Hooks may not be referenced as normal values, they must be called. See https://react.dev/reference/rules/react-calls-components-and-hooks#never-pass-around-hooks-as-regular-values', @@ -157,6 +160,7 @@ export function validateHooksUsage( recordError( place.loc, new CompilerErrorDetail({ + category: ErrorCategory.Hooks, description: null, reason: 'Hooks must be the same function on every render, but this value may change over time to a different function. See https://react.dev/reference/rules/react-calls-components-and-hooks#dont-dynamically-use-hooks', @@ -424,7 +428,7 @@ export function validateHooksUsage( } for (const [, error] of errorsByPlace) { - errors.push(error); + errors.pushErrorDetail(error); } return errors.asResult(); } @@ -448,11 +452,12 @@ function visitFunctionExpression(errors: CompilerError, fn: HIRFunction): void { if (hookKind != null) { errors.pushErrorDetail( new CompilerErrorDetail({ + category: ErrorCategory.Hooks, severity: ErrorSeverity.InvalidReact, reason: 'Hooks must be called at the top level in the body of a function component or custom hook, and may not be called within function expressions. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning)', loc: callee.loc, - description: `Cannot call ${hookKind} within a function component`, + description: `Cannot call ${hookKind === 'Custom' ? 'hook' : hookKind} within a function expression`, suggestions: null, }), ); diff --git a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateLocalsNotReassignedAfterRender.ts b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateLocalsNotReassignedAfterRender.ts index 9c41ebcae19f6..e1ed71049dff3 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateLocalsNotReassignedAfterRender.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateLocalsNotReassignedAfterRender.ts @@ -5,14 +5,15 @@ * LICENSE file in the root directory of this source tree. */ -import {CompilerError, Effect} from '..'; +import {CompilerDiagnostic, CompilerError, Effect, ErrorSeverity} from '..'; +import {ErrorCategory} from '../CompilerError'; import {HIRFunction, IdentifierId, Place} from '../HIR'; import { eachInstructionLValue, eachInstructionValueOperand, eachTerminalOperand, } from '../HIR/visitors'; -import {getFunctionCallSignature} from '../Inference/InferReferenceEffects'; +import {getFunctionCallSignature} from '../Inference/InferMutationAliasingEffects'; /** * Validates that local variables cannot be reassigned after render. @@ -28,16 +29,25 @@ export function validateLocalsNotReassignedAfterRender(fn: HIRFunction): void { false, ); if (reassignment !== null) { - CompilerError.throwInvalidReact({ - reason: - 'Reassigning a variable after render has completed can cause inconsistent behavior on subsequent renders. Consider using state instead', - description: - reassignment.identifier.name !== null && - reassignment.identifier.name.kind === 'named' - ? `Variable \`${reassignment.identifier.name.value}\` cannot be reassigned after render` - : '', - loc: reassignment.loc, - }); + const errors = new CompilerError(); + const variable = + reassignment.identifier.name != null && + reassignment.identifier.name.kind === 'named' + ? `\`${reassignment.identifier.name.value}\`` + : 'variable'; + errors.pushDiagnostic( + CompilerDiagnostic.create({ + category: ErrorCategory.Immutability, + severity: ErrorSeverity.InvalidReact, + reason: 'Cannot reassign variable after render completes', + description: `Reassigning ${variable} after render has completed can cause inconsistent behavior on subsequent renders. Consider using state instead.`, + }).withDetail({ + kind: 'error', + loc: reassignment.loc, + message: `Cannot reassign ${variable} after render completes`, + }), + ); + throw errors; } } @@ -75,16 +85,26 @@ function getContextReassignment( // if the function or its depends reassign, propagate that fact on the lvalue if (reassignment !== null) { if (isAsync || value.loweredFunc.func.async) { - CompilerError.throwInvalidReact({ - reason: - 'Reassigning a variable in an async function can cause inconsistent behavior on subsequent renders. Consider using state instead', - description: - reassignment.identifier.name !== null && - reassignment.identifier.name.kind === 'named' - ? `Variable \`${reassignment.identifier.name.value}\` cannot be reassigned after render` - : '', - loc: reassignment.loc, - }); + const errors = new CompilerError(); + const variable = + reassignment.identifier.name !== null && + reassignment.identifier.name.kind === 'named' + ? `\`${reassignment.identifier.name.value}\`` + : 'variable'; + errors.pushDiagnostic( + CompilerDiagnostic.create({ + category: ErrorCategory.Immutability, + severity: ErrorSeverity.InvalidReact, + reason: 'Cannot reassign variable in async function', + description: + 'Reassigning a variable in an async function can cause inconsistent behavior on subsequent renders. Consider using state instead', + }).withDetail({ + kind: 'error', + loc: reassignment.loc, + message: `Cannot reassign ${variable}`, + }), + ); + throw errors; } reassigningFunctions.set(lvalue.identifier.id, reassignment); } 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 b33cfb1512349..186641c3f20d4 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateMemoizedEffectDependencies.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateMemoizedEffectDependencies.ts @@ -6,6 +6,7 @@ */ import {CompilerError, ErrorSeverity} from '..'; +import {ErrorCategory} from '../CompilerError'; import { Identifier, Instruction, @@ -108,6 +109,7 @@ class Visitor extends ReactiveFunctionVisitor { isUnmemoized(deps.identifier, this.scopes)) ) { state.push({ + category: ErrorCategory.EffectDependencies, reason: 'React Compiler has skipped optimizing this component because the effect dependencies could not be memoized. Unmemoized effect dependencies can trigger an infinite loop or other unexpected behavior', description: null, diff --git a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoCapitalizedCalls.ts b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoCapitalizedCalls.ts index 8989cb1ac2d62..d0cf41a13c4fb 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoCapitalizedCalls.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoCapitalizedCalls.ts @@ -6,6 +6,7 @@ */ import {CompilerError, EnvironmentConfig, ErrorSeverity} from '..'; +import {ErrorCategory} from '../CompilerError'; import {HIRFunction, IdentifierId} from '../HIR'; import {DEFAULT_GLOBALS} from '../HIR/Globals'; import {Result} from '../Utils/Result'; @@ -56,6 +57,7 @@ export function validateNoCapitalizedCalls( const calleeName = capitalLoadGlobals.get(calleeIdentifier); if (calleeName != null) { CompilerError.throwInvalidReact({ + category: ErrorCategory.CapitalizedCalls, reason, description: `${calleeName} may be a component.`, loc: value.loc, @@ -79,6 +81,7 @@ export function validateNoCapitalizedCalls( const propertyName = capitalizedProperties.get(propertyIdentifier); if (propertyName != null) { errors.push({ + category: ErrorCategory.CapitalizedCalls, severity: ErrorSeverity.InvalidReact, reason, description: `${propertyName} may be a component.`, diff --git a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoDerivedComputationsInEffects.ts b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoDerivedComputationsInEffects.ts new file mode 100644 index 0000000000000..78174c656bcb2 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoDerivedComputationsInEffects.ts @@ -0,0 +1,502 @@ +/** + * 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 {TypeOf} from 'zod'; +import {CompilerError, Effect, ErrorSeverity, SourceLocation} from '..'; +import {ErrorCategory} from '../CompilerError'; +import { + ArrayExpression, + BlockId, + FunctionExpression, + HIRFunction, + IdentifierId, + InstructionValue, + Place, + isSetStateType, + isUseEffectHookType, +} from '../HIR'; +import {printInstruction, printPlace} from '../HIR/PrintHIR'; +import { + eachInstructionValueOperand, + eachInstructionOperand, + eachTerminalOperand, + eachInstructionLValue, +} from '../HIR/visitors'; +import {isMutable} from '../ReactiveScopes/InferReactiveScopeVariables'; +import {assertExhaustive} from '../Utils/utils'; + +type SetStateCall = { + loc: SourceLocation; + propsSources: Place[] | undefined; // undefined means state-derived, defined means props-derived +}; +type TypeOfValue = 'ignored' | 'fromProps' | 'fromState' | 'fromPropsOrState'; + +type DerivationMetadata = { + identifierPlace: Place; + sources: Place[]; + typeOfValue: TypeOfValue; +}; + +function joinValue( + lvalueType: TypeOfValue, + valueType: TypeOfValue, +): TypeOfValue { + if (lvalueType === 'ignored') return valueType; + if (valueType === 'ignored') return lvalueType; + if (lvalueType === valueType) return lvalueType; + return 'fromPropsOrState'; +} + +function propagateDerivation( + dest: Place, + source: Place | undefined, + derivedFromProps: Map, +) { + if (source === undefined) { + return; + } + + if (source.identifier.name?.kind === 'promoted') { + derivedFromProps.set(dest.identifier.id, dest); + } else { + derivedFromProps.set(dest.identifier.id, source); + } +} + +function updateDerivationMetadata( + target: Place, + sources: DerivationMetadata[], + typeOfValue: TypeOfValue, + derivedTuple: Map, +): void { + let newValue: DerivationMetadata = { + identifierPlace: target, + sources: [], + typeOfValue: typeOfValue, + }; + + for (const source of sources) { + // If the identifier of the source is a promoted identifier, then + // we should set the source as the first named identifier. + if (source.identifierPlace.identifier.name?.kind === 'promoted') { + newValue.sources.push(target); + } else { + newValue.sources.push(...source.sources); + } + } + derivedTuple.set(target.identifier.id, newValue); +} + +/** + * Validates that useEffect is not used for derived computations which could/should + * be performed in render. + * + * See https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state + * + * Example: + * + * ``` + * // 🔴 Avoid: redundant state and unnecessary Effect + * const [fullName, setFullName] = useState(''); + * useEffect(() => { + * setFullName(firstName + ' ' + lastName); + * }, [firstName, lastName]); + * ``` + * + * Instead use: + * + * ``` + * // ✅ Good: calculated during rendering + * const fullName = firstName + ' ' + lastName; + * ``` + */ +export function validateNoDerivedComputationsInEffects(fn: HIRFunction): void { + const candidateDependencies: Map = new Map(); + const functions: Map = new Map(); + const locals: Map = new Map(); + + // MY take on this + const valueToType: Map = new Map(); + const valueToSourceProps: Map> = new Map(); + const valueToSourceStates: Map> = new Map(); + const valueToSources: Map> = new Map(); + + // Sources are still probably not correct + const derivedTuple: Map = new Map(); + + const errors = new CompilerError(); + + if (fn.fnType === 'Hook') { + for (const param of fn.params) { + if (param.kind === 'Identifier') { + derivedTuple.set(param.identifier.id, { + identifierPlace: param, + sources: [param], + typeOfValue: 'fromProps', + }); + } + } + } else if (fn.fnType === 'Component') { + const props = fn.params[0]; + if (props != null && props.kind === 'Identifier') { + derivedTuple.set(props.identifier.id, { + identifierPlace: props, + sources: [props], + typeOfValue: 'fromProps', + }); + } + } + + for (const block of fn.body.blocks.values()) { + for (const phi of block.phis) { + for (const operand of phi.operands.values()) { + const source = derivedTuple.get(operand.identifier.id); + if (source !== undefined && source.typeOfValue === 'fromProps') { + if ( + source.identifierPlace.identifier.name === null || + source.identifierPlace.identifier.name?.kind === 'promoted' + ) { + derivedTuple.set(phi.place.identifier.id, { + identifierPlace: phi.place, + sources: [phi.place], + typeOfValue: 'fromProps', + }); + } else { + derivedTuple.set(phi.place.identifier.id, { + identifierPlace: phi.place, + sources: source.sources, + typeOfValue: 'fromProps', + }); + } + } + } + } + + for (const instr of block.instructions) { + const {lvalue, value} = instr; + + // This needs to be repeated "recursively" on FunctionExpressions + // HERE >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> + // DERIVATION LOGIC----------------------------------------------------- + console.log('instr', printInstruction(instr)); + console.log('instr', instr); + // console.log('instr lValue', instr.lvalue); + + let typeOfValue: TypeOfValue = 'ignored'; + + // TODO: Add handling for state derived props + let sources: DerivationMetadata[] = []; + for (const operand of eachInstructionValueOperand(value)) { + const opSource = derivedTuple.get(operand.identifier.id); + if (opSource === undefined) { + continue; + } + + typeOfValue = joinValue(typeOfValue, opSource.typeOfValue); + sources.push(opSource); + } + + // TODO: Add handling for state derived props + if (typeOfValue !== 'ignored') { + for (const lvalue of eachInstructionLValue(instr)) { + updateDerivationMetadata(lvalue, sources, typeOfValue, derivedTuple); + } + + for (const operand of eachInstructionValueOperand(value)) { + switch (operand.effect) { + case Effect.Capture: + case Effect.Store: + case Effect.ConditionallyMutate: + case Effect.ConditionallyMutateIterator: + case Effect.Mutate: { + if (isMutable(instr, operand)) { + updateDerivationMetadata( + operand, + sources, + typeOfValue, + derivedTuple, + ); + } + break; + } + case Effect.Freeze: + case Effect.Read: { + // no-op + break; + } + case Effect.Unknown: { + CompilerError.invariant(false, { + reason: 'Unexpected unknown effect', + description: null, + loc: operand.loc, + suggestions: null, + }); + } + default: { + assertExhaustive( + operand.effect, + `Unexpected effect kind \`${operand.effect}\``, + ); + } + } + } + } + console.log('derivedTuple', derivedTuple); + // HERE >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> + + // console.log('derivedTuple', derivedTuple); + // DERIVATION LOGIC----------------------------------------------------- + if (value.kind === 'LoadLocal') { + locals.set(lvalue.identifier.id, value.place.identifier.id); + } else if (value.kind === 'ArrayExpression') { + candidateDependencies.set(lvalue.identifier.id, value); + } else if (value.kind === 'FunctionExpression') { + functions.set(lvalue.identifier.id, value); + } else if ( + value.kind === 'CallExpression' || + value.kind === 'MethodCall' + ) { + const callee = + value.kind === 'CallExpression' ? value.callee : value.property; + + // This is a useEffect hook + if ( + isUseEffectHookType(callee.identifier) && + value.args.length === 2 && + value.args[0].kind === 'Identifier' && + value.args[1].kind === 'Identifier' + ) { + const effectFunction = functions.get(value.args[0].identifier.id); + const deps = candidateDependencies.get(value.args[1].identifier.id); + if ( + effectFunction != null && + deps != null && + deps.elements.length !== 0 && + deps.elements.every(element => element.kind === 'Identifier') + ) { + const dependencies: Array = deps.elements.map(dep => { + CompilerError.invariant(dep.kind === 'Identifier', { + reason: `Dependency is checked as a place above`, + loc: value.loc, + }); + return locals.get(dep.identifier.id) ?? dep.identifier.id; + }); + validateEffect( + effectFunction.loweredFunc.func, + dependencies, + derivedTuple, + errors, + ); + } + } + } + } + } + if (errors.hasErrors()) { + throw errors; + } +} + +function validateEffect( + effectFunction: HIRFunction, + effectDeps: Array, + derivedTuple: Map, + errors: CompilerError, +): void { + for (const operand of effectFunction.context) { + if (isSetStateType(operand.identifier)) { + continue; + } else if (effectDeps.find(dep => dep === operand.identifier.id) != null) { + continue; + } else if (derivedTuple.has(operand.identifier.id)) { + continue; + } else { + // Captured something other than the effect dep or setState + console.log('early return 1'); + return; + } + } + + // This might be wrong gotta double check + let hasInvalidDep = false; + for (const dep of effectDeps) { + const depMetadata = derivedTuple.get(dep); + if ( + effectFunction.context.find(operand => operand.identifier.id === dep) != + null || + (depMetadata !== undefined && depMetadata.typeOfValue !== 'ignored') + ) { + hasInvalidDep = true; + } + } + + if (!hasInvalidDep) { + console.log('early return 2'); + // effect dep wasn't actually used in the function + return; + } + + const seenBlocks: Set = new Set(); + // This variable is suspicious maybe we don't need it? + const values: Map> = new Map(); + const effectInvalidlyDerived: Map = new Map(); + + for (const dep of effectDeps) { + values.set(dep, [dep]); + const depMetadata = derivedTuple.get(dep); + if (depMetadata !== undefined) { + effectInvalidlyDerived.set(dep, depMetadata.sources); + } + } + + const setStateCalls: Array = []; + for (const block of effectFunction.body.blocks.values()) { + for (const pred of block.preds) { + if (!seenBlocks.has(pred)) { + // skip if block has a back edge + return; + } + } + + // TODO: This might need editing + for (const phi of block.phis) { + const aggregateDeps: Set = new Set(); + let propsSources: Place[] | null = null; + + for (const operand of phi.operands.values()) { + const deps = values.get(operand.identifier.id); + if (deps != null) { + for (const dep of deps) { + aggregateDeps.add(dep); + } + } + const sources = effectInvalidlyDerived.get(operand.identifier.id); + if (sources != null) { + propsSources = sources; + } + } + + if (aggregateDeps.size !== 0) { + values.set(phi.place.identifier.id, Array.from(aggregateDeps)); + } + if (propsSources != null) { + effectInvalidlyDerived.set(phi.place.identifier.id, propsSources); + } + } + + for (const instr of block.instructions) { + switch (instr.value.kind) { + case 'Primitive': + case 'JSXText': + case 'LoadGlobal': { + break; + } + case 'LoadLocal': { + const deps = values.get(instr.value.place.identifier.id); + if (deps != null) { + values.set(instr.lvalue.identifier.id, deps); + } + break; + } + case 'ComputedLoad': + case 'PropertyLoad': + case 'BinaryExpression': + case 'TemplateLiteral': + case 'CallExpression': + case 'MethodCall': { + const aggregateDeps: Set = new Set(); + for (const operand of eachInstructionOperand(instr)) { + const deps = values.get(operand.identifier.id); + if (deps != null) { + for (const dep of deps) { + aggregateDeps.add(dep); + } + } + } + if (aggregateDeps.size !== 0) { + values.set(instr.lvalue.identifier.id, Array.from(aggregateDeps)); + } + + if ( + instr.value.kind === 'CallExpression' && + isSetStateType(instr.value.callee.identifier) && + instr.value.args.length === 1 && + instr.value.args[0].kind === 'Identifier' + ) { + const deps = values.get(instr.value.args[0].identifier.id); + console.log('deps', deps); + if (deps != null && new Set(deps).size === effectDeps.length) { + // console.log('setState arg', instr.value.args[0].identifier.id); + // console.log('effectInvalidlyDerived', effectInvalidlyDerived); + // console.log('derivedTuple', derivedTuple); + const propSources = derivedTuple.get( + instr.value.args[0].identifier.id, + ); + + console.log('Final reference', propSources); + if (propSources !== undefined) { + setStateCalls.push({ + loc: instr.value.callee.loc, + propsSources: propSources.sources, + }); + } else { + setStateCalls.push({ + loc: instr.value.callee.loc, + propsSources: undefined, + }); + } + } else { + // doesn't depend on all deps + console.log('early return 3'); + return; + } + } + break; + } + default: { + console.log('early return 4'); + return; + } + } + } + for (const operand of eachTerminalOperand(block.terminal)) { + if (values.has(operand.identifier.id)) { + return; + } + } + seenBlocks.add(block.id); + } + + console.log('setStateCalls', setStateCalls); + for (const call of setStateCalls) { + if (call.propsSources != null) { + const propNames = call.propsSources + .map(place => place.identifier.name?.value) + .join(', '); + const propInfo = propNames != null ? ` (from props '${propNames}')` : ''; + + errors.push({ + reason: `Consider lifting state up to the parent component to make this a controlled component. (https://react.dev/learn/you-might-not-need-an-effect#adjusting-some-state-when-a-prop-changes)`, + description: `You are using props${propInfo} to update local state in an effect.`, + severity: ErrorSeverity.InvalidReact, + loc: call.loc, + suggestions: null, + }); + } else { + errors.push({ + reason: + 'You may not need this effect. Values derived from state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)', + description: + 'This effect updates state based on other state values. ' + + 'Consider calculating this value directly during render', + severity: ErrorSeverity.InvalidReact, + loc: call.loc, + suggestions: null, + }); + } + } +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoFreezingKnownMutableFunctions.ts b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoFreezingKnownMutableFunctions.ts index 81612a7441728..f49a9a0a47560 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoFreezingKnownMutableFunctions.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoFreezingKnownMutableFunctions.ts @@ -5,12 +5,11 @@ * LICENSE file in the root directory of this source tree. */ -import {CompilerError, Effect, ErrorSeverity} from '..'; +import {CompilerDiagnostic, CompilerError, Effect, ErrorSeverity} from '..'; +import {ErrorCategory} from '../CompilerError'; import { - FunctionEffect, HIRFunction, IdentifierId, - isMutableEffect, isRefOrRefLikeMutableType, Place, } from '../HIR'; @@ -18,8 +17,8 @@ import { eachInstructionValueOperand, eachTerminalOperand, } from '../HIR/visitors'; +import {AliasingEffect} from '../Inference/AliasingEffects'; import {Result} from '../Utils/Result'; -import {Iterable_some} from '../Utils/utils'; /** * Validates that functions with known mutations (ie due to types) cannot be passed @@ -50,24 +49,38 @@ export function validateNoFreezingKnownMutableFunctions( const errors = new CompilerError(); const contextMutationEffects: Map< IdentifierId, - Extract + Extract > = new Map(); function visitOperand(operand: Place): void { if (operand.effect === Effect.Freeze) { const effect = contextMutationEffects.get(operand.identifier.id); if (effect != null) { - errors.push({ - reason: `This argument is a function which modifies local variables when called, which can bypass memoization and cause the UI not to update`, - description: `Functions that are returned from hooks, passed as arguments to hooks, or passed as props to components may not mutate local variables`, - loc: operand.loc, - severity: ErrorSeverity.InvalidReact, - }); - errors.push({ - reason: `The function modifies a local variable here`, - loc: effect.loc, - severity: ErrorSeverity.InvalidReact, - }); + const place = effect.value; + const variable = + place != null && + place.identifier.name != null && + place.identifier.name.kind === 'named' + ? `\`${place.identifier.name.value}\`` + : 'a local variable'; + errors.pushDiagnostic( + CompilerDiagnostic.create({ + category: ErrorCategory.Immutability, + severity: ErrorSeverity.InvalidReact, + reason: 'Cannot modify local variables after render completes', + description: `This argument is a function which may reassign or mutate ${variable} after render, which can cause inconsistent behavior on subsequent renders. Consider using state instead.`, + }) + .withDetail({ + kind: 'error', + loc: operand.loc, + message: `This function may (indirectly) reassign or modify ${variable} after render`, + }) + .withDetail({ + kind: 'error', + loc: effect.value.loc, + message: `This modifies ${variable}`, + }), + ); } } } @@ -95,23 +108,47 @@ export function validateNoFreezingKnownMutableFunctions( break; } case 'FunctionExpression': { - const knownMutation = (value.loweredFunc.func.effects ?? []).find( - effect => { - return ( - effect.kind === 'ContextMutation' && - (effect.effect === Effect.Store || - effect.effect === Effect.Mutate) && - Iterable_some(effect.places, place => { - return ( - isMutableEffect(place.effect, place.loc) && - !isRefOrRefLikeMutableType(place.identifier.type) + if (value.loweredFunc.func.aliasingEffects != null) { + const context = new Set( + value.loweredFunc.func.context.map(p => p.identifier.id), + ); + effects: for (const effect of value.loweredFunc.func + .aliasingEffects) { + switch (effect.kind) { + case 'Mutate': + case 'MutateTransitive': { + const knownMutation = contextMutationEffects.get( + effect.value.identifier.id, + ); + if (knownMutation != null) { + contextMutationEffects.set( + lvalue.identifier.id, + knownMutation, + ); + } else if ( + context.has(effect.value.identifier.id) && + !isRefOrRefLikeMutableType(effect.value.identifier.type) + ) { + contextMutationEffects.set(lvalue.identifier.id, effect); + break effects; + } + break; + } + case 'MutateConditionally': + case 'MutateTransitiveConditionally': { + const knownMutation = contextMutationEffects.get( + effect.value.identifier.id, ); - }) - ); - }, - ); - if (knownMutation && knownMutation.kind === 'ContextMutation') { - contextMutationEffects.set(lvalue.identifier.id, knownMutation); + if (knownMutation != null) { + contextMutationEffects.set( + lvalue.identifier.id, + knownMutation, + ); + } + break; + } + } + } } break; } diff --git a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoImpureFunctionsInRender.ts b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoImpureFunctionsInRender.ts index 6e88773ecf0bd..868352e050e7e 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoImpureFunctionsInRender.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoImpureFunctionsInRender.ts @@ -5,9 +5,10 @@ * LICENSE file in the root directory of this source tree. */ -import {CompilerError, ErrorSeverity} from '..'; +import {CompilerDiagnostic, CompilerError, ErrorSeverity} from '..'; +import {ErrorCategory} from '../CompilerError'; import {HIRFunction} from '../HIR'; -import {getFunctionCallSignature} from '../Inference/InferReferenceEffects'; +import {getFunctionCallSignature} from '../Inference/InferMutationAliasingEffects'; import {Result} from '../Utils/Result'; /** @@ -34,17 +35,23 @@ export function validateNoImpureFunctionsInRender( callee.identifier.type, ); if (signature != null && signature.impure === true) { - errors.push({ - reason: - 'Calling an impure function can produce unstable results. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent)', - description: - signature.canonicalName != null - ? `\`${signature.canonicalName}\` is an impure function whose results may change on every call` - : null, - severity: ErrorSeverity.InvalidReact, - loc: callee.loc, - suggestions: null, - }); + errors.pushDiagnostic( + CompilerDiagnostic.create({ + category: ErrorCategory.Purity, + reason: 'Cannot call impure function during render', + description: + (signature.canonicalName != null + ? `\`${signature.canonicalName}\` is an impure function. ` + : '') + + 'Calling an impure function can produce unstable results that update unpredictably when the component happens to re-render. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent)', + severity: ErrorSeverity.InvalidReact, + suggestions: null, + }).withDetail({ + kind: 'error', + loc: callee.loc, + message: 'Cannot call impure function', + }), + ); } } } diff --git a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoJSXInTryStatement.ts b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoJSXInTryStatement.ts index 505302f7d12c2..158935f9e6ed9 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoJSXInTryStatement.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoJSXInTryStatement.ts @@ -5,7 +5,8 @@ * LICENSE file in the root directory of this source tree. */ -import {CompilerError, ErrorSeverity} from '..'; +import {CompilerDiagnostic, CompilerError, ErrorSeverity} from '..'; +import {ErrorCategory} from '../CompilerError'; import {BlockId, HIRFunction} from '../HIR'; import {Result} from '../Utils/Result'; import {retainWhere} from '../Utils/utils'; @@ -34,11 +35,18 @@ export function validateNoJSXInTryStatement( switch (value.kind) { case 'JsxExpression': case 'JsxFragment': { - errors.push({ - severity: ErrorSeverity.InvalidReact, - reason: `Unexpected JSX element within a try statement. To catch errors in rendering a given component, wrap that component in an error boundary. (https://react.dev/reference/react/Component#catching-rendering-errors-with-an-error-boundary)`, - loc: value.loc, - }); + errors.pushDiagnostic( + CompilerDiagnostic.create({ + category: ErrorCategory.ErrorBoundaries, + severity: ErrorSeverity.InvalidReact, + reason: 'Avoid constructing JSX within try/catch', + description: `React does not immediately render components when JSX is rendered, so any errors from this component will not be caught by the try/catch. To catch errors in rendering a given component, wrap that component in an error boundary. (https://react.dev/reference/react/Component#catching-rendering-errors-with-an-error-boundary)`, + }).withDetail({ + kind: 'error', + loc: value.loc, + message: 'Avoid constructing JSX within try/catch', + }), + ); break; } } diff --git a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoRefAccesInRender.ts b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoRefAccessInRender.ts similarity index 63% rename from compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoRefAccesInRender.ts rename to compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoRefAccessInRender.ts index d00302559bb4e..679d355e3215b 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoRefAccesInRender.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoRefAccessInRender.ts @@ -5,7 +5,12 @@ * LICENSE file in the root directory of this source tree. */ -import {CompilerError, ErrorSeverity} from '../CompilerError'; +import { + CompilerDiagnostic, + CompilerError, + ErrorCategory, + ErrorSeverity, +} from '../CompilerError'; import { BlockId, HIRFunction, @@ -23,6 +28,7 @@ import { eachTerminalOperand, } from '../HIR/visitors'; import {Err, Ok, Result} from '../Utils/Result'; +import {retainWhere} from '../Utils/utils'; /** * Validates that a function does not access a ref value during render. This includes a partial check @@ -75,8 +81,18 @@ type RefAccessRefType = type RefFnType = {readRefEffect: boolean; returnType: RefAccessType}; -class Env extends Map { +class Env { #changed = false; + #data: Map = new Map(); + #temporaries: Map = new Map(); + + lookup(place: Place): Place { + return this.#temporaries.get(place.identifier.id) ?? place; + } + + define(place: Place, value: Place): void { + this.#temporaries.set(place.identifier.id, value); + } resetChanged(): void { this.#changed = false; @@ -86,8 +102,14 @@ class Env extends Map { return this.#changed; } - override set(key: IdentifierId, value: RefAccessType): this { - const cur = this.get(key); + get(key: IdentifierId): RefAccessType | undefined { + const operandId = this.#temporaries.get(key)?.identifier.id ?? key; + return this.#data.get(operandId); + } + + set(key: IdentifierId, value: RefAccessType): this { + const operandId = this.#temporaries.get(key)?.identifier.id ?? key; + const cur = this.#data.get(operandId); const widenedValue = joinRefAccessTypes(value, cur ?? {kind: 'None'}); if ( !(cur == null && widenedValue.kind === 'None') && @@ -95,7 +117,8 @@ class Env extends Map { ) { this.#changed = true; } - return super.set(key, widenedValue); + this.#data.set(operandId, widenedValue); + return this; } } @@ -103,9 +126,48 @@ export function validateNoRefAccessInRender( fn: HIRFunction, ): Result { const env = new Env(); + collectTemporariesSidemap(fn, env); return validateNoRefAccessInRenderImpl(fn, env).map(_ => undefined); } +function collectTemporariesSidemap(fn: HIRFunction, env: Env): void { + for (const block of fn.body.blocks.values()) { + for (const instr of block.instructions) { + const {lvalue, value} = instr; + switch (value.kind) { + case 'LoadLocal': { + const temp = env.lookup(value.place); + if (temp != null) { + env.define(lvalue, temp); + } + break; + } + case 'StoreLocal': { + const temp = env.lookup(value.value); + if (temp != null) { + env.define(lvalue, temp); + env.define(value.lvalue.place, temp); + } + break; + } + case 'PropertyLoad': { + if ( + isUseRefType(value.object.identifier) && + value.property === 'current' + ) { + continue; + } + const temp = env.lookup(value.object); + if (temp != null) { + env.define(lvalue, temp); + } + break; + } + } + } + } +} + function refTypeOfType(place: Place): RefAccessType { if (isRefValueType(place.identifier)) { return {kind: 'RefValue'}; @@ -258,12 +320,27 @@ function validateNoRefAccessInRenderImpl( env.set(place.identifier.id, type); } + const interpolatedAsJsx = new Set(); + for (const block of fn.body.blocks.values()) { + for (const instr of block.instructions) { + const {value} = instr; + if (value.kind === 'JsxExpression' || value.kind === 'JsxFragment') { + if (value.children != null) { + for (const child of value.children) { + interpolatedAsJsx.add(child.identifier.id); + } + } + } + } + } + for (let i = 0; (i == 0 || env.hasChanged()) && i < 10; i++) { env.resetChanged(); returnValues = []; - const safeBlocks = new Map(); + const safeBlocks: Array<{block: BlockId; ref: RefId}> = []; const errors = new CompilerError(); for (const [, block] of fn.body.blocks) { + retainWhere(safeBlocks, entry => entry.block !== block.id); for (const phi of block.phis) { env.set( phi.place.identifier.id, @@ -385,28 +462,80 @@ function validateNoRefAccessInRenderImpl( const hookKind = getHookKindForType(fn.env, callee.identifier.type); let returnType: RefAccessType = {kind: 'None'}; const fnType = env.get(callee.identifier.id); + let didError = false; if (fnType?.kind === 'Structure' && fnType.fn !== null) { returnType = fnType.fn.returnType; if (fnType.fn.readRefEffect) { - errors.push({ - severity: ErrorSeverity.InvalidReact, - reason: - 'This function accesses a ref value (the `current` property), which may not be accessed during render. (https://react.dev/reference/react/useRef)', - loc: callee.loc, - description: - callee.identifier.name !== null && - callee.identifier.name.kind === 'named' - ? `Function \`${callee.identifier.name.value}\` accesses a ref` - : null, - suggestions: null, - }); + didError = true; + errors.pushDiagnostic( + CompilerDiagnostic.create({ + category: ErrorCategory.Refs, + severity: ErrorSeverity.InvalidReact, + reason: 'Cannot access refs during render', + description: ERROR_DESCRIPTION, + }).withDetail({ + kind: 'error', + loc: callee.loc, + message: `This function accesses a ref value`, + }), + ); } } - for (const operand of eachInstructionValueOperand(instr.value)) { - if (hookKind != null) { - validateNoDirectRefValueAccess(errors, operand, env); - } else { - validateNoRefAccess(errors, env, operand, operand.loc); + /* + * If we already reported an error on this instruction, don't report + * duplicate errors + */ + if (!didError) { + const isRefLValue = isUseRefType(instr.lvalue.identifier); + for (const operand of eachInstructionValueOperand(instr.value)) { + /** + * By default we check that function call operands are not refs, + * ref values, or functions that can access refs. + */ + if ( + isRefLValue || + (hookKind != null && + hookKind !== 'useState' && + hookKind !== 'useReducer') + ) { + /** + * Special cases: + * + * 1. the lvalue is a ref + * In general passing a ref to a function may access that ref + * value during render, so we disallow it. + * + * The main exception is the "mergeRefs" pattern, ie a function + * that accepts multiple refs as arguments (or an array of refs) + * and returns a new, aggregated ref. If the lvalue is a ref, + * we assume that the user is doing this pattern and allow passing + * refs. + * + * Eg `const mergedRef = mergeRefs(ref1, ref2)` + * + * 2. calling hooks + * + * Hooks are independently checked to ensure they don't access refs + * during render. + */ + validateNoDirectRefValueAccess(errors, operand, env); + } else if (interpolatedAsJsx.has(instr.lvalue.identifier.id)) { + /** + * Special case: the lvalue is passed as a jsx child + * + * For example `{renderHelper(ref)}`. Here we have more + * context and infer that the ref is being passed to a component-like + * render function which attempts to obey the rules. + */ + validateNoRefValueAccess(errors, env, operand); + } else { + validateNoRefPassedToFunction( + errors, + env, + operand, + operand.loc, + ); + } } } env.set(instr.lvalue.identifier.id, returnType); @@ -439,23 +568,39 @@ function validateNoRefAccessInRenderImpl( case 'PropertyStore': case 'ComputedDelete': case 'ComputedStore': { - const safe = safeBlocks.get(block.id); const target = env.get(instr.value.object.identifier.id); + let safe: (typeof safeBlocks)['0'] | null | undefined = null; if ( instr.value.kind === 'PropertyStore' && - safe != null && - target?.kind === 'Ref' && - target.refId === safe + target != null && + target.kind === 'Ref' ) { - safeBlocks.delete(block.id); + safe = safeBlocks.find(entry => entry.ref === target.refId); + } + if (safe != null) { + retainWhere(safeBlocks, entry => entry !== safe); } else { - validateNoRefAccess(errors, env, instr.value.object, instr.loc); + validateNoRefUpdate(errors, env, instr.value.object, instr.loc); } - for (const operand of eachInstructionValueOperand(instr.value)) { - if (operand === instr.value.object) { - continue; + if ( + instr.value.kind === 'ComputedDelete' || + instr.value.kind === 'ComputedStore' + ) { + validateNoRefValueAccess(errors, env, instr.value.property); + } + if ( + instr.value.kind === 'ComputedStore' || + instr.value.kind === 'PropertyStore' + ) { + validateNoDirectRefValueAccess(errors, instr.value.value, env); + const type = env.get(instr.value.value.identifier.id); + if (type != null && type.kind === 'Structure') { + let objectType: RefAccessType = type; + if (target != null) { + objectType = joinRefAccessTypes(objectType, target); + } + env.set(instr.value.object.identifier.id, objectType); } - validateNoRefValueAccess(errors, env, operand); } break; } @@ -535,8 +680,11 @@ function validateNoRefAccessInRenderImpl( if (block.terminal.kind === 'if') { const test = env.get(block.terminal.test.identifier.id); - if (test?.kind === 'Guard') { - safeBlocks.set(block.terminal.consequent, test.refId); + if ( + test?.kind === 'Guard' && + safeBlocks.find(entry => entry.ref === test.refId) == null + ) { + safeBlocks.push({block: block.terminal.fallthrough, ref: test.refId}); } } @@ -583,18 +731,18 @@ function destructure( function guardCheck(errors: CompilerError, operand: Place, env: Env): void { if (env.get(operand.identifier.id)?.kind === 'Guard') { - errors.push({ - severity: ErrorSeverity.InvalidReact, - reason: - 'Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)', - loc: operand.loc, - description: - operand.identifier.name !== null && - operand.identifier.name.kind === 'named' - ? `Cannot access ref value \`${operand.identifier.name.value}\`` - : null, - suggestions: null, - }); + errors.pushDiagnostic( + CompilerDiagnostic.create({ + category: ErrorCategory.Refs, + severity: ErrorSeverity.InvalidReact, + reason: 'Cannot access refs during render', + description: ERROR_DESCRIPTION, + }).withDetail({ + kind: 'error', + loc: operand.loc, + message: `Cannot access ref value during render`, + }), + ); } } @@ -608,22 +756,22 @@ function validateNoRefValueAccess( type?.kind === 'RefValue' || (type?.kind === 'Structure' && type.fn?.readRefEffect) ) { - errors.push({ - severity: ErrorSeverity.InvalidReact, - reason: - 'Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)', - loc: (type.kind === 'RefValue' && type.loc) || operand.loc, - description: - operand.identifier.name !== null && - operand.identifier.name.kind === 'named' - ? `Cannot access ref value \`${operand.identifier.name.value}\`` - : null, - suggestions: null, - }); + errors.pushDiagnostic( + CompilerDiagnostic.create({ + category: ErrorCategory.Refs, + severity: ErrorSeverity.InvalidReact, + reason: 'Cannot access refs during render', + description: ERROR_DESCRIPTION, + }).withDetail({ + kind: 'error', + loc: (type.kind === 'RefValue' && type.loc) || operand.loc, + message: `Cannot access ref value during render`, + }), + ); } } -function validateNoRefAccess( +function validateNoRefPassedToFunction( errors: CompilerError, env: Env, operand: Place, @@ -635,18 +783,41 @@ function validateNoRefAccess( type?.kind === 'RefValue' || (type?.kind === 'Structure' && type.fn?.readRefEffect) ) { - errors.push({ - severity: ErrorSeverity.InvalidReact, - reason: - 'Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)', - loc: (type.kind === 'RefValue' && type.loc) || loc, - description: - operand.identifier.name !== null && - operand.identifier.name.kind === 'named' - ? `Cannot access ref value \`${operand.identifier.name.value}\`` - : null, - suggestions: null, - }); + errors.pushDiagnostic( + CompilerDiagnostic.create({ + category: ErrorCategory.Refs, + severity: ErrorSeverity.InvalidReact, + reason: 'Cannot access refs during render', + description: ERROR_DESCRIPTION, + }).withDetail({ + kind: 'error', + loc: (type.kind === 'RefValue' && type.loc) || loc, + message: `Passing a ref to a function may read its value during render`, + }), + ); + } +} + +function validateNoRefUpdate( + errors: CompilerError, + env: Env, + operand: Place, + loc: SourceLocation, +): void { + const type = destructure(env.get(operand.identifier.id)); + if (type?.kind === 'Ref' || type?.kind === 'RefValue') { + errors.pushDiagnostic( + CompilerDiagnostic.create({ + category: ErrorCategory.Refs, + severity: ErrorSeverity.InvalidReact, + reason: 'Cannot access refs during render', + description: ERROR_DESCRIPTION, + }).withDetail({ + kind: 'error', + loc: (type.kind === 'RefValue' && type.loc) || loc, + message: `Cannot update ref during render`, + }), + ); } } @@ -657,17 +828,23 @@ function validateNoDirectRefValueAccess( ): void { const type = destructure(env.get(operand.identifier.id)); if (type?.kind === 'RefValue') { - errors.push({ - severity: ErrorSeverity.InvalidReact, - reason: - 'Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)', - loc: type.loc ?? operand.loc, - description: - operand.identifier.name !== null && - operand.identifier.name.kind === 'named' - ? `Cannot access ref value \`${operand.identifier.name.value}\`` - : null, - suggestions: null, - }); + errors.pushDiagnostic( + CompilerDiagnostic.create({ + category: ErrorCategory.Refs, + severity: ErrorSeverity.InvalidReact, + reason: 'Cannot access refs during render', + description: ERROR_DESCRIPTION, + }).withDetail({ + kind: 'error', + loc: type.loc ?? operand.loc, + message: `Cannot access ref value during render`, + }), + ); } } + +const ERROR_DESCRIPTION = + 'React refs are values that are not needed for rendering. Refs should only be accessed ' + + 'outside of render, such as in event handlers or effects. ' + + 'Accessing a ref value (the `current` property) during render can cause your component ' + + 'not to update as expected (https://react.dev/reference/react/useRef)'; diff --git a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoSetStateInPassiveEffects.ts b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoSetStateInEffects.ts similarity index 69% rename from compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoSetStateInPassiveEffects.ts rename to compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoSetStateInEffects.ts index a36c347faa001..4a24098a9fe7c 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoSetStateInPassiveEffects.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoSetStateInEffects.ts @@ -5,26 +5,33 @@ * LICENSE file in the root directory of this source tree. */ -import {CompilerError, ErrorSeverity} from '../CompilerError'; +import { + CompilerDiagnostic, + CompilerError, + ErrorCategory, + ErrorSeverity, +} from '../CompilerError'; import { HIRFunction, IdentifierId, isSetStateType, isUseEffectHookType, + isUseInsertionEffectHookType, + isUseLayoutEffectHookType, Place, } from '../HIR'; import {eachInstructionValueOperand} from '../HIR/visitors'; import {Result} from '../Utils/Result'; /** - * Validates against calling setState in the body of a *passive* effect (useEffect), + * Validates against calling setState in the body of an effect (useEffect and friends), * while allowing calling setState in callbacks scheduled by the effect. * * Calling setState during execution of a useEffect triggers a re-render, which is * often bad for performance and frequently has more efficient and straightforward * alternatives. See https://react.dev/learn/you-might-not-need-an-effect for examples. */ -export function validateNoSetStateInPassiveEffects( +export function validateNoSetStateInEffects( fn: HIRFunction, ): Result { const setStateFunctions: Map = new Map(); @@ -79,19 +86,36 @@ export function validateNoSetStateInPassiveEffects( instr.value.kind === 'MethodCall' ? instr.value.receiver : instr.value.callee; - if (isUseEffectHookType(callee.identifier)) { + if ( + isUseEffectHookType(callee.identifier) || + isUseLayoutEffectHookType(callee.identifier) || + isUseInsertionEffectHookType(callee.identifier) + ) { const arg = instr.value.args[0]; if (arg !== undefined && arg.kind === 'Identifier') { const setState = setStateFunctions.get(arg.identifier.id); if (setState !== undefined) { - errors.push({ - reason: - 'Calling setState directly within a useEffect causes cascading renders and is not recommended. Consider alternatives to useEffect. (https://react.dev/learn/you-might-not-need-an-effect)', - description: null, - severity: ErrorSeverity.InvalidReact, - loc: setState.loc, - suggestions: null, - }); + errors.pushDiagnostic( + CompilerDiagnostic.create({ + category: ErrorCategory.EffectSetState, + reason: + 'Calling setState synchronously within an effect can trigger cascading renders', + description: + 'Effects are intended to synchronize state between React and external systems such as manually updating the DOM, state management libraries, or other platform APIs. ' + + 'In general, the body of an effect should do one or both of the following:\n' + + '* Update external systems with the latest state from React.\n' + + '* Subscribe for updates from some external system, calling setState in a callback function when external state changes.\n\n' + + 'Calling setState synchronously within an effect body causes cascading renders that can hurt performance, and is not recommended. ' + + '(https://react.dev/learn/you-might-not-need-an-effect)', + severity: ErrorSeverity.InvalidReact, + suggestions: null, + }).withDetail({ + kind: 'error', + loc: setState.loc, + message: + 'Avoid calling setState() directly within an effect', + }), + ); } } } diff --git a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoSetStateInRender.ts b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoSetStateInRender.ts index fc101581b30b0..2ee9ab64b7ee4 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoSetStateInRender.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoSetStateInRender.ts @@ -5,7 +5,12 @@ * LICENSE file in the root directory of this source tree. */ -import {CompilerError, ErrorSeverity} from '../CompilerError'; +import { + CompilerDiagnostic, + CompilerError, + ErrorCategory, + ErrorSeverity, +} from '../CompilerError'; import {HIRFunction, IdentifierId, isSetStateType} from '../HIR'; import {computeUnconditionalBlocks} from '../HIR/ComputeUnconditionalBlocks'; import {eachInstructionValueOperand} from '../HIR/visitors'; @@ -122,23 +127,37 @@ function validateNoSetStateInRenderImpl( unconditionalSetStateFunctions.has(callee.identifier.id) ) { if (activeManualMemoId !== null) { - errors.push({ - reason: - 'Calling setState from useMemo may trigger an infinite loop. (https://react.dev/reference/react/useState)', - description: null, - severity: ErrorSeverity.InvalidReact, - loc: callee.loc, - suggestions: null, - }); + errors.pushDiagnostic( + CompilerDiagnostic.create({ + category: ErrorCategory.RenderSetState, + reason: + 'Calling setState from useMemo may trigger an infinite loop', + description: + 'Each time the memo callback is evaluated it will change state. This can cause a memoization dependency to change, running the memo function again and causing an infinite loop. Instead of setting state in useMemo(), prefer deriving the value during render. (https://react.dev/reference/react/useState)', + severity: ErrorSeverity.InvalidReact, + suggestions: null, + }).withDetail({ + kind: 'error', + loc: callee.loc, + message: 'Found setState() within useMemo()', + }), + ); } else if (unconditionalBlocks.has(block.id)) { - errors.push({ - reason: - 'This is an unconditional set state during render, which will trigger an infinite loop. (https://react.dev/reference/react/useState)', - description: null, - severity: ErrorSeverity.InvalidReact, - loc: callee.loc, - suggestions: null, - }); + errors.pushDiagnostic( + CompilerDiagnostic.create({ + category: ErrorCategory.RenderSetState, + reason: + 'Calling setState during render may trigger an infinite loop', + description: + 'Calling setState during render will trigger another render, and can lead to infinite loops. (https://react.dev/reference/react/useState)', + severity: ErrorSeverity.InvalidReact, + suggestions: null, + }).withDetail({ + kind: 'error', + loc: callee.loc, + message: 'Found setState() in render', + }), + ); } } break; 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 1829d77822998..624cf382b7d49 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidatePreservedManualMemoization.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidatePreservedManualMemoization.ts @@ -5,7 +5,12 @@ * LICENSE file in the root directory of this source tree. */ -import {CompilerError, ErrorSeverity} from '../CompilerError'; +import { + CompilerDiagnostic, + CompilerError, + ErrorCategory, + ErrorSeverity, +} from '../CompilerError'; import { DeclarationId, Effect, @@ -275,27 +280,37 @@ function validateInferredDep( errorDiagnostic = merge(errorDiagnostic ?? compareResult, compareResult); } } - errorState.push({ - severity: ErrorSeverity.CannotPreserveMemoization, - reason: - 'React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected', - description: - DEBUG || - // If the dependency is a named variable then we can report it. Otherwise only print in debug mode - (dep.identifier.name != null && dep.identifier.name.kind === 'named') - ? `The inferred dependency was \`${prettyPrintScopeDependency( - dep, - )}\`, but the source dependencies were [${validDepsInMemoBlock - .map(dep => printManualMemoDependency(dep, true)) - .join(', ')}]. ${ - errorDiagnostic - ? getCompareDependencyResultDescription(errorDiagnostic) - : 'Inferred dependency not present in source' - }` - : null, - loc: memoLocation, - suggestions: null, - }); + errorState.pushDiagnostic( + CompilerDiagnostic.create({ + category: ErrorCategory.PreserveManualMemo, + severity: ErrorSeverity.CannotPreserveMemoization, + reason: 'Existing memoization could not be preserved', + description: [ + 'React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. ', + 'The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected. ', + DEBUG || + // If the dependency is a named variable then we can report it. Otherwise only print in debug mode + (dep.identifier.name != null && dep.identifier.name.kind === 'named') + ? `The inferred dependency was \`${prettyPrintScopeDependency( + dep, + )}\`, but the source dependencies were [${validDepsInMemoBlock + .map(dep => printManualMemoDependency(dep, true)) + .join(', ')}]. ${ + errorDiagnostic + ? getCompareDependencyResultDescription(errorDiagnostic) + : 'Inferred dependency not present in source' + }.` + : '', + ] + .join('') + .trim(), + suggestions: null, + }).withDetail({ + kind: 'error', + loc: memoLocation, + message: 'Could not preserve existing manual memoization', + }), + ); } class Visitor extends ReactiveFunctionVisitor { @@ -445,11 +460,13 @@ class Visitor extends ReactiveFunctionVisitor { */ this.recordTemporaries(instruction, state); const value = instruction.value; + // Track reassignments from inlining of manual memo if ( value.kind === 'StoreLocal' && value.lvalue.kind === 'Reassign' && state.manualMemoState != null ) { + // Complex cases of inlining end up with a temporary that is reassigned const ids = getOrInsertDefault( state.manualMemoState.reassignments, value.lvalue.place.identifier.declarationId, @@ -457,6 +474,21 @@ class Visitor extends ReactiveFunctionVisitor { ); ids.add(value.value.identifier); } + if ( + value.kind === 'LoadLocal' && + value.place.identifier.scope != null && + instruction.lvalue != null && + instruction.lvalue.identifier.scope == null && + state.manualMemoState != null + ) { + // Simpler cases of inlining assign to the original IIFE lvalue + const ids = getOrInsertDefault( + state.manualMemoState.reassignments, + instruction.lvalue.identifier.declarationId, + new Set(), + ); + ids.add(value.place.identifier); + } if (value.kind === 'StartMemoize') { let depsFromSource: Array | null = null; if (value.deps != null) { @@ -502,14 +534,21 @@ class Visitor extends ReactiveFunctionVisitor { !this.scopes.has(identifier.scope.id) && !this.prunedScopes.has(identifier.scope.id) ) { - state.errors.push({ - reason: - 'React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. This dependency may be mutated later, which could cause the value to change unexpectedly', - description: null, - severity: ErrorSeverity.CannotPreserveMemoization, - loc, - suggestions: null, - }); + state.errors.pushDiagnostic( + CompilerDiagnostic.create({ + category: ErrorCategory.PreserveManualMemo, + severity: ErrorSeverity.CannotPreserveMemoization, + reason: 'Existing memoization could not be preserved', + description: [ + 'React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. ', + 'This dependency may be mutated later, which could cause the value to change unexpectedly.', + ].join(''), + }).withDetail({ + kind: 'error', + loc, + message: 'This dependency may be modified later', + }), + ); } } } @@ -543,16 +582,25 @@ class Visitor extends ReactiveFunctionVisitor { for (const identifier of decls) { if (isUnmemoized(identifier, this.scopes)) { - state.errors.push({ - reason: - 'React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. This value was memoized in source but not in compilation output.', - description: DEBUG - ? `${printIdentifier(identifier)} was not memoized` - : null, - severity: ErrorSeverity.CannotPreserveMemoization, - loc, - suggestions: null, - }); + state.errors.pushDiagnostic( + CompilerDiagnostic.create({ + category: ErrorCategory.PreserveManualMemo, + severity: ErrorSeverity.CannotPreserveMemoization, + reason: 'Existing memoization could not be preserved', + description: [ + 'React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. This value was memoized in source but not in compilation output. ', + DEBUG + ? `${printIdentifier(identifier)} was not memoized.` + : '', + ] + .join('') + .trim(), + }).withDetail({ + kind: 'error', + loc, + message: 'Could not preserve existing memoization', + }), + ); } } } diff --git a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateStaticComponents.ts b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateStaticComponents.ts index f7adef6ca7128..375b8e8f0116f 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateStaticComponents.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateStaticComponents.ts @@ -5,7 +5,12 @@ * LICENSE file in the root directory of this source tree. */ -import {CompilerError, ErrorSeverity} from '../CompilerError'; +import { + CompilerDiagnostic, + CompilerError, + ErrorCategory, + ErrorSeverity, +} from '../CompilerError'; import {HIRFunction, IdentifierId, SourceLocation} from '../HIR'; import {Result} from '../Utils/Result'; @@ -59,20 +64,24 @@ export function validateStaticComponents( value.tag.identifier.id, ); if (location != null) { - error.push({ - reason: `Components created during render will reset their state each time they are created. Declare components outside of render. `, - severity: ErrorSeverity.InvalidReact, - loc: value.tag.loc, - description: null, - suggestions: null, - }); - error.push({ - reason: `The component may be created during render`, - severity: ErrorSeverity.InvalidReact, - loc: location, - description: null, - suggestions: null, - }); + error.pushDiagnostic( + CompilerDiagnostic.create({ + category: ErrorCategory.StaticComponents, + severity: ErrorSeverity.InvalidReact, + reason: 'Cannot create components during render', + description: `Components created during render will reset their state each time they are created. Declare components outside of render. `, + }) + .withDetail({ + kind: 'error', + loc: value.tag.loc, + message: 'This component is created during render', + }) + .withDetail({ + kind: 'error', + loc: location, + message: 'The component is created during render here', + }), + ); } } } diff --git a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateUseMemo.ts b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateUseMemo.ts index fd4d781ef3c6f..3146bbea38a61 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateUseMemo.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateUseMemo.ts @@ -5,7 +5,12 @@ * LICENSE file in the root directory of this source tree. */ -import {CompilerError, ErrorSeverity} from '..'; +import { + CompilerDiagnostic, + CompilerError, + ErrorCategory, + ErrorSeverity, +} from '../CompilerError'; import {FunctionExpression, HIRFunction, IdentifierId} from '../HIR'; import {Result} from '../Utils/Result'; @@ -63,24 +68,43 @@ export function validateUseMemo(fn: HIRFunction): Result { } if (body.loweredFunc.func.params.length > 0) { - errors.push({ - severity: ErrorSeverity.InvalidReact, - reason: 'useMemo callbacks may not accept any arguments', - description: null, - loc: body.loc, - suggestions: null, - }); + const firstParam = body.loweredFunc.func.params[0]; + const loc = + firstParam.kind === 'Identifier' + ? firstParam.loc + : firstParam.place.loc; + errors.pushDiagnostic( + CompilerDiagnostic.create({ + category: ErrorCategory.UseMemo, + severity: ErrorSeverity.InvalidReact, + reason: 'useMemo() callbacks may not accept parameters', + description: + 'useMemo() callbacks are called by React to cache calculations across re-renders. They should not take parameters. Instead, directly reference the props, state, or local variables needed for the computation.', + suggestions: null, + }).withDetail({ + kind: 'error', + loc, + message: 'Callbacks with parameters are not supported', + }), + ); } if (body.loweredFunc.func.async || body.loweredFunc.func.generator) { - errors.push({ - severity: ErrorSeverity.InvalidReact, - reason: - 'useMemo callbacks may not be async or generator functions', - description: null, - loc: body.loc, - suggestions: null, - }); + errors.pushDiagnostic( + CompilerDiagnostic.create({ + category: ErrorCategory.UseMemo, + severity: ErrorSeverity.InvalidReact, + reason: + 'useMemo() callbacks may not be async or generator functions', + description: + 'useMemo() callbacks are called once and must synchronously return a value.', + suggestions: null, + }).withDetail({ + kind: 'error', + loc: body.loc, + message: 'Async and generator functions are not supported', + }), + ); } break; diff --git a/compiler/packages/babel-plugin-react-compiler/src/Validation/index.ts b/compiler/packages/babel-plugin-react-compiler/src/Validation/index.ts index 92d53cbd421a0..3bf03f362fa19 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Validation/index.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Validation/index.ts @@ -9,7 +9,7 @@ export {validateContextVariableLValues} from './ValidateContextVariableLValues'; export {validateHooksUsage} from './ValidateHooksUsage'; export {validateMemoizedEffectDependencies} from './ValidateMemoizedEffectDependencies'; export {validateNoCapitalizedCalls} from './ValidateNoCapitalizedCalls'; -export {validateNoRefAccessInRender} from './ValidateNoRefAccesInRender'; +export {validateNoRefAccessInRender} from './ValidateNoRefAccessInRender'; export {validateNoSetStateInRender} from './ValidateNoSetStateInRender'; export {validatePreservedManualMemoization} from './ValidatePreservedManualMemoization'; export {validateUseMemo} from './ValidateUseMemo'; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/Logger-test.ts b/compiler/packages/babel-plugin-react-compiler/src/__tests__/Logger-test.ts index 4b41068f4eb71..096b723554f01 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/Logger-test.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/Logger-test.ts @@ -58,7 +58,8 @@ it('logs failed compilation', () => { expect(event.detail.severity).toEqual('InvalidReact'); //@ts-ignore - const {start, end, identifierName} = event.detail.loc as t.SourceLocation; + const {start, end, identifierName} = + event.detail.primaryLocation() as t.SourceLocation; expect(start).toEqual({column: 28, index: 28, line: 1}); expect(end).toEqual({column: 33, index: 33, line: 1}); expect(identifierName).toEqual('props'); diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/envConfig-test.ts b/compiler/packages/babel-plugin-react-compiler/src/__tests__/envConfig-test.ts index a96af5b3918be..f8a6330977fe8 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/envConfig-test.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/envConfig-test.ts @@ -20,7 +20,7 @@ describe('parseConfigPragma()', () => { validateHooksUsage: 1, } as any); }).toThrowErrorMatchingInlineSnapshot( - `"InvalidConfig: Could not validate environment config. Update React Compiler config to fix the error. Validation error: Expected boolean, received number at "validateHooksUsage""`, + `"Error: Could not validate environment config. Update React Compiler config to fix the error. Validation error: Expected boolean, received number at "validateHooksUsage"."`, ); }); @@ -33,12 +33,12 @@ describe('parseConfigPragma()', () => { source: 'react', importSpecifierName: 'useEffect', }, - numRequiredArgs: 0, + autodepsIndex: 0, }, ], } as any); }).toThrowErrorMatchingInlineSnapshot( - `"InvalidConfig: Could not validate environment config. Update React Compiler config to fix the error. Validation error: numRequiredArgs must be > 0 at "inferEffectDependencies[0].numRequiredArgs""`, + `"Error: Could not validate environment config. Update React Compiler config to fix the error. Validation error: autodepsIndex must be > 0 at "inferEffectDependencies[0].autodepsIndex"."`, ); }); diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/aliased-nested-scope-truncated-dep.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/aliased-nested-scope-truncated-dep.expect.md index 933fafff5f1ba..12c7b4d5eab93 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/aliased-nested-scope-truncated-dep.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/aliased-nested-scope-truncated-dep.expect.md @@ -175,21 +175,14 @@ import { * and mutability. */ function Component(t0) { - const $ = _c(4); + const $ = _c(2); const { prop } = t0; let t1; if ($[0] !== prop) { const obj = shallowCopy(prop); const aliasedObj = identity(obj); - let t2; - if ($[2] !== obj) { - t2 = [obj.id]; - $[2] = obj; - $[3] = t2; - } else { - t2 = $[3]; - } - const id = t2; + + const id = [obj.id]; mutate(aliasedObj); setPropertyByKey(aliasedObj, "id", prop.id + 1); diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/align-scopes-iife-return-modified-later-logical.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/align-scopes-iife-return-modified-later-logical.expect.md index 8d7b62fe8340f..62ea047e2541f 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/align-scopes-iife-return-modified-later-logical.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/align-scopes-iife-return-modified-later-logical.expect.md @@ -26,20 +26,16 @@ import { c as _c } from "react/compiler-runtime"; import { getNull } from "shared-runtime"; function Component(props) { - const $ = _c(3); - let t0; + const $ = _c(2); let items; if ($[0] !== props.a) { - t0 = getNull() ?? []; - items = t0; + items = getNull() ?? []; items.push(props.a); $[0] = props.a; $[1] = items; - $[2] = t0; } else { items = $[1]; - t0 = $[2]; } return items; } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/align-scopes-reactive-scope-overlaps-if.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/align-scopes-reactive-scope-overlaps-if.expect.md index 7136b3a173f61..03939d16d66e0 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/align-scopes-reactive-scope-overlaps-if.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/align-scopes-reactive-scope-overlaps-if.expect.md @@ -46,14 +46,16 @@ function useFoo(t0) { t1 = $[0]; } let items = t1; - bb0: if ($[1] !== cond) { - if (cond) { - items = []; - } else { - break bb0; - } + if ($[1] !== cond) { + bb0: { + if (cond) { + items = []; + } else { + break bb0; + } - items.push(2); + items.push(2); + } $[1] = cond; $[2] = items; } else { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-assigning-ref-accessing-function-to-object-property-if-not-mutated.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-assigning-ref-accessing-function-to-object-property-if-not-mutated.expect.md new file mode 100644 index 0000000000000..b5fc0a9dc7563 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-assigning-ref-accessing-function-to-object-property-if-not-mutated.expect.md @@ -0,0 +1,52 @@ + +## Input + +```javascript +import {useRef} from 'react'; +import {Stringify} from 'shared-runtime'; + +function Component(props) { + const ref = useRef(props.value); + const object = {}; + object.foo = () => ref.current; + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{value: 42}], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; +import { useRef } from "react"; +import { Stringify } from "shared-runtime"; + +function Component(props) { + const $ = _c(1); + const ref = useRef(props.value); + let t0; + if ($[0] === Symbol.for("react.memo_cache_sentinel")) { + const object = {}; + object.foo = () => ref.current; + t0 = ; + $[0] = t0; + } else { + t0 = $[0]; + } + return t0; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ value: 42 }], +}; + +``` + +### Eval output +(kind: ok)
{"object":{"foo":{"kind":"Function","result":42}},"shouldInvokeFns":true}
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-assigning-ref-accessing-function-to-object-property-if-not-mutated.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-assigning-ref-accessing-function-to-object-property-if-not-mutated.js new file mode 100644 index 0000000000000..2c84772dca06f --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-assigning-ref-accessing-function-to-object-property-if-not-mutated.js @@ -0,0 +1,14 @@ +import {useRef} from 'react'; +import {Stringify} from 'shared-runtime'; + +function Component(props) { + const ref = useRef(props.value); + const object = {}; + object.foo = () => ref.current; + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{value: 42}], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-assigning-to-global-in-function-spread-as-jsx.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-assigning-to-global-in-function-spread-as-jsx.expect.md new file mode 100644 index 0000000000000..90ffebd69583f --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-assigning-to-global-in-function-spread-as-jsx.expect.md @@ -0,0 +1,39 @@ + +## Input + +```javascript +// @enableNewMutationAliasingModel:false +function Component() { + const foo = () => { + someGlobal = true; + }; + // spreading a function is weird, but it doesn't call the function so this is allowed + return
; +} + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel:false +function Component() { + const $ = _c(1); + const foo = _temp; + let t0; + if ($[0] === Symbol.for("react.memo_cache_sentinel")) { + t0 =
; + $[0] = t0; + } else { + t0 = $[0]; + } + return t0; +} +function _temp() { + someGlobal = true; +} + +``` + +### 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/allow-assigning-to-global-in-function-spread-as-jsx.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-assigning-to-global-in-function-spread-as-jsx.js new file mode 100644 index 0000000000000..2ed0c70a7f96e --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-assigning-to-global-in-function-spread-as-jsx.js @@ -0,0 +1,8 @@ +// @enableNewMutationAliasingModel:false +function Component() { + const foo = () => { + someGlobal = true; + }; + // spreading a function is weird, but it doesn't call the function so this is allowed + return
; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-merge-refs-pattern.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-merge-refs-pattern.expect.md new file mode 100644 index 0000000000000..6933edef4678e --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-merge-refs-pattern.expect.md @@ -0,0 +1,44 @@ + +## Input + +```javascript +// @enableTreatRefLikeIdentifiersAsRefs @validateRefAccessDuringRender + +import {useRef} from 'react'; + +function Component() { + const ref = useRef(null); + const ref2 = useRef(null); + const mergedRef = mergeRefs([ref], ref2); + + return ; +} + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; // @enableTreatRefLikeIdentifiersAsRefs @validateRefAccessDuringRender + +import { useRef } from "react"; + +function Component() { + const $ = _c(1); + const ref = useRef(null); + const ref2 = useRef(null); + const mergedRef = mergeRefs([ref], ref2); + let t0; + if ($[0] === Symbol.for("react.memo_cache_sentinel")) { + t0 = ; + $[0] = t0; + } else { + t0 = $[0]; + } + return t0; +} + +``` + +### 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/allow-merge-refs-pattern.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-merge-refs-pattern.js new file mode 100644 index 0000000000000..91c5f0828490d --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-merge-refs-pattern.js @@ -0,0 +1,11 @@ +// @enableTreatRefLikeIdentifiersAsRefs @validateRefAccessDuringRender + +import {useRef} from 'react'; + +function Component() { + const ref = useRef(null); + const ref2 = useRef(null); + const mergedRef = mergeRefs([ref], ref2); + + return ; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-modify-global-in-callback-jsx.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-modify-global-in-callback-jsx.expect.md index a8d767831a9fe..44d4974f6f88d 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-modify-global-in-callback-jsx.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-modify-global-in-callback-jsx.expect.md @@ -52,15 +52,13 @@ function Component(t0) { } const onClick = t1; let t2; - let t3; if ($[2] !== onClick) { - t3 =
{someGlobal.value}
; + t2 =
{someGlobal.value}
; $[2] = onClick; - $[3] = t3; + $[3] = t2; } else { - t3 = $[3]; + t2 = $[3]; } - t2 = t3; return t2; } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-passing-ref-to-render-helper-props-object.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-passing-ref-to-render-helper-props-object.expect.md new file mode 100644 index 0000000000000..f23ab16c16bdd --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-passing-ref-to-render-helper-props-object.expect.md @@ -0,0 +1,45 @@ + +## Input + +```javascript +// @enableTreatRefLikeIdentifiersAsRefs @validateRefAccessDuringRender + +import {useRef} from 'react'; + +function Component(props) { + const ref = useRef(null); + + return {props.render({ref})}; +} + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; // @enableTreatRefLikeIdentifiersAsRefs @validateRefAccessDuringRender + +import { useRef } from "react"; + +function Component(props) { + const $ = _c(3); + const ref = useRef(null); + + const T0 = Foo; + const t0 = props.render({ ref }); + let t1; + if ($[0] !== T0 || $[1] !== t0) { + t1 = {t0}; + $[0] = T0; + $[1] = t0; + $[2] = t1; + } else { + t1 = $[2]; + } + return t1; +} + +``` + +### 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/allow-passing-ref-to-render-helper-props-object.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-passing-ref-to-render-helper-props-object.js new file mode 100644 index 0000000000000..ab9ffe2ed37f1 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-passing-ref-to-render-helper-props-object.js @@ -0,0 +1,9 @@ +// @enableTreatRefLikeIdentifiersAsRefs @validateRefAccessDuringRender + +import {useRef} from 'react'; + +function Component(props) { + const ref = useRef(null); + + return {props.render({ref})}; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-passing-ref-to-render-helper.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-passing-ref-to-render-helper.expect.md new file mode 100644 index 0000000000000..a0ad22fcaf417 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-passing-ref-to-render-helper.expect.md @@ -0,0 +1,49 @@ + +## Input + +```javascript +// @enableTreatRefLikeIdentifiersAsRefs @validateRefAccessDuringRender + +import {useRef} from 'react'; + +function Component(props) { + const ref = useRef(null); + + return {props.render(ref)}; +} + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; // @enableTreatRefLikeIdentifiersAsRefs @validateRefAccessDuringRender + +import { useRef } from "react"; + +function Component(props) { + const $ = _c(4); + const ref = useRef(null); + let t0; + if ($[0] !== props.render) { + t0 = props.render(ref); + $[0] = props.render; + $[1] = t0; + } else { + t0 = $[1]; + } + let t1; + if ($[2] !== t0) { + t1 = {t0}; + $[2] = t0; + $[3] = t1; + } else { + t1 = $[3]; + } + return t1; +} + +``` + +### 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/allow-passing-ref-to-render-helper.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-passing-ref-to-render-helper.js new file mode 100644 index 0000000000000..7c5a70188fab0 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-passing-ref-to-render-helper.js @@ -0,0 +1,9 @@ +// @enableTreatRefLikeIdentifiersAsRefs @validateRefAccessDuringRender + +import {useRef} from 'react'; + +function Component(props) { + const ref = useRef(null); + + return {props.render(ref)}; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-access-in-effect-indirect.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-access-in-effect-indirect.expect.md index 7c1f5ad3727b8..6cf97f6c35157 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-access-in-effect-indirect.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-access-in-effect-indirect.expect.md @@ -27,6 +27,7 @@ function Component() { } function Child({ref}) { + 'use no memo'; // This violates the rules of React, so we access the ref in a child // component return ref.current; @@ -100,8 +101,10 @@ function Component() { return t6; } -function Child(t0) { - const { ref } = t0; +function Child({ ref }) { + "use no memo"; + // This violates the rules of React, so we access the ref in a child + // component return ref.current; } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-access-in-effect-indirect.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-access-in-effect-indirect.js index 69429049022ab..4320b5871daf5 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-access-in-effect-indirect.js +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-access-in-effect-indirect.js @@ -23,6 +23,7 @@ function Component() { } function Child({ref}) { + 'use no memo'; // This violates the rules of React, so we access the ref in a child // component return ref.current; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-access-in-effect.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-access-in-effect.expect.md index 3bdf358611db6..009d504da25b3 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-access-in-effect.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-access-in-effect.expect.md @@ -23,6 +23,7 @@ function Component() { } function Child({ref}) { + 'use no memo'; // This violates the rules of React, so we access the ref in a child // component return ref.current; @@ -86,8 +87,10 @@ function Component() { return t5; } -function Child(t0) { - const { ref } = t0; +function Child({ ref }) { + "use no memo"; + // This violates the rules of React, so we access the ref in a child + // component return ref.current; } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-access-in-effect.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-access-in-effect.js index efba0547ebcd8..54a1dc22c3a6f 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-access-in-effect.js +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-access-in-effect.js @@ -19,6 +19,7 @@ function Component() { } function Child({ref}) { + 'use no memo'; // This violates the rules of React, so we access the ref in a child // component return ref.current; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-access-in-unused-callback-nested.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-access-in-unused-callback-nested.expect.md index 3584faf699f86..26e996017ebd7 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-access-in-unused-callback-nested.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-access-in-unused-callback-nested.expect.md @@ -25,6 +25,7 @@ function Component() { } function Child({ref}) { + 'use no memo'; // This violates the rules of React, so we access the ref in a child // component return ref.current; @@ -83,8 +84,10 @@ function Component() { } function _temp() {} -function Child(t0) { - const { ref } = t0; +function Child({ ref }) { + "use no memo"; + // This violates the rules of React, so we access the ref in a child + // component return ref.current; } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-access-in-unused-callback-nested.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-access-in-unused-callback-nested.js index 1e402886376c3..dcd8540e2a4b4 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-access-in-unused-callback-nested.js +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-access-in-unused-callback-nested.js @@ -21,6 +21,7 @@ function Component() { } function Child({ref}) { + 'use no memo'; // This violates the rules of React, so we access the ref in a child // component return ref.current; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-lazy-initialization-with-logical.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-lazy-initialization-with-logical.expect.md new file mode 100644 index 0000000000000..3540e842f67b8 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-lazy-initialization-with-logical.expect.md @@ -0,0 +1,68 @@ + +## Input + +```javascript +// @validateRefAccessDuringRender + +import {useRef} from 'react'; + +function Component(props) { + const ref = useRef(null); + if (ref.current == null) { + // the logical means the ref write is in a different block + // from the if consequent. this tests that the "safe" blocks + // extend up to the if's fallthrough + ref.current = props.unknownKey ?? props.value; + } + return ; +} + +function Child({ref}) { + 'use no memo'; + return ref.current; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{value: 42}], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; // @validateRefAccessDuringRender + +import { useRef } from "react"; + +function Component(props) { + const $ = _c(1); + const ref = useRef(null); + if (ref.current == null) { + ref.current = props.unknownKey ?? props.value; + } + let t0; + if ($[0] === Symbol.for("react.memo_cache_sentinel")) { + t0 = ; + $[0] = t0; + } else { + t0 = $[0]; + } + return t0; +} + +function Child({ ref }) { + "use no memo"; + return ref.current; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ value: 42 }], +}; + +``` + +### Eval output +(kind: ok) 42 \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-lazy-initialization-with-logical.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-lazy-initialization-with-logical.js new file mode 100644 index 0000000000000..2e1b03a28dadf --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-lazy-initialization-with-logical.js @@ -0,0 +1,24 @@ +// @validateRefAccessDuringRender + +import {useRef} from 'react'; + +function Component(props) { + const ref = useRef(null); + if (ref.current == null) { + // the logical means the ref write is in a different block + // from the if consequent. this tests that the "safe" blocks + // extend up to the if's fallthrough + ref.current = props.unknownKey ?? props.value; + } + return ; +} + +function Child({ref}) { + 'use no memo'; + return ref.current; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{value: 42}], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/array-at-closure.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/array-at-closure.expect.md index 9f26c192f5f87..810f1a3f5e4ee 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/array-at-closure.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/array-at-closure.expect.md @@ -19,7 +19,7 @@ function Component(props) { ```javascript import { c as _c } from "react/compiler-runtime"; function Component(props) { - const $ = _c(7); + const $ = _c(5); let t0; if ($[0] !== props.x) { t0 = foo(props.x); @@ -31,26 +31,19 @@ function Component(props) { const x = t0; let t1; if ($[2] !== props || $[3] !== x) { - t1 = function () { + const fn = function () { const arr = [...bar(props)]; return arr.at(x); }; + + t1 = fn(); $[2] = props; $[3] = x; $[4] = t1; } else { t1 = $[4]; } - const fn = t1; - let t2; - if ($[5] !== fn) { - t2 = fn(); - $[5] = fn; - $[6] = t2; - } else { - t2 = $[6]; - } - const fnResult = t2; + const fnResult = t1; return fnResult; } 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 1680386c741a3..efd094c1a5360 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 @@ -23,34 +23,18 @@ export const FIXTURE_ENTRYPOINT = { ```javascript import { c as _c } from "react/compiler-runtime"; function Component(props) { - const $ = _c(6); + const $ = _c(2); let t0; if ($[0] !== props.a) { - t0 = { a: props.a }; + const item = { a: props.a }; + const items = [item]; + t0 = items.map(_temp); $[0] = props.a; $[1] = t0; } else { t0 = $[1]; } - const item = t0; - let t1; - if ($[2] !== item) { - t1 = [item]; - $[2] = item; - $[3] = t1; - } else { - t1 = $[3]; - } - const items = t1; - let t2; - if ($[4] !== items) { - t2 = items.map(_temp); - $[4] = items; - $[5] = t2; - } else { - t2 = $[5]; - } - const mapped = t2; + const mapped = t0; return mapped; } function _temp(item_0) { 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 867d51cb232bc..f165502a29b60 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,26 +21,18 @@ export const FIXTURE_ENTRYPOINT = { ```javascript import { c as _c } from "react/compiler-runtime"; function Component(props) { - const $ = _c(4); + const $ = _c(2); const f = _temp; let t0; if ($[0] !== props.items) { - t0 = [...props.items].map(f); + const x = [...props.items].map(f); + t0 = [x, f]; $[0] = props.items; $[1] = t0; } else { t0 = $[1]; } - const x = t0; - let t1; - if ($[2] !== x) { - t1 = [x, f]; - $[2] = x; - $[3] = t1; - } else { - t1 = $[3]; - } - return t1; + return t0; } function _temp(item) { return item; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/array-pattern-spread-creates-array.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/array-pattern-spread-creates-array.expect.md new file mode 100644 index 0000000000000..9994a6536f419 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/array-pattern-spread-creates-array.expect.md @@ -0,0 +1,104 @@ + +## Input + +```javascript +// @validatePreserveExistingMemoizationGuarantees +import {useMemo} from 'react'; +import {makeObject_Primitives, ValidateMemoization} from 'shared-runtime'; + +function Component(props) { + // Should memoize independently + const x = useMemo(() => makeObject_Primitives(), []); + + const rest = useMemo(() => { + const [_, ...rest] = props.array; + + // Should be inferred as Array.proto.push which doesn't mutate input + rest.push(x); + return rest; + }); + + return ( + <> + + + + ); +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{array: [0, 1, 2]}], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; // @validatePreserveExistingMemoizationGuarantees +import { useMemo } from "react"; +import { makeObject_Primitives, ValidateMemoization } from "shared-runtime"; + +function Component(props) { + const $ = _c(9); + let t0; + if ($[0] === Symbol.for("react.memo_cache_sentinel")) { + t0 = makeObject_Primitives(); + $[0] = t0; + } else { + t0 = $[0]; + } + const x = t0; + let rest; + if ($[1] !== props.array) { + [, ...rest] = props.array; + + rest.push(x); + $[1] = props.array; + $[2] = rest; + } else { + rest = $[2]; + } + const rest_0 = rest; + let t1; + if ($[3] === Symbol.for("react.memo_cache_sentinel")) { + t1 = ; + $[3] = t1; + } else { + t1 = $[3]; + } + let t2; + if ($[4] !== props.array) { + t2 = [props.array]; + $[4] = props.array; + $[5] = t2; + } else { + t2 = $[5]; + } + let t3; + if ($[6] !== rest_0 || $[7] !== t2) { + t3 = ( + <> + {t1} + + + ); + $[6] = rest_0; + $[7] = t2; + $[8] = t3; + } else { + t3 = $[8]; + } + return t3; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ array: [0, 1, 2] }], +}; + +``` + +### Eval output +(kind: ok)
{"inputs":[],"output":{"a":0,"b":"value1","c":true}}
{"inputs":[[0,1,2]],"output":[1,2,{"a":0,"b":"value1","c":true}]}
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/array-pattern-spread-creates-array.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/array-pattern-spread-creates-array.js new file mode 100644 index 0000000000000..888bdbcb8b935 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/array-pattern-spread-creates-array.js @@ -0,0 +1,28 @@ +// @validatePreserveExistingMemoizationGuarantees +import {useMemo} from 'react'; +import {makeObject_Primitives, ValidateMemoization} from 'shared-runtime'; + +function Component(props) { + // Should memoize independently + const x = useMemo(() => makeObject_Primitives(), []); + + const rest = useMemo(() => { + const [_, ...rest] = props.array; + + // Should be inferred as Array.proto.push which doesn't mutate input + rest.push(x); + return rest; + }); + + return ( + <> + + + + ); +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{array: [0, 1, 2]}], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/babel-existing-react-import.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/babel-existing-react-import.expect.md index 6ffd7fa1cdbc5..8b48145d91302 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/babel-existing-react-import.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/babel-existing-react-import.expect.md @@ -30,50 +30,46 @@ function Component(props) { const $ = _c(4); const [x] = useState(0); let t0; - let t1; if ($[0] !== x) { - t1 = calculateExpensiveNumber(x); + t0 = calculateExpensiveNumber(x); $[0] = x; - $[1] = t1; + $[1] = t0; } else { - t1 = $[1]; + t0 = $[1]; } - t0 = t1; const expensiveNumber = t0; - let t2; + let t1; if ($[2] !== expensiveNumber) { - t2 =
{expensiveNumber}
; + t1 =
{expensiveNumber}
; $[2] = expensiveNumber; - $[3] = t2; + $[3] = t1; } else { - t2 = $[3]; + t1 = $[3]; } - return t2; + return t1; } function Component2(props) { const $ = _c(4); const [x] = useState(0); let t0; - let t1; if ($[0] !== x) { - t1 = calculateExpensiveNumber(x); + t0 = calculateExpensiveNumber(x); $[0] = x; - $[1] = t1; + $[1] = t0; } else { - t1 = $[1]; + t0 = $[1]; } - t0 = t1; const expensiveNumber = t0; - let t2; + let t1; if ($[2] !== expensiveNumber) { - t2 =
{expensiveNumber}
; + t1 =
{expensiveNumber}
; $[2] = expensiveNumber; - $[3] = t2; + $[3] = t1; } else { - t2 = $[3]; + t1 = $[3]; } - return t2; + return t1; } ``` diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/babel-existing-react-kitchensink-import.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/babel-existing-react-kitchensink-import.expect.md index ec5d7ae51da4f..745da40e7c22e 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/babel-existing-react-kitchensink-import.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/babel-existing-react-kitchensink-import.expect.md @@ -32,50 +32,46 @@ function Component(props) { const $ = _c(4); const [x] = useState(0); let t0; - let t1; if ($[0] !== x) { - t1 = calculateExpensiveNumber(x); + t0 = calculateExpensiveNumber(x); $[0] = x; - $[1] = t1; + $[1] = t0; } else { - t1 = $[1]; + t0 = $[1]; } - t0 = t1; const expensiveNumber = t0; - let t2; + let t1; if ($[2] !== expensiveNumber) { - t2 =
{expensiveNumber}
; + t1 =
{expensiveNumber}
; $[2] = expensiveNumber; - $[3] = t2; + $[3] = t1; } else { - t2 = $[3]; + t1 = $[3]; } - return t2; + return t1; } function Component2(props) { const $ = _c(4); const [x] = useState(0); let t0; - let t1; if ($[0] !== x) { - t1 = calculateExpensiveNumber(x); + t0 = calculateExpensiveNumber(x); $[0] = x; - $[1] = t1; + $[1] = t0; } else { - t1 = $[1]; + t0 = $[1]; } - t0 = t1; const expensiveNumber = t0; - let t2; + let t1; if ($[2] !== expensiveNumber) { - t2 =
{expensiveNumber}
; + t1 =
{expensiveNumber}
; $[2] = expensiveNumber; - $[3] = t2; + $[3] = t1; } else { - t2 = $[3]; + t1 = $[3]; } - return t2; + return t1; } ``` diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/babel-existing-react-namespace-import.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/babel-existing-react-namespace-import.expect.md index 161b3707f5c46..297b01229c20a 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/babel-existing-react-namespace-import.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/babel-existing-react-namespace-import.expect.md @@ -30,25 +30,23 @@ function Component(props) { const $ = _c(4); const [x] = React.useState(0); let t0; - let t1; if ($[0] !== x) { - t1 = calculateExpensiveNumber(x); + t0 = calculateExpensiveNumber(x); $[0] = x; - $[1] = t1; + $[1] = t0; } else { - t1 = $[1]; + t0 = $[1]; } - t0 = t1; const expensiveNumber = t0; - let t2; + let t1; if ($[2] !== expensiveNumber) { - t2 =
{expensiveNumber}
; + t1 =
{expensiveNumber}
; $[2] = expensiveNumber; - $[3] = t2; + $[3] = t1; } else { - t2 = $[3]; + t1 = $[3]; } - return t2; + return t1; } export const FIXTURE_ENTRYPOINT = { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/babel-existing-react-runtime-import.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/babel-existing-react-runtime-import.expect.md index 5bb87a2b032c2..ac4e7dc3a8c4d 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/babel-existing-react-runtime-import.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/babel-existing-react-runtime-import.expect.md @@ -36,30 +36,28 @@ function Component(props) { const $ = _c(4); const [x] = React.useState(0); let t0; - let t1; if ($[0] !== x) { - t1 = calculateExpensiveNumber(x); + t0 = calculateExpensiveNumber(x); $[0] = x; - $[1] = t1; + $[1] = t0; } else { - t1 = $[1]; + t0 = $[1]; } - t0 = t1; const expensiveNumber = t0; - let t2; + let t1; if ($[2] !== expensiveNumber) { - t2 = ( + t1 = (
{expensiveNumber} {`${someImport}`}
); $[2] = expensiveNumber; - $[3] = t2; + $[3] = t1; } else { - t2 = $[3]; + t1 = $[3]; } - return t2; + return t1; } export const FIXTURE_ENTRYPOINT = { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/babel-repro-compact-negative-number.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/babel-repro-compact-negative-number.expect.md new file mode 100644 index 0000000000000..70e19e0744a4a --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/babel-repro-compact-negative-number.expect.md @@ -0,0 +1,56 @@ + +## Input + +```javascript +import {Stringify} from 'shared-runtime'; + +function Repro(props) { + const MY_CONST = -2; + return {props.arg - MY_CONST}; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Repro, + params: [ + { + arg: 3, + }, + ], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; +import { Stringify } from "shared-runtime"; + +function Repro(props) { + const $ = _c(2); + + const t0 = props.arg - -2; + let t1; + if ($[0] !== t0) { + t1 = {t0}; + $[0] = t0; + $[1] = t1; + } else { + t1 = $[1]; + } + return t1; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Repro, + params: [ + { + arg: 3, + }, + ], +}; + +``` + +### Eval output +(kind: ok)
{"children":5}
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/babel-repro-compact-negative-number.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/babel-repro-compact-negative-number.js new file mode 100644 index 0000000000000..891589bc981d4 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/babel-repro-compact-negative-number.js @@ -0,0 +1,15 @@ +import {Stringify} from 'shared-runtime'; + +function Repro(props) { + const MY_CONST = -2; + return {props.arg - MY_CONST}; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Repro, + params: [ + { + arg: 3, + }, + ], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/block-scoping-switch-variable-scoping.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/block-scoping-switch-variable-scoping.expect.md index d5bf094ef4ff9..ce8e7b422329f 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/block-scoping-switch-variable-scoping.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/block-scoping-switch-variable-scoping.expect.md @@ -36,26 +36,22 @@ import { useMemo } from "react"; function Component(props) { const $ = _c(2); let t0; - let t1; if ($[0] !== props.value) { - t1 = { value: props.value }; + t0 = { value: props.value }; $[0] = props.value; - $[1] = t1; + $[1] = t0; } else { - t1 = $[1]; + t0 = $[1]; } - const handlers = t1; + const handlers = t0; bb0: switch (props.test) { case true: { console.log(handlers.value); break bb0; } - default: { - } + default: } - - t0 = handlers; - const outerHandlers = t0; + const outerHandlers = handlers; return outerHandlers; } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-capturing-func-maybealias-captured-mutate.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-capturing-func-maybealias-captured-mutate.expect.md index b8c7f8d4225f7..8d1e852225583 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-capturing-func-maybealias-captured-mutate.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-capturing-func-maybealias-captured-mutate.expect.md @@ -2,6 +2,7 @@ ## Input ```javascript +// @enableNewMutationAliasingModel:false import {makeArray, mutate} from 'shared-runtime'; /** @@ -56,7 +57,7 @@ export const FIXTURE_ENTRYPOINT = { ## Code ```javascript -import { c as _c } from "react/compiler-runtime"; +import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel:false import { makeArray, mutate } from "shared-runtime"; /** @@ -84,19 +85,11 @@ import { makeArray, mutate } from "shared-runtime"; * used when we analyze CallExpressions. */ function Component(t0) { - const $ = _c(5); + const $ = _c(3); const { foo, bar } = t0; - let t1; - if ($[0] !== foo) { - t1 = { foo }; - $[0] = foo; - $[1] = t1; - } else { - t1 = $[1]; - } - const x = t1; let y; - if ($[2] !== bar || $[3] !== x) { + if ($[0] !== bar || $[1] !== foo) { + const x = { foo }; y = { bar }; const f0 = function () { const a = makeArray(y); @@ -107,11 +100,11 @@ function Component(t0) { f0(); mutate(y.x); - $[2] = bar; - $[3] = x; - $[4] = y; + $[0] = bar; + $[1] = foo; + $[2] = y; } else { - y = $[4]; + y = $[2]; } return y; } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-capturing-func-maybealias-captured-mutate.ts b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-capturing-func-maybealias-captured-mutate.ts index ca7076fda4019..62d891febf41a 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-capturing-func-maybealias-captured-mutate.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-capturing-func-maybealias-captured-mutate.ts @@ -1,3 +1,4 @@ +// @enableNewMutationAliasingModel:false import {makeArray, mutate} from 'shared-runtime'; /** diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-ref-prefix-postfix-operator.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-ref-prefix-postfix-operator.expect.md new file mode 100644 index 0000000000000..ccfc451750004 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-ref-prefix-postfix-operator.expect.md @@ -0,0 +1,132 @@ + +## Input + +```javascript +import {useRef, useEffect} from 'react'; + +/** + * The postfix increment operator should return the value before incrementing. + * ```js + * const id = count.current; // 0 + * count.current = count.current + 1; // 1 + * return id; + * ``` + * The bug is that we currently increment the value before the expression is evaluated. + * This bug does not trigger when the incremented value is a plain primitive. + * + * Found differences in evaluator results + * Non-forget (expected): + * (kind: ok) {"count":{"current":0},"updateCountPostfix":"[[ function params=0 ]]","updateCountPrefix":"[[ function params=0 ]]"} + * logs: ['id = 0','count = 1'] + * Forget: + * (kind: ok) {"count":{"current":0},"updateCountPostfix":"[[ function params=0 ]]","updateCountPrefix":"[[ function params=0 ]]"} + * logs: ['id = 1','count = 1'] + */ +function useFoo() { + const count = useRef(0); + const updateCountPostfix = () => { + const id = count.current++; + return id; + }; + const updateCountPrefix = () => { + const id = ++count.current; + return id; + }; + useEffect(() => { + const id = updateCountPostfix(); + console.log(`id = ${id}`); + console.log(`count = ${count.current}`); + }, []); + return {count, updateCountPostfix, updateCountPrefix}; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; +import { useRef, useEffect } from "react"; + +/** + * The postfix increment operator should return the value before incrementing. + * ```js + * const id = count.current; // 0 + * count.current = count.current + 1; // 1 + * return id; + * ``` + * The bug is that we currently increment the value before the expression is evaluated. + * This bug does not trigger when the incremented value is a plain primitive. + * + * Found differences in evaluator results + * Non-forget (expected): + * (kind: ok) {"count":{"current":0},"updateCountPostfix":"[[ function params=0 ]]","updateCountPrefix":"[[ function params=0 ]]"} + * logs: ['id = 0','count = 1'] + * Forget: + * (kind: ok) {"count":{"current":0},"updateCountPostfix":"[[ function params=0 ]]","updateCountPrefix":"[[ function params=0 ]]"} + * logs: ['id = 1','count = 1'] + */ +function useFoo() { + const $ = _c(5); + const count = useRef(0); + let t0; + if ($[0] === Symbol.for("react.memo_cache_sentinel")) { + t0 = () => { + count.current = count.current + 1; + const id = count.current; + return id; + }; + $[0] = t0; + } else { + t0 = $[0]; + } + const updateCountPostfix = t0; + let t1; + if ($[1] === Symbol.for("react.memo_cache_sentinel")) { + t1 = () => { + const id_0 = (count.current = count.current + 1); + return id_0; + }; + $[1] = t1; + } else { + t1 = $[1]; + } + const updateCountPrefix = t1; + let t2; + let t3; + if ($[2] === Symbol.for("react.memo_cache_sentinel")) { + t2 = () => { + const id_1 = updateCountPostfix(); + console.log(`id = ${id_1}`); + console.log(`count = ${count.current}`); + }; + t3 = []; + $[2] = t2; + $[3] = t3; + } else { + t2 = $[2]; + t3 = $[3]; + } + useEffect(t2, t3); + let t4; + if ($[4] === Symbol.for("react.memo_cache_sentinel")) { + t4 = { count, updateCountPostfix, updateCountPrefix }; + $[4] = t4; + } else { + t4 = $[4]; + } + return t4; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [], +}; + +``` + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-ref-prefix-postfix-operator.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-ref-prefix-postfix-operator.js new file mode 100644 index 0000000000000..a7c1fad8bf231 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-ref-prefix-postfix-operator.js @@ -0,0 +1,42 @@ +import {useRef, useEffect} from 'react'; + +/** + * The postfix increment operator should return the value before incrementing. + * ```js + * const id = count.current; // 0 + * count.current = count.current + 1; // 1 + * return id; + * ``` + * The bug is that we currently increment the value before the expression is evaluated. + * This bug does not trigger when the incremented value is a plain primitive. + * + * Found differences in evaluator results + * Non-forget (expected): + * (kind: ok) {"count":{"current":0},"updateCountPostfix":"[[ function params=0 ]]","updateCountPrefix":"[[ function params=0 ]]"} + * logs: ['id = 0','count = 1'] + * Forget: + * (kind: ok) {"count":{"current":0},"updateCountPostfix":"[[ function params=0 ]]","updateCountPrefix":"[[ function params=0 ]]"} + * logs: ['id = 1','count = 1'] + */ +function useFoo() { + const count = useRef(0); + const updateCountPostfix = () => { + const id = count.current++; + return id; + }; + const updateCountPrefix = () => { + const id = ++count.current; + return id; + }; + useEffect(() => { + const id = updateCountPostfix(); + console.log(`id = ${id}`); + console.log(`count = ${count.current}`); + }, []); + return {count, updateCountPostfix, updateCountPrefix}; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-separate-memoization-due-to-callback-capturing.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-separate-memoization-due-to-callback-capturing.expect.md new file mode 100644 index 0000000000000..7767989574c73 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-separate-memoization-due-to-callback-capturing.expect.md @@ -0,0 +1,138 @@ + +## Input + +```javascript +// @enableNewMutationAliasingModel:false +import {ValidateMemoization} from 'shared-runtime'; + +const Codes = { + en: {name: 'English'}, + ja: {name: 'Japanese'}, + ko: {name: 'Korean'}, + zh: {name: 'Chinese'}, +}; + +function Component(a) { + let keys; + if (a) { + keys = Object.keys(Codes); + } else { + return null; + } + const options = keys.map(code => { + const country = Codes[code]; + return { + name: country.name, + code, + }; + }); + return ( + <> + + + + ); +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{a: false}], + sequentialRenders: [ + {a: false}, + {a: true}, + {a: true}, + {a: false}, + {a: true}, + {a: false}, + ], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel:false +import { ValidateMemoization } from "shared-runtime"; + +const Codes = { + en: { name: "English" }, + ja: { name: "Japanese" }, + ko: { name: "Korean" }, + zh: { name: "Chinese" }, +}; + +function Component(a) { + const $ = _c(4); + let keys; + if (a) { + let t0; + if ($[0] === Symbol.for("react.memo_cache_sentinel")) { + t0 = Object.keys(Codes); + $[0] = t0; + } else { + t0 = $[0]; + } + keys = t0; + } else { + return null; + } + let t0; + if ($[1] === Symbol.for("react.memo_cache_sentinel")) { + t0 = keys.map(_temp); + $[1] = t0; + } else { + t0 = $[1]; + } + const options = t0; + let t1; + if ($[2] === Symbol.for("react.memo_cache_sentinel")) { + t1 = ( + + ); + $[2] = t1; + } else { + t1 = $[2]; + } + let t2; + if ($[3] === Symbol.for("react.memo_cache_sentinel")) { + t2 = ( + <> + {t1} + + + ); + $[3] = t2; + } else { + t2 = $[3]; + } + return t2; +} +function _temp(code) { + const country = Codes[code]; + return { name: country.name, code }; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ a: false }], + sequentialRenders: [ + { a: false }, + { a: true }, + { a: true }, + { a: false }, + { a: true }, + { a: false }, + ], +}; + +``` + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-separate-memoization-due-to-callback-capturing.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-separate-memoization-due-to-callback-capturing.js new file mode 100644 index 0000000000000..c28ee705d1667 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-separate-memoization-due-to-callback-capturing.js @@ -0,0 +1,48 @@ +// @enableNewMutationAliasingModel:false +import {ValidateMemoization} from 'shared-runtime'; + +const Codes = { + en: {name: 'English'}, + ja: {name: 'Japanese'}, + ko: {name: 'Korean'}, + zh: {name: 'Chinese'}, +}; + +function Component(a) { + let keys; + if (a) { + keys = Object.keys(Codes); + } else { + return null; + } + const options = keys.map(code => { + const country = Codes[code]; + return { + name: country.name, + code, + }; + }); + return ( + <> + + + + ); +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{a: false}], + sequentialRenders: [ + {a: false}, + {a: true}, + {a: true}, + {a: false}, + {a: true}, + {a: false}, + ], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-arrow-function-1.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-arrow-function-1.expect.md index a11b0d8ada4da..85950edabac7a 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-arrow-function-1.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-arrow-function-1.expect.md @@ -23,27 +23,19 @@ export const FIXTURE_ENTRYPOINT = { ```javascript import { c as _c } from "react/compiler-runtime"; function component(a) { - const $ = _c(4); + const $ = _c(2); let t0; if ($[0] !== a) { - t0 = { a }; + const z = { a }; + t0 = () => { + console.log(z); + }; $[0] = a; $[1] = t0; } else { t0 = $[1]; } - const z = t0; - let t1; - if ($[2] !== z) { - t1 = () => { - console.log(z); - }; - $[2] = z; - $[3] = t1; - } else { - t1 = $[3]; - } - const x = t1; + const x = t0; return x; } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-function-1.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-function-1.expect.md index 206cf4b6b73b0..41732aed0d6e8 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-function-1.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-function-1.expect.md @@ -23,27 +23,19 @@ export const FIXTURE_ENTRYPOINT = { ```javascript import { c as _c } from "react/compiler-runtime"; function component(a) { - const $ = _c(4); + const $ = _c(2); let t0; if ($[0] !== a) { - t0 = { a }; + const z = { a }; + t0 = function () { + console.log(z); + }; $[0] = a; $[1] = t0; } else { t0 = $[1]; } - const z = t0; - let t1; - if ($[2] !== z) { - t1 = function () { - console.log(z); - }; - $[2] = z; - $[3] = t1; - } else { - t1 = $[3]; - } - const x = t1; + const x = t0; return x; } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-function-alias-computed-load-2-iife.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-function-alias-computed-load-2-iife.expect.md index 2afc5fd25dbac..50480f1b2515e 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-function-alias-computed-load-2-iife.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-function-alias-computed-load-2-iife.expect.md @@ -25,17 +25,25 @@ export const FIXTURE_ENTRYPOINT = { ```javascript import { c as _c } from "react/compiler-runtime"; function bar(a) { - const $ = _c(2); - let y; + const $ = _c(4); + let t0; if ($[0] !== a) { - const x = [a]; + t0 = [a]; + $[0] = a; + $[1] = t0; + } else { + t0 = $[1]; + } + const x = t0; + let y; + if ($[2] !== x[0][1]) { y = {}; y = x[0][1]; - $[0] = a; - $[1] = y; + $[2] = x[0][1]; + $[3] = y; } else { - y = $[1]; + y = $[3]; } return y; } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-function-alias-computed-load-3-iife.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-function-alias-computed-load-3-iife.expect.md index f0267c3309f5b..9678918b3d27f 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-function-alias-computed-load-3-iife.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-function-alias-computed-load-3-iife.expect.md @@ -29,20 +29,29 @@ export const FIXTURE_ENTRYPOINT = { ```javascript import { c as _c } from "react/compiler-runtime"; function bar(a, b) { - const $ = _c(3); - let y; + const $ = _c(6); + let t0; if ($[0] !== a || $[1] !== b) { - const x = [a, b]; + t0 = [a, b]; + $[0] = a; + $[1] = b; + $[2] = t0; + } else { + t0 = $[2]; + } + const x = t0; + let y; + if ($[3] !== x[0][1] || $[4] !== x[1][0]) { y = {}; let t = {}; y = x[0][1]; t = x[1][0]; - $[0] = a; - $[1] = b; - $[2] = y; + $[3] = x[0][1]; + $[4] = x[1][0]; + $[5] = y; } else { - y = $[2]; + y = $[5]; } return y; } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-function-alias-computed-load-4-iife.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-function-alias-computed-load-4-iife.expect.md index 22728aaf4323d..edddf3715a453 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-function-alias-computed-load-4-iife.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-function-alias-computed-load-4-iife.expect.md @@ -25,17 +25,25 @@ export const FIXTURE_ENTRYPOINT = { ```javascript import { c as _c } from "react/compiler-runtime"; function bar(a) { - const $ = _c(2); - let y; + const $ = _c(4); + let t0; if ($[0] !== a) { - const x = [a]; + t0 = [a]; + $[0] = a; + $[1] = t0; + } else { + t0 = $[1]; + } + const x = t0; + let y; + if ($[2] !== x[0].a[1]) { y = {}; y = x[0].a[1]; - $[0] = a; - $[1] = y; + $[2] = x[0].a[1]; + $[3] = y; } else { - y = $[1]; + y = $[3]; } return y; } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-function-alias-computed-load-iife.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-function-alias-computed-load-iife.expect.md index 60f829cdc4d66..c9ce6dda9f627 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-function-alias-computed-load-iife.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-function-alias-computed-load-iife.expect.md @@ -24,17 +24,25 @@ export const FIXTURE_ENTRYPOINT = { ```javascript import { c as _c } from "react/compiler-runtime"; function bar(a) { - const $ = _c(2); - let y; + const $ = _c(4); + let t0; if ($[0] !== a) { - const x = [a]; + t0 = [a]; + $[0] = a; + $[1] = t0; + } else { + t0 = $[1]; + } + const x = t0; + let y; + if ($[2] !== x[0]) { y = {}; y = x[0]; - $[0] = a; - $[1] = y; + $[2] = x[0]; + $[3] = y; } else { - y = $[1]; + y = $[3]; } return y; } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-function-runs-inference.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-function-runs-inference.expect.md index dc0961c61264c..31b80bcda3b29 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-function-runs-inference.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-function-runs-inference.expect.md @@ -22,35 +22,19 @@ export const FIXTURE_ENTRYPOINT = { import { c as _c } from "react/compiler-runtime"; import { Stringify } from "shared-runtime"; function Component(t0) { - const $ = _c(6); + const $ = _c(2); const { a } = t0; let t1; if ($[0] !== a) { - t1 = { a }; + const z = { a }; + const p = () => {z}; + t1 = p(); $[0] = a; $[1] = t1; } else { t1 = $[1]; } - const z = t1; - let t2; - if ($[2] !== z) { - t2 = () => {z}; - $[2] = z; - $[3] = t2; - } else { - t2 = $[3]; - } - const p = t2; - let t3; - if ($[4] !== p) { - t3 = p(); - $[4] = p; - $[5] = t3; - } else { - t3 = $[5]; - } - return t3; + return t1; } export const FIXTURE_ENTRYPOINT = { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-variable-in-nested-block.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-variable-in-nested-block.expect.md index 0dc10c4851094..ec8a96a392678 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-variable-in-nested-block.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-variable-in-nested-block.expect.md @@ -25,27 +25,19 @@ export const FIXTURE_ENTRYPOINT = { ```javascript import { c as _c } from "react/compiler-runtime"; function component(a) { - const $ = _c(4); + const $ = _c(2); let t0; if ($[0] !== a) { - t0 = { a }; + const z = { a }; + t0 = function () { + console.log(z); + }; $[0] = a; $[1] = t0; } else { t0 = $[1]; } - const z = t0; - let t1; - if ($[2] !== z) { - t1 = function () { - console.log(z); - }; - $[2] = z; - $[3] = t1; - } else { - t1 = $[3]; - } - const x = t1; + const x = t0; return x; } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-variable-in-nested-function.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-variable-in-nested-function.expect.md index b66953a43d353..ee41bc88f5eef 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-variable-in-nested-function.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-variable-in-nested-function.expect.md @@ -25,29 +25,21 @@ export const FIXTURE_ENTRYPOINT = { ```javascript import { c as _c } from "react/compiler-runtime"; function component(a) { - const $ = _c(4); + const $ = _c(2); let t0; if ($[0] !== a) { - t0 = { a }; - $[0] = a; - $[1] = t0; - } else { - t0 = $[1]; - } - const z = t0; - let t1; - if ($[2] !== z) { - t1 = function () { + const z = { a }; + t0 = function () { (function () { console.log(z); })(); }; - $[2] = z; - $[3] = t1; + $[0] = a; + $[1] = t0; } else { - t1 = $[3]; + t0 = $[1]; } - const x = t1; + const x = t0; return x; } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/codegen-inline-iife-reassign.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/codegen-inline-iife-reassign.expect.md index 9c8fc0f1c5b67..b7288a854ffe3 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/codegen-inline-iife-reassign.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/codegen-inline-iife-reassign.expect.md @@ -37,11 +37,9 @@ function useTest() { const t1 = (w = 42); const t2 = w; - let t3; w = 999; - t3 = 2; - t0 = makeArray(t1, t2, t3); + t0 = makeArray(t1, t2, 2); $[0] = t0; } else { t0 = $[0]; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/codegen-inline-iife-storeprop.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/codegen-inline-iife-storeprop.expect.md index 58c54ddaab891..85a66bb204cbb 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/codegen-inline-iife-storeprop.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/codegen-inline-iife-storeprop.expect.md @@ -37,11 +37,9 @@ function useTest() { const t1 = (w.x = 42); const t2 = w.x; - let t3; w.x = 999; - t3 = 2; - t0 = makeArray(t1, t2, t3); + t0 = makeArray(t1, t2, 2); $[0] = t0; } else { t0 = $[0]; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/codegen-inline-iife.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/codegen-inline-iife.expect.md index 25a08bc3329cb..70b23c70c093d 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/codegen-inline-iife.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/codegen-inline-iife.expect.md @@ -32,11 +32,9 @@ function useTest() { let t0; if ($[0] === Symbol.for("react.memo_cache_sentinel")) { const t1 = print(1); - let t2; print(2); - t2 = 2; - t0 = makeArray(t1, t2); + t0 = makeArray(t1, 2); $[0] = t0; } else { t0 = $[0]; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/consecutive-use-memo.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/consecutive-use-memo.expect.md index 534093bdde10b..b09c51a8768d7 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/consecutive-use-memo.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/consecutive-use-memo.expect.md @@ -29,37 +29,33 @@ function useHook(t0) { const $ = _c(7); const { a, b } = t0; let t1; - let t2; if ($[0] !== a) { - t2 = identity({ a }); + t1 = identity({ a }); $[0] = a; - $[1] = t2; + $[1] = t1; } else { - t2 = $[1]; + t1 = $[1]; } - t1 = t2; const valA = t1; - let t3; - let t4; + let t2; if ($[2] !== b) { - t4 = identity([b]); + t2 = identity([b]); $[2] = b; - $[3] = t4; + $[3] = t2; } else { - t4 = $[3]; + t2 = $[3]; } - t3 = t4; - const valB = t3; - let t5; + const valB = t2; + let t3; if ($[4] !== valA || $[5] !== valB) { - t5 = [valA, valB]; + t3 = [valA, valB]; $[4] = valA; $[5] = valB; - $[6] = t5; + $[6] = t3; } else { - t5 = $[6]; + t3 = $[6]; } - return t5; + return t3; } export const FIXTURE_ENTRYPOINT = { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/context-variable-as-jsx-element-tag.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/context-variable-as-jsx-element-tag.expect.md index da3bb94ed5ed4..5b8824eca6b6f 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/context-variable-as-jsx-element-tag.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/context-variable-as-jsx-element-tag.expect.md @@ -34,10 +34,8 @@ function Component(props) { let Component; if ($[0] === Symbol.for("react.memo_cache_sentinel")) { Component = Stringify; - let t0; - t0 = Component; - Component = t0; + Component = Component; $[0] = Component; } else { Component = $[0]; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/custom-opt-out-directive.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/custom-opt-out-directive.expect.md new file mode 100644 index 0000000000000..7875137a88f55 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/custom-opt-out-directive.expect.md @@ -0,0 +1,35 @@ + +## Input + +```javascript +// @customOptOutDirectives:["use todo memo"] +function Component() { + 'use todo memo'; + return
hello world!
; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [], +}; + +``` + +## Code + +```javascript +// @customOptOutDirectives:["use todo memo"] +function Component() { + "use todo memo"; + return
hello world!
; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [], +}; + +``` + +### Eval output +(kind: ok)
hello world!
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/custom-opt-out-directive.tsx b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/custom-opt-out-directive.tsx new file mode 100644 index 0000000000000..225559618386f --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/custom-opt-out-directive.tsx @@ -0,0 +1,10 @@ +// @customOptOutDirectives:["use todo memo"] +function Component() { + 'use todo memo'; + return
hello world!
; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [], +}; 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 880c158b721d0..7d0a1ffed1fac 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 @@ -28,20 +28,18 @@ import { c as _c } from "react/compiler-runtime"; function Foo() { const $ = _c(1); let t0; - let t1; if ($[0] === Symbol.for("react.memo_cache_sentinel")) { - t1 = function a(t2) { - const x_0 = t2 === undefined ? _temp : t2; - return (function b(t3) { - const y_0 = t3 === undefined ? [] : t3; + t0 = function a(t1) { + const x_0 = t1 === undefined ? _temp : t1; + return (function b(t2) { + const y_0 = t2 === undefined ? [] : t2; return [x_0, y_0]; })(); }; - $[0] = t1; + $[0] = t0; } else { - t1 = $[0]; + t0 = $[0]; } - t0 = t1; return t0; } function _temp() {} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/dominator.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/dominator.expect.md index e878d4fb7f825..508a7b62581d7 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/dominator.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/dominator.expect.md @@ -67,8 +67,7 @@ function Component(props) { case "b": { break bb1; } - case "c": { - } + case "c": default: { x = 6; } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/dont-memoize-primitive-function-call-non-escaping-useMemo.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/dont-memoize-primitive-function-call-non-escaping-useMemo.expect.md new file mode 100644 index 0000000000000..93b08128a0afe --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/dont-memoize-primitive-function-call-non-escaping-useMemo.expect.md @@ -0,0 +1,77 @@ + +## Input + +```javascript +// @compilationMode:"infer" @enablePreserveExistingMemoizationGuarantees @validatePreserveExistingMemoizationGuarantees +import {useMemo} from 'react'; +import {makeObject_Primitives, ValidateMemoization} from 'shared-runtime'; + +function Component(props) { + const result = useMemo( + () => makeObject(props.value).value + 1, + [props.value] + ); + console.log(result); + return 'ok'; +} + +function makeObject(value) { + console.log(value); + return {value}; +} + +export const TODO_FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{value: 42}], + sequentialRenders: [ + {value: 42}, + {value: 42}, + {value: 3.14}, + {value: 3.14}, + {value: 42}, + {value: 3.14}, + {value: 42}, + {value: 3.14}, + ], +}; + +``` + +## Code + +```javascript +// @compilationMode:"infer" @enablePreserveExistingMemoizationGuarantees @validatePreserveExistingMemoizationGuarantees +import { useMemo } from "react"; +import { makeObject_Primitives, ValidateMemoization } from "shared-runtime"; + +function Component(props) { + const result = makeObject(props.value).value + 1; + + console.log(result); + return "ok"; +} + +function makeObject(value) { + console.log(value); + return { value }; +} + +export const TODO_FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ value: 42 }], + sequentialRenders: [ + { value: 42 }, + { value: 42 }, + { value: 3.14 }, + { value: 3.14 }, + { value: 42 }, + { value: 3.14 }, + { value: 42 }, + { value: 3.14 }, + ], +}; + +``` + +### 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/dont-memoize-primitive-function-call-non-escaping-useMemo.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/dont-memoize-primitive-function-call-non-escaping-useMemo.js new file mode 100644 index 0000000000000..2ee24917c53e6 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/dont-memoize-primitive-function-call-non-escaping-useMemo.js @@ -0,0 +1,32 @@ +// @compilationMode:"infer" @enablePreserveExistingMemoizationGuarantees @validatePreserveExistingMemoizationGuarantees +import {useMemo} from 'react'; +import {makeObject_Primitives, ValidateMemoization} from 'shared-runtime'; + +function Component(props) { + const result = useMemo( + () => makeObject(props.value).value + 1, + [props.value] + ); + console.log(result); + return 'ok'; +} + +function makeObject(value) { + console.log(value); + return {value}; +} + +export const TODO_FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{value: 42}], + sequentialRenders: [ + {value: 42}, + {value: 42}, + {value: 3.14}, + {value: 3.14}, + {value: 42}, + {value: 3.14}, + {value: 42}, + {value: 3.14}, + ], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/dont-memoize-primitive-function-call-non-escaping.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/dont-memoize-primitive-function-call-non-escaping.expect.md new file mode 100644 index 0000000000000..e2f6c9e6c2ccf --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/dont-memoize-primitive-function-call-non-escaping.expect.md @@ -0,0 +1,81 @@ + +## Input + +```javascript +// @compilationMode:"infer" @enablePreserveExistingMemoizationGuarantees @validatePreserveExistingMemoizationGuarantees +import {useMemo} from 'react'; +import {makeObject_Primitives, ValidateMemoization} from 'shared-runtime'; + +function Component(props) { + const result = makeObject(props.value).value + 1; + console.log(result); + return 'ok'; +} + +function makeObject(value) { + console.log(value); + return {value}; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{value: 42}], + sequentialRenders: [ + {value: 42}, + {value: 42}, + {value: 3.14}, + {value: 3.14}, + {value: 42}, + {value: 3.14}, + {value: 42}, + {value: 3.14}, + ], +}; + +``` + +## Code + +```javascript +// @compilationMode:"infer" @enablePreserveExistingMemoizationGuarantees @validatePreserveExistingMemoizationGuarantees +import { useMemo } from "react"; +import { makeObject_Primitives, ValidateMemoization } from "shared-runtime"; + +function Component(props) { + const result = makeObject(props.value).value + 1; + console.log(result); + return "ok"; +} + +function makeObject(value) { + console.log(value); + return { value }; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ value: 42 }], + sequentialRenders: [ + { value: 42 }, + { value: 42 }, + { value: 3.14 }, + { value: 3.14 }, + { value: 42 }, + { value: 3.14 }, + { value: 42 }, + { value: 3.14 }, + ], +}; + +``` + +### Eval output +(kind: ok) "ok" +"ok" +"ok" +"ok" +"ok" +"ok" +"ok" +"ok" +logs: [42,43,42,43,3.14,4.140000000000001,3.14,4.140000000000001,42,43,3.14,4.140000000000001,42,43,3.14,4.140000000000001] \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/dont-memoize-primitive-function-call-non-escaping.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/dont-memoize-primitive-function-call-non-escaping.js new file mode 100644 index 0000000000000..b4d8d344441c9 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/dont-memoize-primitive-function-call-non-escaping.js @@ -0,0 +1,29 @@ +// @compilationMode:"infer" @enablePreserveExistingMemoizationGuarantees @validatePreserveExistingMemoizationGuarantees +import {useMemo} from 'react'; +import {makeObject_Primitives, ValidateMemoization} from 'shared-runtime'; + +function Component(props) { + const result = makeObject(props.value).value + 1; + console.log(result); + return 'ok'; +} + +function makeObject(value) { + console.log(value); + return {value}; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{value: 42}], + sequentialRenders: [ + {value: 42}, + {value: 42}, + {value: 3.14}, + {value: 3.14}, + {value: 42}, + {value: 3.14}, + {value: 42}, + {value: 3.14}, + ], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/drop-methodcall-usememo.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/drop-methodcall-usememo.expect.md index 89041ed0b1711..29d581df9c335 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/drop-methodcall-usememo.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/drop-methodcall-usememo.expect.md @@ -28,7 +28,6 @@ import * as React from "react"; function Component(props) { const $ = _c(2); - let t0; let x; if ($[0] !== props.value) { x = []; @@ -38,8 +37,7 @@ function Component(props) { } else { x = $[1]; } - t0 = x; - const x_0 = t0; + const x_0 = x; return x_0; } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ecma/error.reserved-words.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ecma/error.reserved-words.expect.md new file mode 100644 index 0000000000000..986fb8a5b26f1 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ecma/error.reserved-words.expect.md @@ -0,0 +1,41 @@ + +## Input + +```javascript +import {useRef} from 'react'; + +function useThing(fn) { + const fnRef = useRef(fn); + const ref = useRef(null); + + if (ref.current === null) { + ref.current = function (this: unknown, ...args) { + return fnRef.current.call(this, ...args); + }; + } + return ref.current; +} + +``` + + +## Error + +``` +Found 1 error: + +Error: `this` is not supported syntax + +React Compiler does not support compiling functions that use `this` + +error.reserved-words.ts:8:28 + 6 | + 7 | if (ref.current === null) { +> 8 | ref.current = function (this: unknown, ...args) { + | ^^^^^^^^^^^^^ `this` was used here + 9 | return fnRef.current.call(this, ...args); + 10 | }; + 11 | } +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ecma/error.reserved-words.ts b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ecma/error.reserved-words.ts new file mode 100644 index 0000000000000..2937ba8df5e28 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ecma/error.reserved-words.ts @@ -0,0 +1,13 @@ +import {useRef} from 'react'; + +function useThing(fn) { + const fnRef = useRef(fn); + const ref = useRef(null); + + if (ref.current === null) { + ref.current = function (this: unknown, ...args) { + return fnRef.current.call(this, ...args); + }; + } + return ref.current; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error._todo.computed-lval-in-destructure.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error._todo.computed-lval-in-destructure.expect.md index f44ae83b2cee0..2d633a3d0fdd9 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error._todo.computed-lval-in-destructure.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error._todo.computed-lval-in-destructure.expect.md @@ -15,10 +15,15 @@ function Component(props) { ## Error ``` +Found 1 error: + +Todo: (BuildHIR::lowerAssignment) Handle computed properties in ObjectPattern + +error._todo.computed-lval-in-destructure.ts:3:9 1 | function Component(props) { 2 | const computedKey = props.key; > 3 | const {[computedKey]: x} = props.val; - | ^^^^^^^^^^^^^^^^ Todo: (BuildHIR::lowerAssignment) Handle computed properties in ObjectPattern (3:3) + | ^^^^^^^^^^^^^^^^ (BuildHIR::lowerAssignment) Handle computed properties in ObjectPattern 4 | 5 | return x; 6 | } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.assign-global-in-component-tag-function.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.assign-global-in-component-tag-function.expect.md index 5553f235a0847..ce42e651259c7 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.assign-global-in-component-tag-function.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.assign-global-in-component-tag-function.expect.md @@ -15,10 +15,17 @@ function Component() { ## Error ``` +Found 1 error: + +Error: Cannot reassign variables declared outside of the component/hook + +Variable `someGlobal` is declared outside of the component/hook. Reassigning this value during render is a form of side effect, which can cause unpredictable behavior depending on when the component happens to re-render. If this variable is used in rendering, use useState instead. Otherwise, consider updating it in an effect. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#side-effects-must-run-outside-of-render) + +error.assign-global-in-component-tag-function.ts:3:4 1 | function Component() { 2 | const Foo = () => { > 3 | someGlobal = true; - | ^^^^^^^^^^ InvalidReact: Unexpected reassignment of a variable which was defined outside of the component. Components and hooks should be pure and side-effect free, but variable reassignment is a form of side-effect. If this variable is used in rendering, use useState instead. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#side-effects-must-run-outside-of-render) (3:3) + | ^^^^^^^^^^ `someGlobal` cannot be reassigned 4 | }; 5 | return ; 6 | } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.assign-global-in-jsx-children.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.assign-global-in-jsx-children.expect.md index d380137836cc1..ee57ea6eb0cd9 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.assign-global-in-jsx-children.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.assign-global-in-jsx-children.expect.md @@ -18,10 +18,17 @@ function Component() { ## Error ``` +Found 1 error: + +Error: Cannot reassign variables declared outside of the component/hook + +Variable `someGlobal` is declared outside of the component/hook. Reassigning this value during render is a form of side effect, which can cause unpredictable behavior depending on when the component happens to re-render. If this variable is used in rendering, use useState instead. Otherwise, consider updating it in an effect. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#side-effects-must-run-outside-of-render) + +error.assign-global-in-jsx-children.ts:3:4 1 | function Component() { 2 | const foo = () => { > 3 | someGlobal = true; - | ^^^^^^^^^^ InvalidReact: Unexpected reassignment of a variable which was defined outside of the component. Components and hooks should be pure and side-effect free, but variable reassignment is a form of side-effect. If this variable is used in rendering, use useState instead. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#side-effects-must-run-outside-of-render) (3:3) + | ^^^^^^^^^^ `someGlobal` cannot be reassigned 4 | }; 5 | // Children are generally access/called during render, so 6 | // modifying a global in a children function is almost diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.assign-global-in-jsx-spread-attribute.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.assign-global-in-jsx-spread-attribute.expect.md deleted file mode 100644 index 3861b16e90dcf..0000000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.assign-global-in-jsx-spread-attribute.expect.md +++ /dev/null @@ -1,27 +0,0 @@ - -## Input - -```javascript -function Component() { - const foo = () => { - someGlobal = true; - }; - return
; -} - -``` - - -## Error - -``` - 1 | function Component() { - 2 | const foo = () => { -> 3 | someGlobal = true; - | ^^^^^^^^^^ InvalidReact: Unexpected reassignment of a variable which was defined outside of the component. Components and hooks should be pure and side-effect free, but variable reassignment is a form of side-effect. If this variable is used in rendering, use useState instead. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#side-effects-must-run-outside-of-render) (3:3) - 4 | }; - 5 | return
; - 6 | } -``` - - \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.assign-global-in-jsx-spread-attribute.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.assign-global-in-jsx-spread-attribute.js deleted file mode 100644 index 1eea9267b5098..0000000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.assign-global-in-jsx-spread-attribute.js +++ /dev/null @@ -1,6 +0,0 @@ -function Component() { - const foo = () => { - someGlobal = true; - }; - return
; -} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.assign-ref-in-effect-hint.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.assign-ref-in-effect-hint.expect.md new file mode 100644 index 0000000000000..c89e773f3212c --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.assign-ref-in-effect-hint.expect.md @@ -0,0 +1,37 @@ + +## Input + +```javascript +// Fixture to test that we show a hint to name as `ref` or `-Ref` when attempting +// to assign .current inside an effect +function Component({foo}) { + useEffect(() => { + foo.current = true; + }, [foo]); +} + +``` + + +## Error + +``` +Found 1 error: + +Error: This value cannot be modified + +Modifying component props or hook arguments is not allowed. Consider using a local variable instead. + +error.assign-ref-in-effect-hint.ts:5:4 + 3 | function Component({foo}) { + 4 | useEffect(() => { +> 5 | foo.current = true; + | ^^^ `foo` cannot be modified + 6 | }, [foo]); + 7 | } + 8 | + +Hint: If this value is a Ref (value returned by `useRef()`), rename the variable to end in "Ref". +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.assign-ref-in-effect-hint.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.assign-ref-in-effect-hint.js new file mode 100644 index 0000000000000..1546734959648 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.assign-ref-in-effect-hint.js @@ -0,0 +1,7 @@ +// Fixture to test that we show a hint to name as `ref` or `-Ref` when attempting +// to assign .current inside an effect +function Component({foo}) { + useEffect(() => { + foo.current = true; + }, [foo]); +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bailout-on-flow-suppression.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bailout-on-flow-suppression.expect.md index 1d5b4abdf7d42..6e522e16669dc 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bailout-on-flow-suppression.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bailout-on-flow-suppression.expect.md @@ -16,10 +16,17 @@ function Foo(props) { ## Error ``` +Found 1 error: + +Error: React Compiler has skipped optimizing this component because one or more React rule violations were reported by Flow + +React Compiler only works when your components follow all the rules of React, disabling them may result in unexpected or incorrect behavior. Found suppression `$FlowFixMe[react-rule-hook]` + +error.bailout-on-flow-suppression.ts:4:2 2 | 3 | function Foo(props) { > 4 | // $FlowFixMe[react-rule-hook] - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ InvalidReact: React Compiler has skipped optimizing this component because one or more React rule violations were reported by Flow. React Compiler only works when your components follow all the rules of React, disabling them may result in unexpected or incorrect behavior. $FlowFixMe[react-rule-hook] (4:4) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Found React rule suppression 5 | useX(); 6 | return null; 7 | } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bailout-on-suppression-of-custom-rule.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bailout-on-suppression-of-custom-rule.expect.md index d74ebd119c345..3221f97731d9a 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bailout-on-suppression-of-custom-rule.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bailout-on-suppression-of-custom-rule.expect.md @@ -19,15 +19,33 @@ function lowercasecomponent() { ## Error ``` +Found 2 errors: + +Error: React Compiler has skipped optimizing this component because one or more React ESLint rules were disabled + +React Compiler only works when your components follow all the rules of React, disabling them may result in unexpected or incorrect behavior. Found suppression `eslint-disable my-app/react-rule` + +error.bailout-on-suppression-of-custom-rule.ts:3:0 1 | // @eslintSuppressionRules:["my-app","react-rule"] 2 | > 3 | /* eslint-disable my-app/react-rule */ - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ InvalidReact: React Compiler has skipped optimizing this component because one or more React ESLint rules were disabled. React Compiler only works when your components follow all the rules of React, disabling them may result in unexpected or incorrect behavior. eslint-disable my-app/react-rule (3:3) - -InvalidReact: React Compiler has skipped optimizing this component because one or more React ESLint rules were disabled. React Compiler only works when your components follow all the rules of React, disabling them may result in unexpected or incorrect behavior. eslint-disable-next-line my-app/react-rule (7:7) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Found React rule suppression 4 | function lowercasecomponent() { 5 | 'use forget'; 6 | const x = []; + +Error: React Compiler has skipped optimizing this component because one or more React ESLint rules were disabled + +React Compiler only works when your components follow all the rules of React, disabling them may result in unexpected or incorrect behavior. Found suppression `eslint-disable-next-line my-app/react-rule` + +error.bailout-on-suppression-of-custom-rule.ts:7:2 + 5 | 'use forget'; + 6 | const x = []; +> 7 | // eslint-disable-next-line my-app/react-rule + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Found React rule suppression + 8 | return
{x}
; + 9 | } + 10 | /* eslint-enable my-app/react-rule */ ``` \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-infer-mutation-aliasing-effects.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-infer-mutation-aliasing-effects.expect.md new file mode 100644 index 0000000000000..a1c64e50483f8 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-infer-mutation-aliasing-effects.expect.md @@ -0,0 +1,46 @@ + +## Input + +```javascript +import {useCallback, useRef} from 'react'; + +export default function useThunkDispatch(state, dispatch, extraArg) { + const stateRef = useRef(state); + stateRef.current = state; + + return useCallback( + function thunk(action) { + if (typeof action === 'function') { + return action(thunk, () => stateRef.current, extraArg); + } else { + dispatch(action); + return undefined; + } + }, + [dispatch, extraArg] + ); +} + +``` + + +## Error + +``` +Found 1 error: + +Invariant: [InferMutationAliasingEffects] Expected value kind to be initialized + + thunk$14. + +error.bug-infer-mutation-aliasing-effects.ts:10:22 + 8 | function thunk(action) { + 9 | if (typeof action === 'function') { +> 10 | return action(thunk, () => stateRef.current, extraArg); + | ^^^^^ [InferMutationAliasingEffects] Expected value kind to be initialized + 11 | } else { + 12 | dispatch(action); + 13 | return undefined; +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-infer-mutation-aliasing-effects.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-infer-mutation-aliasing-effects.js new file mode 100644 index 0000000000000..3309406fc70ca --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-infer-mutation-aliasing-effects.js @@ -0,0 +1,18 @@ +import {useCallback, useRef} from 'react'; + +export default function useThunkDispatch(state, dispatch, extraArg) { + const stateRef = useRef(state); + stateRef.current = state; + + return useCallback( + function thunk(action) { + if (typeof action === 'function') { + return action(thunk, () => stateRef.current, extraArg); + } else { + dispatch(action); + return undefined; + } + }, + [dispatch, extraArg] + ); +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-invariant-codegen-methodcall.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-invariant-codegen-methodcall.expect.md new file mode 100644 index 0000000000000..4ea831de8751e --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-invariant-codegen-methodcall.expect.md @@ -0,0 +1,31 @@ + +## Input + +```javascript +const YearsAndMonthsSince = () => { + const diff = foo(); + const months = Math.floor(diff.bar()); + return <>{months}; +}; + +``` + + +## Error + +``` +Found 1 error: + +Invariant: [Codegen] Internal error: MethodCall::property must be an unpromoted + unmemoized MemberExpression. Got a `Identifier` + +error.bug-invariant-codegen-methodcall.ts:3:17 + 1 | const YearsAndMonthsSince = () => { + 2 | const diff = foo(); +> 3 | const months = Math.floor(diff.bar()); + | ^^^^^^^^^^ [Codegen] Internal error: MethodCall::property must be an unpromoted + unmemoized MemberExpression. Got a `Identifier` + 4 | return <>{months}; + 5 | }; + 6 | +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-invariant-codegen-methodcall.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-invariant-codegen-methodcall.js new file mode 100644 index 0000000000000..948182653cbe0 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-invariant-codegen-methodcall.js @@ -0,0 +1,5 @@ +const YearsAndMonthsSince = () => { + const diff = foo(); + const months = Math.floor(diff.bar()); + return <>{months}; +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-invariant-couldnt-find-binding-for-decl.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-invariant-couldnt-find-binding-for-decl.expect.md new file mode 100644 index 0000000000000..b50ad7035939e --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-invariant-couldnt-find-binding-for-decl.expect.md @@ -0,0 +1,37 @@ + +## Input + +```javascript +import {useEffect} from 'react'; + +export function Foo() { + useEffect(() => { + try { + // do something + } catch ({status}) { + // do something + } + }, []); +} + +``` + + +## Error + +``` +Found 1 error: + +Invariant: (BuildHIR::lowerAssignment) Could not find binding for declaration. + +error.bug-invariant-couldnt-find-binding-for-decl.ts:7:14 + 5 | try { + 6 | // do something +> 7 | } catch ({status}) { + | ^^^^^^ (BuildHIR::lowerAssignment) Could not find binding for declaration. + 8 | // do something + 9 | } + 10 | }, []); +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-invariant-couldnt-find-binding-for-decl.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-invariant-couldnt-find-binding-for-decl.js new file mode 100644 index 0000000000000..c005fec1bd5a5 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-invariant-couldnt-find-binding-for-decl.js @@ -0,0 +1,11 @@ +import {useEffect} from 'react'; + +export function Foo() { + useEffect(() => { + try { + // do something + } catch ({status}) { + // do something + } + }, []); +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-invariant-expected-break-target.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-invariant-expected-break-target.expect.md new file mode 100644 index 0000000000000..226ab20ac269b --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-invariant-expected-break-target.expect.md @@ -0,0 +1,32 @@ + +## Input + +```javascript +import {useMemo} from 'react'; + +export default function useFoo(text) { + return useMemo(() => { + try { + let formattedText = ''; + try { + formattedText = format(text); + } catch { + console.log('error'); + } + return formattedText || ''; + } catch (e) {} + }, [text]); +} + +``` + + +## Error + +``` +Found 1 error: + +Invariant: Expected a break target +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-invariant-expected-break-target.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-invariant-expected-break-target.js new file mode 100644 index 0000000000000..4616e0232aafe --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-invariant-expected-break-target.js @@ -0,0 +1,15 @@ +import {useMemo} from 'react'; + +export default function useFoo(text) { + return useMemo(() => { + try { + let formattedText = ''; + try { + formattedText = format(text); + } catch { + console.log('error'); + } + return formattedText || ''; + } catch (e) {} + }, [text]); +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-invariant-expected-consistent-destructuring.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-invariant-expected-consistent-destructuring.expect.md new file mode 100644 index 0000000000000..a30ccffcd7bd0 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-invariant-expected-consistent-destructuring.expect.md @@ -0,0 +1,44 @@ + +## Input + +```javascript +import {useMemo} from 'react'; +import {useFoo, formatB, Baz} from './lib'; + +export const Example = ({data}) => { + let a; + let b; + + if (data) { + ({a, b} = data); + } + + const foo = useFoo(a); + const bar = useMemo(() => formatB(b), [b]); + + return ; +}; + +``` + + +## Error + +``` +Found 1 error: + +Invariant: Expected consistent kind for destructuring + +Other places were `Reassign` but 'mutate? #t8$46[7:9]{reactive}' is const. + +error.bug-invariant-expected-consistent-destructuring.ts:9:9 + 7 | + 8 | if (data) { +> 9 | ({a, b} = data); + | ^ Expected consistent kind for destructuring + 10 | } + 11 | + 12 | const foo = useFoo(a); +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-invariant-expected-consistent-destructuring.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-invariant-expected-consistent-destructuring.js new file mode 100644 index 0000000000000..c37b19314431b --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-invariant-expected-consistent-destructuring.js @@ -0,0 +1,16 @@ +import {useMemo} from 'react'; +import {useFoo, formatB, Baz} from './lib'; + +export const Example = ({data}) => { + let a; + let b; + + if (data) { + ({a, b} = data); + } + + const foo = useFoo(a); + const bar = useMemo(() => formatB(b), [b]); + + return ; +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-invariant-local-or-context-references.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-invariant-local-or-context-references.expect.md new file mode 100644 index 0000000000000..bbf753f965091 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-invariant-local-or-context-references.expect.md @@ -0,0 +1,46 @@ + +## Input + +```javascript +import {useState} from 'react'; +import {bar} from './bar'; + +export const useFoot = () => { + const [, setState] = useState(null); + try { + const {data} = bar(); + setState({ + data, + error: null, + }); + } catch (err) { + setState(_prevState => ({ + loading: false, + error: err, + })); + } +}; + +``` + + +## Error + +``` +Found 1 error: + +Invariant: Expected all references to a variable to be consistently local or context references + +Identifier err$7 is referenced as a context variable, but was previously referenced as a [object Object] variable. + +error.bug-invariant-local-or-context-references.ts:15:13 + 13 | setState(_prevState => ({ + 14 | loading: false, +> 15 | error: err, + | ^^^ Expected all references to a variable to be consistently local or context references + 16 | })); + 17 | } + 18 | }; +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-invariant-local-or-context-references.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-invariant-local-or-context-references.js new file mode 100644 index 0000000000000..561bc25fb72ad --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-invariant-local-or-context-references.js @@ -0,0 +1,18 @@ +import {useState} from 'react'; +import {bar} from './bar'; + +export const useFoot = () => { + const [, setState] = useState(null); + try { + const {data} = bar(); + setState({ + data, + error: null, + }); + } catch (err) { + setState(_prevState => ({ + loading: false, + error: err, + })); + } +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-invariant-unexpected-terminal-in-optional.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-invariant-unexpected-terminal-in-optional.expect.md new file mode 100644 index 0000000000000..743d2b9071e6a --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-invariant-unexpected-terminal-in-optional.expect.md @@ -0,0 +1,34 @@ + +## Input + +```javascript +const Foo = ({json}) => { + try { + const foo = JSON.parse(json)?.foo; + return {foo}; + } catch { + return null; + } +}; + +``` + + +## Error + +``` +Found 1 error: + +Invariant: Unexpected terminal in optional + +error.bug-invariant-unexpected-terminal-in-optional.ts:3:16 + 1 | const Foo = ({json}) => { + 2 | try { +> 3 | const foo = JSON.parse(json)?.foo; + | ^^^^ Unexpected terminal in optional + 4 | return {foo}; + 5 | } catch { + 6 | return null; +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-invariant-unexpected-terminal-in-optional.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-invariant-unexpected-terminal-in-optional.js new file mode 100644 index 0000000000000..961640bfbd387 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-invariant-unexpected-terminal-in-optional.js @@ -0,0 +1,8 @@ +const Foo = ({json}) => { + try { + const foo = JSON.parse(json)?.foo; + return {foo}; + } catch { + return null; + } +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-invariant-unnamed-temporary.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-invariant-unnamed-temporary.expect.md new file mode 100644 index 0000000000000..f8c46659bf7c2 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-invariant-unnamed-temporary.expect.md @@ -0,0 +1,30 @@ + +## Input + +```javascript +import Bar from './Bar'; + +export function Foo() { + return ( + { + return {displayValue}; + }} + /> + ); +} + +``` + + +## Error + +``` +Found 1 error: + +Invariant: Expected temporaries to be promoted to named identifiers in an earlier pass + +identifier 15 is unnamed. +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-invariant-unnamed-temporary.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-invariant-unnamed-temporary.js new file mode 100644 index 0000000000000..4a06093d9f1ef --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-invariant-unnamed-temporary.js @@ -0,0 +1,11 @@ +import Bar from './Bar'; + +export function Foo() { + return ( + { + return {displayValue}; + }} + /> + ); +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.call-args-destructuring-asignment-complex.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.call-args-destructuring-asignment-complex.expect.md index cb2ce1a20df0a..624bc8b0b571c 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.call-args-destructuring-asignment-complex.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.call-args-destructuring-asignment-complex.expect.md @@ -14,10 +14,15 @@ function Component(props) { ## Error ``` +Found 1 error: + +Invariant: Const declaration cannot be referenced as an expression + +error.call-args-destructuring-asignment-complex.ts:3:9 1 | function Component(props) { 2 | let x = makeObject(); > 3 | x.foo(([[x]] = makeObject())); - | ^^^^^ Invariant: Const declaration cannot be referenced as an expression (3:3) + | ^^^^^ Const declaration cannot be referenced as an expression 4 | return x; 5 | } 6 | diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.capitalized-function-call-aliased.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.capitalized-function-call-aliased.expect.md index 94b3ae1035ea2..499f2dd873972 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.capitalized-function-call-aliased.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.capitalized-function-call-aliased.expect.md @@ -14,10 +14,17 @@ function Foo() { ## Error ``` +Found 1 error: + +Error: Capitalized functions are reserved for components, which must be invoked with JSX. If this is a component, render it with JSX. Otherwise, ensure that it has no hook calls and rename it to begin with a lowercase letter. Alternatively, if you know for a fact that this function is not a component, you can allowlist it via the compiler config + +Bar may be a component.. + +error.capitalized-function-call-aliased.ts:4:2 2 | function Foo() { 3 | let x = Bar; > 4 | x(); // ERROR - | ^^^ InvalidReact: Capitalized functions are reserved for components, which must be invoked with JSX. If this is a component, render it with JSX. Otherwise, ensure that it has no hook calls and rename it to begin with a lowercase letter. Alternatively, if you know for a fact that this function is not a component, you can allowlist it via the compiler config. Bar may be a component. (4:4) + | ^^^ Capitalized functions are reserved for components, which must be invoked with JSX. If this is a component, render it with JSX. Otherwise, ensure that it has no hook calls and rename it to begin with a lowercase letter. Alternatively, if you know for a fact that this function is not a component, you can allowlist it via the compiler config 5 | } 6 | ``` diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.capitalized-function-call.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.capitalized-function-call.expect.md index d8b0f8facfed0..a89efa7c451d8 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.capitalized-function-call.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.capitalized-function-call.expect.md @@ -15,10 +15,17 @@ function Component() { ## Error ``` +Found 1 error: + +Error: Capitalized functions are reserved for components, which must be invoked with JSX. If this is a component, render it with JSX. Otherwise, ensure that it has no hook calls and rename it to begin with a lowercase letter. Alternatively, if you know for a fact that this function is not a component, you can allowlist it via the compiler config + +SomeFunc may be a component.. + +error.capitalized-function-call.ts:3:12 1 | // @validateNoCapitalizedCalls 2 | function Component() { > 3 | const x = SomeFunc(); - | ^^^^^^^^^^ InvalidReact: Capitalized functions are reserved for components, which must be invoked with JSX. If this is a component, render it with JSX. Otherwise, ensure that it has no hook calls and rename it to begin with a lowercase letter. Alternatively, if you know for a fact that this function is not a component, you can allowlist it via the compiler config. SomeFunc may be a component. (3:3) + | ^^^^^^^^^^ Capitalized functions are reserved for components, which must be invoked with JSX. If this is a component, render it with JSX. Otherwise, ensure that it has no hook calls and rename it to begin with a lowercase letter. Alternatively, if you know for a fact that this function is not a component, you can allowlist it via the compiler config 4 | 5 | return x; 6 | } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.capitalized-method-call.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.capitalized-method-call.expect.md index 39dc43e4a563c..c957e5bf7a555 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.capitalized-method-call.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.capitalized-method-call.expect.md @@ -15,10 +15,17 @@ function Component() { ## Error ``` +Found 1 error: + +Error: Capitalized functions are reserved for components, which must be invoked with JSX. If this is a component, render it with JSX. Otherwise, ensure that it has no hook calls and rename it to begin with a lowercase letter. Alternatively, if you know for a fact that this function is not a component, you can allowlist it via the compiler config + +SomeFunc may be a component.. + +error.capitalized-method-call.ts:3:12 1 | // @validateNoCapitalizedCalls 2 | function Component() { > 3 | const x = someGlobal.SomeFunc(); - | ^^^^^^^^^^^^^^^^^^^^^ InvalidReact: Capitalized functions are reserved for components, which must be invoked with JSX. If this is a component, render it with JSX. Otherwise, ensure that it has no hook calls and rename it to begin with a lowercase letter. Alternatively, if you know for a fact that this function is not a component, you can allowlist it via the compiler config. SomeFunc may be a component. (3:3) + | ^^^^^^^^^^^^^^^^^^^^^ Capitalized functions are reserved for components, which must be invoked with JSX. If this is a component, render it with JSX. Otherwise, ensure that it has no hook calls and rename it to begin with a lowercase letter. Alternatively, if you know for a fact that this function is not a component, you can allowlist it via the compiler config 4 | 5 | return x; 6 | } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.capture-ref-for-mutation.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.capture-ref-for-mutation.expect.md index cff34e3449376..cb2256a187fac 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.capture-ref-for-mutation.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.capture-ref-for-mutation.expect.md @@ -32,19 +32,33 @@ export const FIXTURE_ENTRYPOINT = { ## Error ``` +Found 2 errors: + +Error: Cannot access refs during render + +React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef) + +error.capture-ref-for-mutation.ts:12:13 10 | }; 11 | const moveLeft = { > 12 | handler: handleKey('left')(), - | ^^^^^^^^^^^^^^^^^ InvalidReact: This function accesses a ref value (the `current` property), which may not be accessed during render. (https://react.dev/reference/react/useRef) (12:12) + | ^^^^^^^^^^^^^^^^^ This function accesses a ref value + 13 | }; + 14 | const moveRight = { + 15 | handler: handleKey('right')(), -InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (12:12) +Error: Cannot access refs during render -InvalidReact: This function accesses a ref value (the `current` property), which may not be accessed during render. (https://react.dev/reference/react/useRef) (15:15) +React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef) -InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (15:15) +error.capture-ref-for-mutation.ts:15:13 13 | }; 14 | const moveRight = { - 15 | handler: handleKey('right')(), +> 15 | handler: handleKey('right')(), + | ^^^^^^^^^^^^^^^^^^ This function accesses a ref value + 16 | }; + 17 | return [moveLeft, moveRight]; + 18 | } ``` \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.conditional-hook-unknown-hook-react-namespace.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.conditional-hook-unknown-hook-react-namespace.expect.md index 7ea8ae9809370..fbf5ca665b4f7 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.conditional-hook-unknown-hook-react-namespace.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.conditional-hook-unknown-hook-react-namespace.expect.md @@ -16,10 +16,15 @@ function Component(props) { ## Error ``` +Found 1 error: + +Error: Hooks must always be called in a consistent order, and may not be called conditionally. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning) + +error.conditional-hook-unknown-hook-react-namespace.ts:4:8 2 | let x = null; 3 | if (props.cond) { > 4 | x = React.useNonexistentHook(); - | ^^^^^^^^^^^^^^^^^^^^^^^^ InvalidReact: Hooks must always be called in a consistent order, and may not be called conditionally. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning) (4:4) + | ^^^^^^^^^^^^^^^^^^^^^^^^ Hooks must always be called in a consistent order, and may not be called conditionally. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning) 5 | } 6 | return x; 7 | } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.conditional-hooks-as-method-call.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.conditional-hooks-as-method-call.expect.md index c2ad547414aa9..2f8806787d2ee 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.conditional-hooks-as-method-call.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.conditional-hooks-as-method-call.expect.md @@ -16,10 +16,15 @@ function Component(props) { ## Error ``` +Found 1 error: + +Error: Hooks must always be called in a consistent order, and may not be called conditionally. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning) + +error.conditional-hooks-as-method-call.ts:4:8 2 | let x = null; 3 | if (props.cond) { > 4 | x = Foo.useFoo(); - | ^^^^^^^^^^ InvalidReact: Hooks must always be called in a consistent order, and may not be called conditionally. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning) (4:4) + | ^^^^^^^^^^ Hooks must always be called in a consistent order, and may not be called conditionally. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning) 5 | } 6 | return x; 7 | } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.context-variable-only-chained-assign.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.context-variable-only-chained-assign.expect.md index 0318fa9525fda..6e9887c5ac521 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.context-variable-only-chained-assign.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.context-variable-only-chained-assign.expect.md @@ -28,10 +28,17 @@ export const FIXTURE_ENTRYPOINT = { ## Error ``` +Found 1 error: + +Error: Cannot reassign variable after render completes + +Reassigning `x` after render has completed can cause inconsistent behavior on subsequent renders. Consider using state instead. + +error.context-variable-only-chained-assign.ts:10:19 8 | }; 9 | const fn2 = () => { > 10 | const copy2 = (x = 4); - | ^ InvalidReact: Reassigning a variable after render has completed can cause inconsistent behavior on subsequent renders. Consider using state instead. Variable `x` cannot be reassigned after render (10:10) + | ^ Cannot reassign `x` after render completes 11 | return [invoke(fn1), copy2, identity(copy2)]; 12 | }; 13 | return invoke(fn2); diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.declare-reassign-variable-in-function-declaration.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.declare-reassign-variable-in-function-declaration.expect.md index 2a6dce11f242e..e5c28e6e362f4 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.declare-reassign-variable-in-function-declaration.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.declare-reassign-variable-in-function-declaration.expect.md @@ -17,10 +17,17 @@ function Component() { ## Error ``` +Found 1 error: + +Error: Cannot reassign variable after render completes + +Reassigning `x` after render has completed can cause inconsistent behavior on subsequent renders. Consider using state instead. + +error.declare-reassign-variable-in-function-declaration.ts:4:4 2 | let x = null; 3 | function foo() { > 4 | x = 9; - | ^ InvalidReact: Reassigning a variable after render has completed can cause inconsistent behavior on subsequent renders. Consider using state instead. Variable `x` cannot be reassigned after render (4:4) + | ^ Cannot reassign `x` after render completes 5 | } 6 | const y = bar(foo); 7 | return ; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.default-param-accesses-local.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.default-param-accesses-local.expect.md index dbf084466d80d..4bf9a06b6e979 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.default-param-accesses-local.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.default-param-accesses-local.expect.md @@ -22,6 +22,11 @@ export const FIXTURE_ENTRYPOINT = { ## Error ``` +Found 1 error: + +Todo: (BuildHIR::node.lowerReorderableExpression) Expression type `ArrowFunctionExpression` cannot be safely reordered + +error.default-param-accesses-local.ts:3:6 1 | function Component( 2 | x, > 3 | y = () => { @@ -29,7 +34,7 @@ export const FIXTURE_ENTRYPOINT = { > 4 | return x; | ^^^^^^^^^^^^^ > 5 | } - | ^^^^ Todo: (BuildHIR::node.lowerReorderableExpression) Expression type `ArrowFunctionExpression` cannot be safely reordered (3:5) + | ^^^^ (BuildHIR::node.lowerReorderableExpression) Expression type `ArrowFunctionExpression` cannot be safely reordered 6 | ) { 7 | return y(); 8 | } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.dont-hoist-inline-reference.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.dont-hoist-inline-reference.expect.md index b08d151be64c4..00a68405f8a1f 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.dont-hoist-inline-reference.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.dont-hoist-inline-reference.expect.md @@ -19,10 +19,17 @@ export const FIXTURE_ENTRYPOINT = { ## Error ``` +Found 1 error: + +Todo: [hoisting] EnterSSA: Expected identifier to be defined before being used + +Identifier x$1 is undefined. + +error.dont-hoist-inline-reference.ts:3:2 1 | import {identity} from 'shared-runtime'; 2 | function useInvalid() { > 3 | const x = identity(x); - | ^^^^^^^^^^^^^^^^^^^^^^ Todo: [hoisting] EnterSSA: Expected identifier to be defined before being used. Identifier x$1 is undefined (3:3) + | ^^^^^^^^^^^^^^^^^^^^^^ [hoisting] EnterSSA: Expected identifier to be defined before being used 4 | return x; 5 | } 6 | diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.emit-freeze-conflicting-global.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.emit-freeze-conflicting-global.expect.md index a54cc98708f1e..d8436fa2c046e 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.emit-freeze-conflicting-global.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.emit-freeze-conflicting-global.expect.md @@ -15,10 +15,17 @@ function useFoo(props) { ## Error ``` +Found 1 error: + +Todo: Encountered conflicting global in generated program + +Conflict from local binding __DEV__. + +error.emit-freeze-conflicting-global.ts:3:8 1 | // @enableEmitFreeze @instrumentForget 2 | function useFoo(props) { > 3 | const __DEV__ = 'conflicting global'; - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Todo: Encountered conflicting global in generated program. Conflict from local binding __DEV__ (3:3) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Encountered conflicting global in generated program 4 | console.log(__DEV__); 5 | return foo(props.x); 6 | } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.function-expression-references-variable-its-assigned-to.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.function-expression-references-variable-its-assigned-to.expect.md index 76ac6d77a2774..a8a83f6b116da 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.function-expression-references-variable-its-assigned-to.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.function-expression-references-variable-its-assigned-to.expect.md @@ -15,10 +15,17 @@ function Component() { ## Error ``` +Found 1 error: + +Error: Cannot reassign variable after render completes + +Reassigning `callback` after render has completed can cause inconsistent behavior on subsequent renders. Consider using state instead. + +error.function-expression-references-variable-its-assigned-to.ts:3:4 1 | function Component() { 2 | let callback = () => { > 3 | callback = null; - | ^^^^^^^^ InvalidReact: Reassigning a variable after render has completed can cause inconsistent behavior on subsequent renders. Consider using state instead. Variable `callback` cannot be reassigned after render (3:3) + | ^^^^^^^^ Cannot reassign `callback` after render completes 4 | }; 5 | return
; 6 | } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.hoist-optional-member-expression-with-conditional-optional.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.hoist-optional-member-expression-with-conditional-optional.expect.md index 048fee7ee1d58..7913666aa31b4 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.hoist-optional-member-expression-with-conditional-optional.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.hoist-optional-member-expression-with-conditional-optional.expect.md @@ -24,6 +24,13 @@ function Component(props) { ## Error ``` +Found 1 error: + +Compilation Skipped: Existing memoization could not be preserved + +React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected. The inferred dependency was `props.items`, but the source dependencies were [props?.items, props.cond]. Inferred different dependency than source. + +error.hoist-optional-member-expression-with-conditional-optional.ts:4:23 2 | import {ValidateMemoization} from 'shared-runtime'; 3 | function Component(props) { > 4 | const data = useMemo(() => { @@ -41,7 +48,7 @@ function Component(props) { > 10 | return x; | ^^^^^^^^^^^^^^^^^ > 11 | }, [props?.items, props.cond]); - | ^^^^ CannotPreserveMemoization: React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected. The inferred dependency was `props.items`, but the source dependencies were [props?.items, props.cond]. Inferred different dependency than source (4:11) + | ^^^^ Could not preserve existing manual memoization 12 | return ( 13 | 14 | ); diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.hoist-optional-member-expression-with-conditional.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.hoist-optional-member-expression-with-conditional.expect.md index ca3ee2ae1388e..b60a91187552f 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.hoist-optional-member-expression-with-conditional.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.hoist-optional-member-expression-with-conditional.expect.md @@ -24,6 +24,13 @@ function Component(props) { ## Error ``` +Found 1 error: + +Compilation Skipped: Existing memoization could not be preserved + +React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected. The inferred dependency was `props.items`, but the source dependencies were [props?.items, props.cond]. Inferred different dependency than source. + +error.hoist-optional-member-expression-with-conditional.ts:4:23 2 | import {ValidateMemoization} from 'shared-runtime'; 3 | function Component(props) { > 4 | const data = useMemo(() => { @@ -41,7 +48,7 @@ function Component(props) { > 10 | return x; | ^^^^^^^^^^^^^^^^^ > 11 | }, [props?.items, props.cond]); - | ^^^^ CannotPreserveMemoization: React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected. The inferred dependency was `props.items`, but the source dependencies were [props?.items, props.cond]. Inferred different dependency than source (4:11) + | ^^^^ Could not preserve existing manual memoization 12 | return ( 13 | 14 | ); diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.hoisting-simple-function-declaration.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.hoisting-simple-function-declaration.expect.md index 1ba0d59e17265..d323477c175dc 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.hoisting-simple-function-declaration.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.hoisting-simple-function-declaration.expect.md @@ -24,6 +24,11 @@ export const FIXTURE_ENTRYPOINT = { ## Error ``` +Found 1 error: + +Todo: Support functions with unreachable code that may contain hoisted declarations + +error.hoisting-simple-function-declaration.ts:6:2 4 | } 5 | return baz(); // OK: FuncDecls are HoistableDeclarations that have both declaration and value hoisting > 6 | function baz() { @@ -31,7 +36,7 @@ export const FIXTURE_ENTRYPOINT = { > 7 | return bar(); | ^^^^^^^^^^^^^^^^^ > 8 | } - | ^^^^ Todo: Support functions with unreachable code that may contain hoisted declarations (6:8) + | ^^^^ Support functions with unreachable code that may contain hoisted declarations 9 | } 10 | 11 | export const FIXTURE_ENTRYPOINT = { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.hook-call-freezes-captured-identifier.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.hook-call-freezes-captured-identifier.expect.md index 7babe57b000e2..2b60b2ec7cf51 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.hook-call-freezes-captured-identifier.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.hook-call-freezes-captured-identifier.expect.md @@ -29,10 +29,17 @@ export const FIXTURE_ENTRYPOINT = { ## Error ``` +Found 1 error: + +Error: This value cannot be modified + +Modifying a value previously passed as an argument to a hook is not allowed. Consider moving the modification before calling the hook. + +error.hook-call-freezes-captured-identifier.ts:13:2 11 | }); 12 | > 13 | x.value += count; - | ^ InvalidReact: This mutates a variable that React considers immutable (13:13) + | ^ value cannot be modified 14 | return ; 15 | } 16 | diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.hook-call-freezes-captured-memberexpr.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.hook-call-freezes-captured-memberexpr.expect.md index fcc47ddc2b14f..c57d55e29a3d9 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.hook-call-freezes-captured-memberexpr.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.hook-call-freezes-captured-memberexpr.expect.md @@ -29,10 +29,17 @@ export const FIXTURE_ENTRYPOINT = { ## Error ``` +Found 1 error: + +Error: This value cannot be modified + +Modifying a value previously passed as an argument to a hook is not allowed. Consider moving the modification before calling the hook. + +error.hook-call-freezes-captured-memberexpr.ts:13:2 11 | }); 12 | > 13 | x.value += count; - | ^ InvalidReact: This mutates a variable that React considers immutable (13:13) + | ^ value cannot be modified 14 | return ; 15 | } 16 | diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.hook-property-load-local-hook.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.hook-property-load-local-hook.expect.md index 0949fb3072fcb..3f8e6403aff01 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.hook-property-load-local-hook.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.hook-property-load-local-hook.expect.md @@ -23,15 +23,29 @@ export const FIXTURE_ENTRYPOINT = { ## Error ``` +Found 2 errors: + +Error: Hooks may not be referenced as normal values, they must be called. See https://react.dev/reference/rules/react-calls-components-and-hooks#never-pass-around-hooks-as-regular-values + +error.hook-property-load-local-hook.ts:7:12 5 | 6 | function Foo() { > 7 | let bar = useFoo.useBar; - | ^^^^^^^^^^^^^ InvalidReact: Hooks may not be referenced as normal values, they must be called. See https://react.dev/reference/rules/react-calls-components-and-hooks#never-pass-around-hooks-as-regular-values (7:7) - -InvalidReact: Hooks may not be referenced as normal values, they must be called. See https://react.dev/reference/rules/react-calls-components-and-hooks#never-pass-around-hooks-as-regular-values (8:8) + | ^^^^^^^^^^^^^ Hooks may not be referenced as normal values, they must be called. See https://react.dev/reference/rules/react-calls-components-and-hooks#never-pass-around-hooks-as-regular-values 8 | return bar(); 9 | } 10 | + +Error: Hooks may not be referenced as normal values, they must be called. See https://react.dev/reference/rules/react-calls-components-and-hooks#never-pass-around-hooks-as-regular-values + +error.hook-property-load-local-hook.ts:8:9 + 6 | function Foo() { + 7 | let bar = useFoo.useBar; +> 8 | return bar(); + | ^^^ Hooks may not be referenced as normal values, they must be called. See https://react.dev/reference/rules/react-calls-components-and-hooks#never-pass-around-hooks-as-regular-values + 9 | } + 10 | + 11 | export const FIXTURE_ENTRYPOINT = { ``` \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.hook-ref-value.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.hook-ref-value.expect.md index d92d918fe9f3c..36949c65042d9 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.hook-ref-value.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.hook-ref-value.expect.md @@ -20,12 +20,30 @@ export const FIXTURE_ENTRYPOINT = { ## Error ``` +Found 2 errors: + +Error: Cannot access refs during render + +React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef) + +error.hook-ref-value.ts:5:23 3 | function Component(props) { 4 | const ref = useRef(); > 5 | useEffect(() => {}, [ref.current]); - | ^^^^^^^^^^^ InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (5:5) + | ^^^^^^^^^^^ Cannot access ref value during render + 6 | } + 7 | + 8 | export const FIXTURE_ENTRYPOINT = { -InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (5:5) +Error: Cannot access refs during render + +React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef) + +error.hook-ref-value.ts:5:23 + 3 | function Component(props) { + 4 | const ref = useRef(); +> 5 | useEffect(() => {}, [ref.current]); + | ^^^^^^^^^^^ Cannot access ref value during render 6 | } 7 | 8 | export const FIXTURE_ENTRYPOINT = { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-ReactUseMemo-async-callback.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-ReactUseMemo-async-callback.expect.md index db616600e80dc..4aac70a933274 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-ReactUseMemo-async-callback.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-ReactUseMemo-async-callback.expect.md @@ -15,13 +15,20 @@ function component(a, b) { ## Error ``` +Found 1 error: + +Error: useMemo() callbacks may not be async or generator functions + +useMemo() callbacks are called once and must synchronously return a value. + +error.invalid-ReactUseMemo-async-callback.ts:2:24 1 | function component(a, b) { > 2 | let x = React.useMemo(async () => { | ^^^^^^^^^^^^^ > 3 | await a; | ^^^^^^^^^^^^ > 4 | }, []); - | ^^^^ InvalidReact: useMemo callbacks may not be async or generator functions (2:4) + | ^^^^ Async and generator functions are not supported 5 | return x; 6 | } 7 | diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-during-render.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-during-render.expect.md index 02748366456fb..989e68efd8809 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-during-render.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-during-render.expect.md @@ -15,10 +15,17 @@ function Component(props) { ## Error ``` +Found 1 error: + +Error: Cannot access refs during render + +React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef) + +error.invalid-access-ref-during-render.ts:4:16 2 | function Component(props) { 3 | const ref = useRef(null); > 4 | const value = ref.current; - | ^^^^^^^^^^^ InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (4:4) + | ^^^^^^^^^^^ Cannot access ref value during render 5 | return value; 6 | } 7 | diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-in-reducer-init.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-in-reducer-init.expect.md new file mode 100644 index 0000000000000..29fe24a22025e --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-in-reducer-init.expect.md @@ -0,0 +1,45 @@ + +## Input + +```javascript +import {useReducer, useRef} from 'react'; + +function Component(props) { + const ref = useRef(props.value); + const [state] = useReducer( + (state, action) => state + action, + 0, + init => ref.current + ); + + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{value: 42}], +}; + +``` + + +## Error + +``` +Found 1 error: + +Error: Cannot access refs during render + +React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef) + +error.invalid-access-ref-in-reducer-init.ts:8:4 + 6 | (state, action) => state + action, + 7 | 0, +> 8 | init => ref.current + | ^^^^^^^^^^^^^^^^^^^ Passing a ref to a function may read its value during render + 9 | ); + 10 | + 11 | return ; +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-in-reducer-init.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-in-reducer-init.js new file mode 100644 index 0000000000000..df10b8a9ebd0f --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-in-reducer-init.js @@ -0,0 +1,17 @@ +import {useReducer, useRef} from 'react'; + +function Component(props) { + const ref = useRef(props.value); + const [state] = useReducer( + (state, action) => state + action, + 0, + init => ref.current + ); + + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{value: 42}], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-in-reducer.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-in-reducer.expect.md new file mode 100644 index 0000000000000..f23560b4f69c7 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-in-reducer.expect.md @@ -0,0 +1,41 @@ + +## Input + +```javascript +import {useReducer, useRef} from 'react'; + +function Component(props) { + const ref = useRef(props.value); + const [state] = useReducer(() => ref.current, null); + + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{value: 42}], +}; + +``` + + +## Error + +``` +Found 1 error: + +Error: Cannot access refs during render + +React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef) + +error.invalid-access-ref-in-reducer.ts:5:29 + 3 | function Component(props) { + 4 | const ref = useRef(props.value); +> 5 | const [state] = useReducer(() => ref.current, null); + | ^^^^^^^^^^^^^^^^^ Passing a ref to a function may read its value during render + 6 | + 7 | return ; + 8 | } +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-in-reducer.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-in-reducer.js new file mode 100644 index 0000000000000..135a78e0baef5 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-in-reducer.js @@ -0,0 +1,13 @@ +import {useReducer, useRef} from 'react'; + +function Component(props) { + const ref = useRef(props.value); + const [state] = useReducer(() => ref.current, null); + + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{value: 42}], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-in-render-mutate-object-with-ref-function.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-in-render-mutate-object-with-ref-function.expect.md new file mode 100644 index 0000000000000..a70fcf39b3d80 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-in-render-mutate-object-with-ref-function.expect.md @@ -0,0 +1,37 @@ + +## Input + +```javascript +import {useRef} from 'react'; + +function Component() { + const ref = useRef(null); + const object = {}; + object.foo = () => ref.current; + const refValue = object.foo(); + return
{refValue}
; +} + +``` + + +## Error + +``` +Found 1 error: + +Error: Cannot access refs during render + +React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef) + +error.invalid-access-ref-in-render-mutate-object-with-ref-function.ts:7:19 + 5 | const object = {}; + 6 | object.foo = () => ref.current; +> 7 | const refValue = object.foo(); + | ^^^^^^^^^^ This function accesses a ref value + 8 | return
{refValue}
; + 9 | } + 10 | +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-in-render-mutate-object-with-ref-function.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-in-render-mutate-object-with-ref-function.js new file mode 100644 index 0000000000000..9d3faac764b5a --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-in-render-mutate-object-with-ref-function.js @@ -0,0 +1,9 @@ +import {useRef} from 'react'; + +function Component() { + const ref = useRef(null); + const object = {}; + object.foo = () => ref.current; + const refValue = object.foo(); + return
{refValue}
; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-in-state-initializer.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-in-state-initializer.expect.md new file mode 100644 index 0000000000000..dd6a64d9db748 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-in-state-initializer.expect.md @@ -0,0 +1,41 @@ + +## Input + +```javascript +import {useRef, useState} from 'react'; + +function Component(props) { + const ref = useRef(props.value); + const [state] = useState(() => ref.current); + + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{value: 42}], +}; + +``` + + +## Error + +``` +Found 1 error: + +Error: Cannot access refs during render + +React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef) + +error.invalid-access-ref-in-state-initializer.ts:5:27 + 3 | function Component(props) { + 4 | const ref = useRef(props.value); +> 5 | const [state] = useState(() => ref.current); + | ^^^^^^^^^^^^^^^^^ Passing a ref to a function may read its value during render + 6 | + 7 | return ; + 8 | } +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-in-state-initializer.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-in-state-initializer.js new file mode 100644 index 0000000000000..c3f233023ec85 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-in-state-initializer.js @@ -0,0 +1,13 @@ +import {useRef, useState} from 'react'; + +function Component(props) { + const ref = useRef(props.value); + const [state] = useState(() => ref.current); + + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{value: 42}], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-aliased-ref-in-callback-invoked-during-render-.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-aliased-ref-in-callback-invoked-during-render-.expect.md index e2ce2cceae3b9..3aa5237533962 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-aliased-ref-in-callback-invoked-during-render-.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-aliased-ref-in-callback-invoked-during-render-.expect.md @@ -19,10 +19,17 @@ function Component(props) { ## Error ``` +Found 1 error: + +Error: Cannot access refs during render + +React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef) + +error.invalid-aliased-ref-in-callback-invoked-during-render-.ts:9:33 7 | return ; 8 | }; > 9 | return {props.items.map(item => renderItem(item))}; - | ^^^^^^^^^^^^^^^^^^^^^^^^ InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (9:9) + | ^^^^^^^^^^^^^^^^^^^^^^^^ Cannot access ref value during render 10 | } 11 | ``` diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-array-push-frozen.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-array-push-frozen.expect.md index 0440117adbfc7..a401df523c623 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-array-push-frozen.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-array-push-frozen.expect.md @@ -15,10 +15,17 @@ function Component(props) { ## Error ``` +Found 1 error: + +Error: This value cannot be modified + +Modifying a value used previously in JSX is not allowed. Consider moving the modification before the JSX. + +error.invalid-array-push-frozen.ts:4:2 2 | const x = []; 3 |
{x}
; > 4 | x.push(props.value); - | ^ InvalidReact: Updating a value used previously in JSX is not allowed. Consider moving the mutation before the JSX (4:4) + | ^ value cannot be modified 5 | return x; 6 | } 7 | diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-assign-current-inferred-ref-during-render.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-assign-current-inferred-ref-during-render.expect.md new file mode 100644 index 0000000000000..4f4ed63550d08 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-assign-current-inferred-ref-during-render.expect.md @@ -0,0 +1,36 @@ + +## Input + +```javascript +// @flow @enableTreatRefLikeIdentifiersAsRefs @validateRefAccessDuringRender +import {makeObject_Primitives} from 'shared-runtime'; + +component Example() { + const fooRef = makeObject_Primitives(); + fooRef.current = true; + + return ; +} + +``` + + +## Error + +``` +Found 1 error: + +Error: Cannot access refs during render + +React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef) + + 4 | component Example() { + 5 | const fooRef = makeObject_Primitives(); +> 6 | fooRef.current = true; + | ^^^^^^^^^^^^^^ Cannot update ref during render + 7 | + 8 | return ; + 9 | } +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-assign-current-inferred-ref-during-render.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-assign-current-inferred-ref-during-render.js new file mode 100644 index 0000000000000..39df293ba6258 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-assign-current-inferred-ref-during-render.js @@ -0,0 +1,9 @@ +// @flow @enableTreatRefLikeIdentifiersAsRefs @validateRefAccessDuringRender +import {makeObject_Primitives} from 'shared-runtime'; + +component Example() { + const fooRef = makeObject_Primitives(); + fooRef.current = true; + + return ; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-assign-hook-to-local.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-assign-hook-to-local.expect.md index a4327cf961bfc..e07aa2e32c1b4 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-assign-hook-to-local.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-assign-hook-to-local.expect.md @@ -14,9 +14,14 @@ function Component(props) { ## Error ``` +Found 1 error: + +Error: Hooks may not be referenced as normal values, they must be called. See https://react.dev/reference/rules/react-calls-components-and-hooks#never-pass-around-hooks-as-regular-values + +error.invalid-assign-hook-to-local.ts:2:12 1 | function Component(props) { > 2 | const x = useState; - | ^^^^^^^^ InvalidReact: Hooks may not be referenced as normal values, they must be called. See https://react.dev/reference/rules/react-calls-components-and-hooks#never-pass-around-hooks-as-regular-values (2:2) + | ^^^^^^^^ Hooks may not be referenced as normal values, they must be called. See https://react.dev/reference/rules/react-calls-components-and-hooks#never-pass-around-hooks-as-regular-values 3 | const state = x(null); 4 | return state[0]; 5 | } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-assing-to-ref-current-in-render.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-assing-to-ref-current-in-render.expect.md new file mode 100644 index 0000000000000..aef40d34cb5f3 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-assing-to-ref-current-in-render.expect.md @@ -0,0 +1,36 @@ + +## Input + +```javascript +// @flow + +component Foo() { + const foo = useFoo(); + foo.current = true; + return
; +} + +``` + + +## Error + +``` +Found 1 error: + +Error: This value cannot be modified + +Modifying a value returned from a hook is not allowed. Consider moving the modification into the hook where the value is constructed. + + 3 | component Foo() { + 4 | const foo = useFoo(); +> 5 | foo.current = true; + | ^^^ value cannot be modified + 6 | return
; + 7 | } + 8 | + +Hint: If this value is a Ref (value returned by `useRef()`), rename the variable to end in "Ref". +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-assing-to-ref-current-in-render.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-assing-to-ref-current-in-render.js new file mode 100644 index 0000000000000..efe92bc034e78 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-assing-to-ref-current-in-render.js @@ -0,0 +1,7 @@ +// @flow + +component Foo() { + const foo = useFoo(); + foo.current = true; + return
; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-computed-store-to-frozen-value.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-computed-store-to-frozen-value.expect.md index 2318d38feb80f..d0e4864a76187 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-computed-store-to-frozen-value.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-computed-store-to-frozen-value.expect.md @@ -16,10 +16,17 @@ function Component(props) { ## Error ``` +Found 1 error: + +Error: This value cannot be modified + +Modifying a value used previously in JSX is not allowed. Consider moving the modification before the JSX. + +error.invalid-computed-store-to-frozen-value.ts:5:2 3 | // freeze 4 |
{x}
; > 5 | x[0] = true; - | ^ InvalidReact: Updating a value used previously in JSX is not allowed. Consider moving the mutation before the JSX (5:5) + | ^ value cannot be modified 6 | return x; 7 | } 8 | diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-conditional-call-aliased-hook-import.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-conditional-call-aliased-hook-import.expect.md index 14bf83054607e..a89b7dc0f0902 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-conditional-call-aliased-hook-import.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-conditional-call-aliased-hook-import.expect.md @@ -18,10 +18,15 @@ function Component(props) { ## Error ``` +Found 1 error: + +Error: Hooks must always be called in a consistent order, and may not be called conditionally. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning) + +error.invalid-conditional-call-aliased-hook-import.ts:6:11 4 | let data; 5 | if (props.cond) { > 6 | data = readFragment(); - | ^^^^^^^^^^^^ InvalidReact: Hooks must always be called in a consistent order, and may not be called conditionally. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning) (6:6) + | ^^^^^^^^^^^^ Hooks must always be called in a consistent order, and may not be called conditionally. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning) 7 | } 8 | return data; 9 | } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-conditional-call-aliased-react-hook.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-conditional-call-aliased-react-hook.expect.md index 6c81f3d2be5b0..b5c2a7eb59793 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-conditional-call-aliased-react-hook.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-conditional-call-aliased-react-hook.expect.md @@ -18,10 +18,15 @@ function Component(props) { ## Error ``` +Found 1 error: + +Error: Hooks must always be called in a consistent order, and may not be called conditionally. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning) + +error.invalid-conditional-call-aliased-react-hook.ts:6:10 4 | let s; 5 | if (props.cond) { > 6 | [s] = state(); - | ^^^^^ InvalidReact: Hooks must always be called in a consistent order, and may not be called conditionally. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning) (6:6) + | ^^^^^ Hooks must always be called in a consistent order, and may not be called conditionally. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning) 7 | } 8 | return s; 9 | } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-conditional-call-non-hook-imported-as-hook.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-conditional-call-non-hook-imported-as-hook.expect.md index d0fb92e751c86..c904e866ff63c 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-conditional-call-non-hook-imported-as-hook.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-conditional-call-non-hook-imported-as-hook.expect.md @@ -18,10 +18,15 @@ function Component(props) { ## Error ``` +Found 1 error: + +Error: Hooks must always be called in a consistent order, and may not be called conditionally. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning) + +error.invalid-conditional-call-non-hook-imported-as-hook.ts:6:11 4 | let data; 5 | if (props.cond) { > 6 | data = useArray(); - | ^^^^^^^^ InvalidReact: Hooks must always be called in a consistent order, and may not be called conditionally. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning) (6:6) + | ^^^^^^^^ Hooks must always be called in a consistent order, and may not be called conditionally. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning) 7 | } 8 | return data; 9 | } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-conditional-setState-in-useMemo.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-conditional-setState-in-useMemo.expect.md index f1666cc4013f7..c99dfc1e195df 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-conditional-setState-in-useMemo.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-conditional-setState-in-useMemo.expect.md @@ -22,15 +22,33 @@ function Component({item, cond}) { ## Error ``` +Found 2 errors: + +Error: Calling setState from useMemo may trigger an infinite loop + +Each time the memo callback is evaluated it will change state. This can cause a memoization dependency to change, running the memo function again and causing an infinite loop. Instead of setting state in useMemo(), prefer deriving the value during render. (https://react.dev/reference/react/useState) + +error.invalid-conditional-setState-in-useMemo.ts:7:6 5 | useMemo(() => { 6 | if (cond) { > 7 | setPrevItem(item); - | ^^^^^^^^^^^ InvalidReact: Calling setState from useMemo may trigger an infinite loop. (https://react.dev/reference/react/useState) (7:7) - -InvalidReact: Calling setState from useMemo may trigger an infinite loop. (https://react.dev/reference/react/useState) (8:8) + | ^^^^^^^^^^^ Found setState() within useMemo() 8 | setState(0); 9 | } 10 | }, [cond, key, init]); + +Error: Calling setState from useMemo may trigger an infinite loop + +Each time the memo callback is evaluated it will change state. This can cause a memoization dependency to change, running the memo function again and causing an infinite loop. Instead of setting state in useMemo(), prefer deriving the value during render. (https://react.dev/reference/react/useState) + +error.invalid-conditional-setState-in-useMemo.ts:8:6 + 6 | if (cond) { + 7 | setPrevItem(item); +> 8 | setState(0); + | ^^^^^^^^ Found setState() within useMemo() + 9 | } + 10 | }, [cond, key, init]); + 11 | ``` \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-delete-computed-property-of-frozen-value.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-delete-computed-property-of-frozen-value.expect.md index 7116e4d197154..1518035ae04da 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-delete-computed-property-of-frozen-value.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-delete-computed-property-of-frozen-value.expect.md @@ -16,10 +16,17 @@ function Component(props) { ## Error ``` +Found 1 error: + +Error: This value cannot be modified + +Modifying a value used previously in JSX is not allowed. Consider moving the modification before the JSX. + +error.invalid-delete-computed-property-of-frozen-value.ts:5:9 3 | // freeze 4 |
{x}
; > 5 | delete x[y]; - | ^ InvalidReact: Updating a value used previously in JSX is not allowed. Consider moving the mutation before the JSX (5:5) + | ^ value cannot be modified 6 | return x; 7 | } 8 | diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-delete-property-of-frozen-value.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-delete-property-of-frozen-value.expect.md index c6176d1afc5d9..47f10323ca4d8 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-delete-property-of-frozen-value.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-delete-property-of-frozen-value.expect.md @@ -16,10 +16,17 @@ function Component(props) { ## Error ``` +Found 1 error: + +Error: This value cannot be modified + +Modifying a value used previously in JSX is not allowed. Consider moving the modification before the JSX. + +error.invalid-delete-property-of-frozen-value.ts:5:9 3 | // freeze 4 |
{x}
; > 5 | delete x.y; - | ^ InvalidReact: Updating a value used previously in JSX is not allowed. Consider moving the mutation before the JSX (5:5) + | ^ value cannot be modified 6 | return x; 7 | } 8 | diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-derived-computation-in-effect.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-derived-computation-in-effect.expect.md new file mode 100644 index 0000000000000..1d7e24b3efaff --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-derived-computation-in-effect.expect.md @@ -0,0 +1,41 @@ + +## Input + +```javascript +// @validateNoDerivedComputationsInEffects +function BadExample() { + const [firstName, setFirstName] = useState('Taylor'); + const [lastName, setLastName] = useState('Swift'); + + // 🔴 Avoid: redundant state and unnecessary Effect + const [fullName, setFullName] = useState(''); + useEffect(() => { + setFullName(capitalize(firstName + ' ' + lastName)); + }, [firstName, lastName]); + + return
{fullName}
; +} + +``` + + +## Error + +``` +Found 1 error: + +Error: You may not need this effect. Values derived from state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state) + +This effect updates state based on other state values. Consider calculating this value directly during render. + +error.invalid-derived-computation-in-effect.ts:9:4 + 7 | const [fullName, setFullName] = useState(''); + 8 | useEffect(() => { +> 9 | setFullName(capitalize(firstName + ' ' + lastName)); + | ^^^^^^^^^^^ You may not need this effect. Values derived from state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state) + 10 | }, [firstName, lastName]); + 11 | + 12 | return
{fullName}
; +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-derived-computation-in-effect.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-derived-computation-in-effect.js new file mode 100644 index 0000000000000..d803d3c4a3a1f --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-derived-computation-in-effect.js @@ -0,0 +1,13 @@ +// @validateNoDerivedComputationsInEffects +function BadExample() { + const [firstName, setFirstName] = useState('Taylor'); + const [lastName, setLastName] = useState('Swift'); + + // 🔴 Avoid: redundant state and unnecessary Effect + const [fullName, setFullName] = useState(''); + useEffect(() => { + setFullName(capitalize(firstName + ' ' + lastName)); + }, [firstName, lastName]); + + return
{fullName}
; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-destructure-assignment-to-global.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-destructure-assignment-to-global.expect.md index b3471873eb079..4b49c5f65301c 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-destructure-assignment-to-global.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-destructure-assignment-to-global.expect.md @@ -13,9 +13,16 @@ function useFoo(props) { ## Error ``` +Found 1 error: + +Error: Cannot reassign variables declared outside of the component/hook + +Variable `x` is declared outside of the component/hook. Reassigning this value during render is a form of side effect, which can cause unpredictable behavior depending on when the component happens to re-render. If this variable is used in rendering, use useState instead. Otherwise, consider updating it in an effect. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#side-effects-must-run-outside-of-render) + +error.invalid-destructure-assignment-to-global.ts:2:3 1 | function useFoo(props) { > 2 | [x] = props; - | ^ InvalidReact: Unexpected reassignment of a variable which was defined outside of the component. Components and hooks should be pure and side-effect free, but variable reassignment is a form of side-effect. If this variable is used in rendering, use useState instead. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#side-effects-must-run-outside-of-render) (2:2) + | ^ `x` cannot be reassigned 3 | return {x}; 4 | } 5 | diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-destructure-to-local-global-variables.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-destructure-to-local-global-variables.expect.md index b3303fa189a33..6da3b558bda7f 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-destructure-to-local-global-variables.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-destructure-to-local-global-variables.expect.md @@ -15,10 +15,17 @@ function Component(props) { ## Error ``` +Found 1 error: + +Error: Cannot reassign variables declared outside of the component/hook + +Variable `b` is declared outside of the component/hook. Reassigning this value during render is a form of side effect, which can cause unpredictable behavior depending on when the component happens to re-render. If this variable is used in rendering, use useState instead. Otherwise, consider updating it in an effect. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#side-effects-must-run-outside-of-render) + +error.invalid-destructure-to-local-global-variables.ts:3:6 1 | function Component(props) { 2 | let a; > 3 | [a, b] = props.value; - | ^ InvalidReact: Unexpected reassignment of a variable which was defined outside of the component. Components and hooks should be pure and side-effect free, but variable reassignment is a form of side-effect. If this variable is used in rendering, use useState instead. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#side-effects-must-run-outside-of-render) (3:3) + | ^ `b` cannot be reassigned 4 | 5 | return [a, b]; 6 | } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-disallow-mutating-ref-in-render.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-disallow-mutating-ref-in-render.expect.md index b5547a1328629..9f19d10b9d458 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-disallow-mutating-ref-in-render.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-disallow-mutating-ref-in-render.expect.md @@ -16,10 +16,17 @@ function Component() { ## Error ``` +Found 1 error: + +Error: Cannot access refs during render + +React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef) + +error.invalid-disallow-mutating-ref-in-render.ts:4:2 2 | function Component() { 3 | const ref = useRef(null); > 4 | ref.current = false; - | ^^^^^^^^^^^ InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (4:4) + | ^^^^^^^^^^^ Cannot update ref during render 5 | 6 | return ; +} + +``` + + +## Error + +``` +Found 1 error: + +Error: Cannot reassign variable after render completes + +Reassigning `local` after render has completed can cause inconsistent behavior on subsequent renders. Consider using state instead. + +error.invalid-reassign-local-variable-in-jsx-callback.ts:6:4 + 4 | + 5 | const reassignLocal = newValue => { +> 6 | local = newValue; + | ^^^^^ Cannot reassign `local` after render completes + 7 | }; + 8 | + 9 | const onClick = newValue => { +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/error.invalid-reassign-local-variable-in-jsx-callback.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/error.invalid-reassign-local-variable-in-jsx-callback.js new file mode 100644 index 0000000000000..2cfb336bcf5e3 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/error.invalid-reassign-local-variable-in-jsx-callback.js @@ -0,0 +1,33 @@ +// @enableNewMutationAliasingModel +function Component() { + let local; + + const reassignLocal = newValue => { + local = newValue; + }; + + const onClick = newValue => { + reassignLocal('hello'); + + if (local === newValue) { + // Without React Compiler, `reassignLocal` is freshly created + // on each render, capturing a binding to the latest `local`, + // such that invoking reassignLocal will reassign the same + // binding that we are observing in the if condition, and + // we reach this branch + console.log('`local` was updated!'); + } else { + // With React Compiler enabled, `reassignLocal` is only created + // once, capturing a binding to `local` in that render pass. + // Therefore, calling `reassignLocal` will reassign the wrong + // version of `local`, and not update the binding we are checking + // in the if condition. + // + // To protect against this, we disallow reassigning locals from + // functions that escape + throw new Error('`local` not updated!'); + } + }; + + return ; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/error.invalid-referencing-frozen-hoisted-storecontext-const.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/error.invalid-referencing-frozen-hoisted-storecontext-const.expect.md new file mode 100644 index 0000000000000..d78e4becec787 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/error.invalid-referencing-frozen-hoisted-storecontext-const.expect.md @@ -0,0 +1,57 @@ + +## Input + +```javascript +//@flow @validatePreserveExistingMemoizationGuarantees @enableNewMutationAliasingModel + +import {useCallback} from 'react'; +import {useIdentity} from 'shared-runtime'; + +function Component({content, refetch}) { + // This callback function accesses a hoisted const as a dependency, + // but it cannot reference it as a dependency since that would be a + // TDZ violation! + const onRefetch = useCallback(() => { + refetch(data); + }, [refetch]); + + // The context variable gets frozen here since it's passed to a hook + const onSubmit = useIdentity(onRefetch); + + // This has to error: onRefetch needs to memoize with `content` as a + // dependency, but the dependency comes later + const {data = null} = content; + + return ; +} + +``` + + +## Error + +``` +Found 1 error: + +Error: Cannot access variable before it is declared + +`data` is accessed before it is declared, which prevents the earlier access from updating when this value changes over time. + + 9 | // TDZ violation! + 10 | const onRefetch = useCallback(() => { +> 11 | refetch(data); + | ^^^^ `data` accessed before it is declared + 12 | }, [refetch]); + 13 | + 14 | // The context variable gets frozen here since it's passed to a hook + + 17 | // This has to error: onRefetch needs to memoize with `content` as a + 18 | // dependency, but the dependency comes later +> 19 | const {data = null} = content; + | ^^^^^^^^^^^ `data` is declared here + 20 | + 21 | return ; + 22 | } +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/error.invalid-referencing-frozen-hoisted-storecontext-const.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/error.invalid-referencing-frozen-hoisted-storecontext-const.js new file mode 100644 index 0000000000000..30d1e0e35e3ff --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/error.invalid-referencing-frozen-hoisted-storecontext-const.js @@ -0,0 +1,22 @@ +//@flow @validatePreserveExistingMemoizationGuarantees @enableNewMutationAliasingModel + +import {useCallback} from 'react'; +import {useIdentity} from 'shared-runtime'; + +function Component({content, refetch}) { + // This callback function accesses a hoisted const as a dependency, + // but it cannot reference it as a dependency since that would be a + // TDZ violation! + const onRefetch = useCallback(() => { + refetch(data); + }, [refetch]); + + // The context variable gets frozen here since it's passed to a hook + const onSubmit = useIdentity(onRefetch); + + // This has to error: onRefetch needs to memoize with `content` as a + // dependency, but the dependency comes later + const {data = null} = content; + + return ; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/error.invalid-useCallback-captures-reassigned-context.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/error.invalid-useCallback-captures-reassigned-context.expect.md new file mode 100644 index 0000000000000..a6d7eb64e10aa --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/error.invalid-useCallback-captures-reassigned-context.expect.md @@ -0,0 +1,61 @@ + +## Input + +```javascript +// @validatePreserveExistingMemoizationGuarantees @enableNewMutationAliasingModel +import {useCallback} from 'react'; +import {makeArray} from 'shared-runtime'; + +// This case is already unsound in source, so we can safely bailout +function Foo(props) { + let x = []; + x.push(props); + + // makeArray() is captured, but depsList contains [props] + const cb = useCallback(() => [x], [x]); + + x = makeArray(); + + return cb; +} +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [{}], +}; + +``` + + +## Error + +``` +Found 2 errors: + +Compilation Skipped: Existing memoization could not be preserved + +React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. This dependency may be mutated later, which could cause the value to change unexpectedly. + +error.invalid-useCallback-captures-reassigned-context.ts:11:37 + 9 | + 10 | // makeArray() is captured, but depsList contains [props] +> 11 | const cb = useCallback(() => [x], [x]); + | ^ This dependency may be modified later + 12 | + 13 | x = makeArray(); + 14 | + +Compilation Skipped: Existing memoization could not be preserved + +React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. This value was memoized in source but not in compilation output. + +error.invalid-useCallback-captures-reassigned-context.ts:11:25 + 9 | + 10 | // makeArray() is captured, but depsList contains [props] +> 11 | const cb = useCallback(() => [x], [x]); + | ^^^^^^^^^ Could not preserve existing memoization + 12 | + 13 | x = makeArray(); + 14 | +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/error.invalid-useCallback-captures-reassigned-context.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/error.invalid-useCallback-captures-reassigned-context.js new file mode 100644 index 0000000000000..b9b914d30ec90 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/error.invalid-useCallback-captures-reassigned-context.js @@ -0,0 +1,20 @@ +// @validatePreserveExistingMemoizationGuarantees @enableNewMutationAliasingModel +import {useCallback} from 'react'; +import {makeArray} from 'shared-runtime'; + +// This case is already unsound in source, so we can safely bailout +function Foo(props) { + let x = []; + x.push(props); + + // makeArray() is captured, but depsList contains [props] + const cb = useCallback(() => [x], [x]); + + x = makeArray(); + + return cb; +} +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [{}], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/error.mutate-frozen-value.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/error.mutate-frozen-value.expect.md new file mode 100644 index 0000000000000..f73f23b262d94 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/error.mutate-frozen-value.expect.md @@ -0,0 +1,35 @@ + +## Input + +```javascript +// @enableNewMutationAliasingModel +function Component({a, b}) { + const x = {a}; + useFreeze(x); + x.y = true; + return
error
; +} + +``` + + +## Error + +``` +Found 1 error: + +Error: This value cannot be modified + +Modifying a value previously passed as an argument to a hook is not allowed. Consider moving the modification before calling the hook. + +error.mutate-frozen-value.ts:5:2 + 3 | const x = {a}; + 4 | useFreeze(x); +> 5 | x.y = true; + | ^ value cannot be modified + 6 | return
error
; + 7 | } + 8 | +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/error.mutate-frozen-value.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/error.mutate-frozen-value.js new file mode 100644 index 0000000000000..4964f2304912a --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/error.mutate-frozen-value.js @@ -0,0 +1,7 @@ +// @enableNewMutationAliasingModel +function Component({a, b}) { + const x = {a}; + useFreeze(x); + x.y = true; + return
error
; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/error.mutate-hook-argument.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/error.mutate-hook-argument.expect.md new file mode 100644 index 0000000000000..3de6acb91ba47 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/error.mutate-hook-argument.expect.md @@ -0,0 +1,45 @@ + +## Input + +```javascript +// @enableNewMutationAliasingModel +function useHook(a, b) { + b.test = 1; + a.test = 2; +} + +``` + + +## Error + +``` +Found 2 errors: + +Error: This value cannot be modified + +Modifying component props or hook arguments is not allowed. Consider using a local variable instead. + +error.mutate-hook-argument.ts:3:2 + 1 | // @enableNewMutationAliasingModel + 2 | function useHook(a, b) { +> 3 | b.test = 1; + | ^ value cannot be modified + 4 | a.test = 2; + 5 | } + 6 | + +Error: This value cannot be modified + +Modifying component props or hook arguments is not allowed. Consider using a local variable instead. + +error.mutate-hook-argument.ts:4:2 + 2 | function useHook(a, b) { + 3 | b.test = 1; +> 4 | a.test = 2; + | ^ value cannot be modified + 5 | } + 6 | +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/error.mutate-hook-argument.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/error.mutate-hook-argument.js new file mode 100644 index 0000000000000..41c5b99132460 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/error.mutate-hook-argument.js @@ -0,0 +1,5 @@ +// @enableNewMutationAliasingModel +function useHook(a, b) { + b.test = 1; + a.test = 2; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/error.not-useEffect-external-mutate.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/error.not-useEffect-external-mutate.expect.md new file mode 100644 index 0000000000000..80a12e5d409fa --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/error.not-useEffect-external-mutate.expect.md @@ -0,0 +1,50 @@ + +## Input + +```javascript +// @enableNewMutationAliasingModel +let x = {a: 42}; + +function Component(props) { + foo(() => { + x.a = 10; + x.a = 20; + }); +} + +``` + + +## Error + +``` +Found 2 errors: + +Error: This value cannot be modified + +Modifying a variable defined outside a component or hook is not allowed. Consider using an effect. + +error.not-useEffect-external-mutate.ts:6:4 + 4 | function Component(props) { + 5 | foo(() => { +> 6 | x.a = 10; + | ^ value cannot be modified + 7 | x.a = 20; + 8 | }); + 9 | } + +Error: This value cannot be modified + +Modifying a variable defined outside a component or hook is not allowed. Consider using an effect. + +error.not-useEffect-external-mutate.ts:7:4 + 5 | foo(() => { + 6 | x.a = 10; +> 7 | x.a = 20; + | ^ value cannot be modified + 8 | }); + 9 | } + 10 | +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/error.not-useEffect-external-mutate.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/error.not-useEffect-external-mutate.js new file mode 100644 index 0000000000000..ed51080726b5a --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/error.not-useEffect-external-mutate.js @@ -0,0 +1,9 @@ +// @enableNewMutationAliasingModel +let x = {a: 42}; + +function Component(props) { + foo(() => { + x.a = 10; + x.a = 20; + }); +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/error.reassignment-to-global-indirect.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/error.reassignment-to-global-indirect.expect.md new file mode 100644 index 0000000000000..41ed513912f9a --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/error.reassignment-to-global-indirect.expect.md @@ -0,0 +1,50 @@ + +## Input + +```javascript +// @enableNewMutationAliasingModel +function Component() { + const foo = () => { + // Cannot assign to globals + someUnknownGlobal = true; + moduleLocal = true; + }; + foo(); +} + +``` + + +## Error + +``` +Found 2 errors: + +Error: Cannot reassign variables declared outside of the component/hook + +Variable `someUnknownGlobal` is declared outside of the component/hook. Reassigning this value during render is a form of side effect, which can cause unpredictable behavior depending on when the component happens to re-render. If this variable is used in rendering, use useState instead. Otherwise, consider updating it in an effect. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#side-effects-must-run-outside-of-render) + +error.reassignment-to-global-indirect.ts:5:4 + 3 | const foo = () => { + 4 | // Cannot assign to globals +> 5 | someUnknownGlobal = true; + | ^^^^^^^^^^^^^^^^^ `someUnknownGlobal` cannot be reassigned + 6 | moduleLocal = true; + 7 | }; + 8 | foo(); + +Error: Cannot reassign variables declared outside of the component/hook + +Variable `moduleLocal` is declared outside of the component/hook. Reassigning this value during render is a form of side effect, which can cause unpredictable behavior depending on when the component happens to re-render. If this variable is used in rendering, use useState instead. Otherwise, consider updating it in an effect. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#side-effects-must-run-outside-of-render) + +error.reassignment-to-global-indirect.ts:6:4 + 4 | // Cannot assign to globals + 5 | someUnknownGlobal = true; +> 6 | moduleLocal = true; + | ^^^^^^^^^^^ `moduleLocal` cannot be reassigned + 7 | }; + 8 | foo(); + 9 | } +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/error.reassignment-to-global-indirect.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/error.reassignment-to-global-indirect.js new file mode 100644 index 0000000000000..6d6681e60ad34 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/error.reassignment-to-global-indirect.js @@ -0,0 +1,9 @@ +// @enableNewMutationAliasingModel +function Component() { + const foo = () => { + // Cannot assign to globals + someUnknownGlobal = true; + moduleLocal = true; + }; + foo(); +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/error.reassignment-to-global.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/error.reassignment-to-global.expect.md new file mode 100644 index 0000000000000..6089255fd5efe --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/error.reassignment-to-global.expect.md @@ -0,0 +1,46 @@ + +## Input + +```javascript +// @enableNewMutationAliasingModel +function Component() { + // Cannot assign to globals + someUnknownGlobal = true; + moduleLocal = true; +} + +``` + + +## Error + +``` +Found 2 errors: + +Error: Cannot reassign variables declared outside of the component/hook + +Variable `someUnknownGlobal` is declared outside of the component/hook. Reassigning this value during render is a form of side effect, which can cause unpredictable behavior depending on when the component happens to re-render. If this variable is used in rendering, use useState instead. Otherwise, consider updating it in an effect. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#side-effects-must-run-outside-of-render) + +error.reassignment-to-global.ts:4:2 + 2 | function Component() { + 3 | // Cannot assign to globals +> 4 | someUnknownGlobal = true; + | ^^^^^^^^^^^^^^^^^ `someUnknownGlobal` cannot be reassigned + 5 | moduleLocal = true; + 6 | } + 7 | + +Error: Cannot reassign variables declared outside of the component/hook + +Variable `moduleLocal` is declared outside of the component/hook. Reassigning this value during render is a form of side effect, which can cause unpredictable behavior depending on when the component happens to re-render. If this variable is used in rendering, use useState instead. Otherwise, consider updating it in an effect. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#side-effects-must-run-outside-of-render) + +error.reassignment-to-global.ts:5:2 + 3 | // Cannot assign to globals + 4 | someUnknownGlobal = true; +> 5 | moduleLocal = true; + | ^^^^^^^^^^^ `moduleLocal` cannot be reassigned + 6 | } + 7 | +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/error.reassignment-to-global.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/error.reassignment-to-global.js new file mode 100644 index 0000000000000..41b706866bf7c --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/error.reassignment-to-global.js @@ -0,0 +1,6 @@ +// @enableNewMutationAliasingModel +function Component() { + // Cannot assign to globals + someUnknownGlobal = true; + moduleLocal = true; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/error.todo-repro-named-function-with-shadowed-local-same-name.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/error.todo-repro-named-function-with-shadowed-local-same-name.expect.md new file mode 100644 index 0000000000000..092c895cc9812 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/error.todo-repro-named-function-with-shadowed-local-same-name.expect.md @@ -0,0 +1,38 @@ + +## Input + +```javascript +// @enableNewMutationAliasingModel +function Component(props) { + function hasErrors() { + let hasErrors = false; + if (props.items == null) { + hasErrors = true; + } + return hasErrors; + } + return hasErrors(); +} + +``` + + +## Error + +``` +Found 1 error: + +Invariant: [InferMutationAliasingEffects] Expected value kind to be initialized + + hasErrors_0$15:TFunction. + +error.todo-repro-named-function-with-shadowed-local-same-name.ts:10:9 + 8 | return hasErrors; + 9 | } +> 10 | return hasErrors(); + | ^^^^^^^^^ [InferMutationAliasingEffects] Expected value kind to be initialized + 11 | } + 12 | +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/error.todo-repro-named-function-with-shadowed-local-same-name.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/error.todo-repro-named-function-with-shadowed-local-same-name.js new file mode 100644 index 0000000000000..b58c0aea7daf7 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/error.todo-repro-named-function-with-shadowed-local-same-name.js @@ -0,0 +1,11 @@ +// @enableNewMutationAliasingModel +function Component(props) { + function hasErrors() { + let hasErrors = false; + if (props.items == null) { + hasErrors = true; + } + return hasErrors; + } + return hasErrors(); +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/iife-return-modified-later-phi.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/iife-return-modified-later-phi.expect.md new file mode 100644 index 0000000000000..22f967883b09a --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/iife-return-modified-later-phi.expect.md @@ -0,0 +1,58 @@ + +## Input + +```javascript +function Component(props) { + const items = (() => { + if (props.cond) { + return []; + } else { + return null; + } + })(); + items?.push(props.a); + return items; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{a: {}}], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; +function Component(props) { + const $ = _c(3); + let items; + if ($[0] !== props.a || $[1] !== props.cond) { + let t0; + if (props.cond) { + t0 = []; + } else { + t0 = null; + } + items = t0; + + items?.push(props.a); + $[0] = props.a; + $[1] = props.cond; + $[2] = items; + } else { + items = $[2]; + } + return items; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ a: {} }], +}; + +``` + +### Eval output +(kind: ok) null \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/iife-return-modified-later-phi.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/iife-return-modified-later-phi.js new file mode 100644 index 0000000000000..f4f953d294e6f --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/iife-return-modified-later-phi.js @@ -0,0 +1,16 @@ +function Component(props) { + const items = (() => { + if (props.cond) { + return []; + } else { + return null; + } + })(); + items?.push(props.a); + return items; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{a: {}}], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/mutate-after-useeffect-optional-chain.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/mutate-after-useeffect-optional-chain.expect.md new file mode 100644 index 0000000000000..6a9b4d98e28f5 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/mutate-after-useeffect-optional-chain.expect.md @@ -0,0 +1,58 @@ + +## Input + +```javascript +// @inferEffectDependencies @panicThreshold:"none" @loggerTestOnly @enableNewMutationAliasingModel +import {useEffect, AUTODEPS} from 'react'; +import {print} from 'shared-runtime'; + +function Component({foo}) { + const arr = []; + // Taking either arr[0].value or arr as a dependency is reasonable + // as long as developers know what to expect. + useEffect(() => print(arr[0]?.value), AUTODEPS); + arr.push({value: foo}); + return arr; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{foo: 1}], +}; + +``` + +## Code + +```javascript +// @inferEffectDependencies @panicThreshold:"none" @loggerTestOnly @enableNewMutationAliasingModel +import { useEffect, AUTODEPS } from "react"; +import { print } from "shared-runtime"; + +function Component(t0) { + const { foo } = t0; + const arr = []; + + useEffect(() => print(arr[0]?.value), [arr[0]?.value]); + arr.push({ value: foo }); + return arr; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ foo: 1 }], +}; + +``` + +## Logs + +``` +{"kind":"CompileError","fnLoc":{"start":{"line":5,"column":0,"index":181},"end":{"line":12,"column":1,"index":436},"filename":"mutate-after-useeffect-optional-chain.ts"},"detail":{"options":{"category":"Immutability","severity":"InvalidReact","reason":"This value cannot be modified","description":"Modifying a value used previously in an effect function or as an effect dependency is not allowed. Consider moving the modification before calling useEffect().","details":[{"kind":"error","loc":{"start":{"line":10,"column":2,"index":397},"end":{"line":10,"column":5,"index":400},"filename":"mutate-after-useeffect-optional-chain.ts","identifierName":"arr"},"message":"value cannot be modified"}]}}} +{"kind":"AutoDepsDecorations","fnLoc":{"start":{"line":9,"column":2,"index":346},"end":{"line":9,"column":49,"index":393},"filename":"mutate-after-useeffect-optional-chain.ts"},"decorations":[{"start":{"line":9,"column":24,"index":368},"end":{"line":9,"column":27,"index":371},"filename":"mutate-after-useeffect-optional-chain.ts","identifierName":"arr"}]} +{"kind":"CompileSuccess","fnLoc":{"start":{"line":5,"column":0,"index":181},"end":{"line":12,"column":1,"index":436},"filename":"mutate-after-useeffect-optional-chain.ts"},"fnName":"Component","memoSlots":0,"memoBlocks":0,"memoValues":0,"prunedMemoBlocks":0,"prunedMemoValues":0} +``` + +### Eval output +(kind: ok) [{"value":1}] +logs: [1] \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/mutate-after-useeffect-optional-chain.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/mutate-after-useeffect-optional-chain.js new file mode 100644 index 0000000000000..ee59d8fe4ea90 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/mutate-after-useeffect-optional-chain.js @@ -0,0 +1,17 @@ +// @inferEffectDependencies @panicThreshold:"none" @loggerTestOnly @enableNewMutationAliasingModel +import {useEffect, AUTODEPS} from 'react'; +import {print} from 'shared-runtime'; + +function Component({foo}) { + const arr = []; + // Taking either arr[0].value or arr as a dependency is reasonable + // as long as developers know what to expect. + useEffect(() => print(arr[0]?.value), AUTODEPS); + arr.push({value: foo}); + return arr; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{foo: 1}], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/mutate-after-useeffect-ref-access.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/mutate-after-useeffect-ref-access.expect.md new file mode 100644 index 0000000000000..00473c8eec7ca --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/mutate-after-useeffect-ref-access.expect.md @@ -0,0 +1,57 @@ + +## Input + +```javascript +// @inferEffectDependencies @panicThreshold:"none" @loggerTestOnly @enableNewMutationAliasingModel + +import {useEffect, useRef, AUTODEPS} from 'react'; +import {print} from 'shared-runtime'; + +function Component({arrRef}) { + // Avoid taking arr.current as a dependency + useEffect(() => print(arrRef.current), AUTODEPS); + arrRef.current.val = 2; + return arrRef; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{arrRef: {current: {val: 'initial ref value'}}}], +}; + +``` + +## Code + +```javascript +// @inferEffectDependencies @panicThreshold:"none" @loggerTestOnly @enableNewMutationAliasingModel + +import { useEffect, useRef, AUTODEPS } from "react"; +import { print } from "shared-runtime"; + +function Component(t0) { + const { arrRef } = t0; + + useEffect(() => print(arrRef.current), [arrRef]); + arrRef.current.val = 2; + return arrRef; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ arrRef: { current: { val: "initial ref value" } } }], +}; + +``` + +## Logs + +``` +{"kind":"CompileError","fnLoc":{"start":{"line":6,"column":0,"index":190},"end":{"line":11,"column":1,"index":363},"filename":"mutate-after-useeffect-ref-access.ts"},"detail":{"options":{"category":"Refs","severity":"InvalidReact","reason":"Cannot access refs during render","description":"React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef)","details":[{"kind":"error","loc":{"start":{"line":9,"column":2,"index":321},"end":{"line":9,"column":16,"index":335},"filename":"mutate-after-useeffect-ref-access.ts"},"message":"Cannot update ref during render"}]}}} +{"kind":"AutoDepsDecorations","fnLoc":{"start":{"line":8,"column":2,"index":269},"end":{"line":8,"column":50,"index":317},"filename":"mutate-after-useeffect-ref-access.ts"},"decorations":[{"start":{"line":8,"column":24,"index":291},"end":{"line":8,"column":30,"index":297},"filename":"mutate-after-useeffect-ref-access.ts","identifierName":"arrRef"}]} +{"kind":"CompileSuccess","fnLoc":{"start":{"line":6,"column":0,"index":190},"end":{"line":11,"column":1,"index":363},"filename":"mutate-after-useeffect-ref-access.ts"},"fnName":"Component","memoSlots":0,"memoBlocks":0,"memoValues":0,"prunedMemoBlocks":0,"prunedMemoValues":0} +``` + +### Eval output +(kind: ok) {"current":{"val":2}} +logs: [{ val: 2 }] \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/mutate-after-useeffect-ref-access.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/mutate-after-useeffect-ref-access.js new file mode 100644 index 0000000000000..9d0f7e194d0e1 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/mutate-after-useeffect-ref-access.js @@ -0,0 +1,16 @@ +// @inferEffectDependencies @panicThreshold:"none" @loggerTestOnly @enableNewMutationAliasingModel + +import {useEffect, useRef, AUTODEPS} from 'react'; +import {print} from 'shared-runtime'; + +function Component({arrRef}) { + // Avoid taking arr.current as a dependency + useEffect(() => print(arrRef.current), AUTODEPS); + arrRef.current.val = 2; + return arrRef; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{arrRef: {current: {val: 'initial ref value'}}}], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/mutate-after-useeffect.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/mutate-after-useeffect.expect.md new file mode 100644 index 0000000000000..825bc4a1df05d --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/mutate-after-useeffect.expect.md @@ -0,0 +1,56 @@ + +## Input + +```javascript +// @inferEffectDependencies @panicThreshold:"none" @loggerTestOnly @enableNewMutationAliasingModel +import {useEffect, AUTODEPS} from 'react'; + +function Component({foo}) { + const arr = []; + useEffect(() => { + arr.push(foo); + }, AUTODEPS); + arr.push(2); + return arr; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{foo: 1}], +}; + +``` + +## Code + +```javascript +// @inferEffectDependencies @panicThreshold:"none" @loggerTestOnly @enableNewMutationAliasingModel +import { useEffect, AUTODEPS } from "react"; + +function Component(t0) { + const { foo } = t0; + const arr = []; + useEffect(() => { + arr.push(foo); + }, [arr, foo]); + arr.push(2); + return arr; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ foo: 1 }], +}; + +``` + +## Logs + +``` +{"kind":"CompileError","fnLoc":{"start":{"line":4,"column":0,"index":143},"end":{"line":11,"column":1,"index":274},"filename":"mutate-after-useeffect.ts"},"detail":{"options":{"category":"Immutability","severity":"InvalidReact","reason":"This value cannot be modified","description":"Modifying a value used previously in an effect function or as an effect dependency is not allowed. Consider moving the modification before calling useEffect().","details":[{"kind":"error","loc":{"start":{"line":9,"column":2,"index":246},"end":{"line":9,"column":5,"index":249},"filename":"mutate-after-useeffect.ts","identifierName":"arr"},"message":"value cannot be modified"}]}}} +{"kind":"AutoDepsDecorations","fnLoc":{"start":{"line":6,"column":2,"index":191},"end":{"line":8,"column":14,"index":242},"filename":"mutate-after-useeffect.ts"},"decorations":[{"start":{"line":7,"column":4,"index":213},"end":{"line":7,"column":7,"index":216},"filename":"mutate-after-useeffect.ts","identifierName":"arr"},{"start":{"line":7,"column":4,"index":213},"end":{"line":7,"column":7,"index":216},"filename":"mutate-after-useeffect.ts","identifierName":"arr"},{"start":{"line":7,"column":13,"index":222},"end":{"line":7,"column":16,"index":225},"filename":"mutate-after-useeffect.ts","identifierName":"foo"}]} +{"kind":"CompileSuccess","fnLoc":{"start":{"line":4,"column":0,"index":143},"end":{"line":11,"column":1,"index":274},"filename":"mutate-after-useeffect.ts"},"fnName":"Component","memoSlots":0,"memoBlocks":0,"memoValues":0,"prunedMemoBlocks":0,"prunedMemoValues":0} +``` + +### Eval output +(kind: ok) [2] \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/mutate-after-useeffect.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/mutate-after-useeffect.js new file mode 100644 index 0000000000000..21298604043a4 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/mutate-after-useeffect.js @@ -0,0 +1,16 @@ +// @inferEffectDependencies @panicThreshold:"none" @loggerTestOnly @enableNewMutationAliasingModel +import {useEffect, AUTODEPS} from 'react'; + +function Component({foo}) { + const arr = []; + useEffect(() => { + arr.push(foo); + }, AUTODEPS); + arr.push(2); + return arr; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{foo: 1}], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/mutate-through-boxing-unboxing-function-call-indirections-2.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/mutate-through-boxing-unboxing-function-call-indirections-2.expect.md new file mode 100644 index 0000000000000..013da083261e5 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/mutate-through-boxing-unboxing-function-call-indirections-2.expect.md @@ -0,0 +1,67 @@ + +## Input + +```javascript +// @enableNewMutationAliasingModel +import {Stringify} from 'shared-runtime'; + +function Component({a, b}) { + const x = {a, b}; + const f = () => { + const y = [x]; + return y[0]; + }; + const x0 = f(); + const z = [x0]; + const x1 = z[0]; + x1.key = 'value'; + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{a: 0, b: 1}], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel +import { Stringify } from "shared-runtime"; + +function Component(t0) { + const $ = _c(3); + const { a, b } = t0; + let t1; + if ($[0] !== a || $[1] !== b) { + const x = { a, b }; + const f = () => { + const y = [x]; + return y[0]; + }; + + const x0 = f(); + const z = [x0]; + const x1 = z[0]; + x1.key = "value"; + t1 = ; + $[0] = a; + $[1] = b; + $[2] = t1; + } else { + t1 = $[2]; + } + return t1; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ a: 0, b: 1 }], +}; + +``` + +### Eval output +(kind: ok)
{"x":{"a":0,"b":1,"key":"value"}}
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/mutate-through-boxing-unboxing-function-call-indirections-2.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/mutate-through-boxing-unboxing-function-call-indirections-2.js new file mode 100644 index 0000000000000..6a981e840891c --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/mutate-through-boxing-unboxing-function-call-indirections-2.js @@ -0,0 +1,20 @@ +// @enableNewMutationAliasingModel +import {Stringify} from 'shared-runtime'; + +function Component({a, b}) { + const x = {a, b}; + const f = () => { + const y = [x]; + return y[0]; + }; + const x0 = f(); + const z = [x0]; + const x1 = z[0]; + x1.key = 'value'; + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{a: 0, b: 1}], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/mutate-through-boxing-unboxing-function-call-indirections.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/mutate-through-boxing-unboxing-function-call-indirections.expect.md new file mode 100644 index 0000000000000..f8ceba27158bd --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/mutate-through-boxing-unboxing-function-call-indirections.expect.md @@ -0,0 +1,67 @@ + +## Input + +```javascript +// @enableNewMutationAliasingModel +import {Stringify} from 'shared-runtime'; + +function Component({a, b}) { + const x = {a, b}; + const y = [x]; + const f = () => { + const x0 = y[0]; + return [x0]; + }; + const z = f(); + const x1 = z[0]; + x1.key = 'value'; + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{a: 0, b: 1}], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel +import { Stringify } from "shared-runtime"; + +function Component(t0) { + const $ = _c(3); + const { a, b } = t0; + let t1; + if ($[0] !== a || $[1] !== b) { + const x = { a, b }; + const y = [x]; + const f = () => { + const x0 = y[0]; + return [x0]; + }; + + const z = f(); + const x1 = z[0]; + x1.key = "value"; + t1 = ; + $[0] = a; + $[1] = b; + $[2] = t1; + } else { + t1 = $[2]; + } + return t1; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ a: 0, b: 1 }], +}; + +``` + +### Eval output +(kind: ok)
{"x":{"a":0,"b":1,"key":"value"}}
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/mutate-through-boxing-unboxing-function-call-indirections.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/mutate-through-boxing-unboxing-function-call-indirections.js new file mode 100644 index 0000000000000..aecd27a093094 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/mutate-through-boxing-unboxing-function-call-indirections.js @@ -0,0 +1,20 @@ +// @enableNewMutationAliasingModel +import {Stringify} from 'shared-runtime'; + +function Component({a, b}) { + const x = {a, b}; + const y = [x]; + const f = () => { + const x0 = y[0]; + return [x0]; + }; + const z = f(); + const x1 = z[0]; + x1.key = 'value'; + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{a: 0, b: 1}], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/mutate-through-boxing-unboxing-indirections.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/mutate-through-boxing-unboxing-indirections.expect.md new file mode 100644 index 0000000000000..5f14dd1fe0770 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/mutate-through-boxing-unboxing-indirections.expect.md @@ -0,0 +1,60 @@ + +## Input + +```javascript +// @enableNewMutationAliasingModel +import {Stringify} from 'shared-runtime'; + +function Component({a, b}) { + const x = {a, b}; + const y = [x]; + const x0 = y[0]; + const z = [x0]; + const x1 = z[0]; + x1.key = 'value'; + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{a: 0, b: 1}], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel +import { Stringify } from "shared-runtime"; + +function Component(t0) { + const $ = _c(3); + const { a, b } = t0; + let t1; + if ($[0] !== a || $[1] !== b) { + const x = { a, b }; + const y = [x]; + const x0 = y[0]; + const z = [x0]; + const x1 = z[0]; + x1.key = "value"; + t1 = ; + $[0] = a; + $[1] = b; + $[2] = t1; + } else { + t1 = $[2]; + } + return t1; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ a: 0, b: 1 }], +}; + +``` + +### Eval output +(kind: ok)
{"x":{"a":0,"b":1,"key":"value"}}
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/mutate-through-boxing-unboxing-indirections.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/mutate-through-boxing-unboxing-indirections.js new file mode 100644 index 0000000000000..ba8808eedfe8c --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/mutate-through-boxing-unboxing-indirections.js @@ -0,0 +1,17 @@ +// @enableNewMutationAliasingModel +import {Stringify} from 'shared-runtime'; + +function Component({a, b}) { + const x = {a, b}; + const y = [x]; + const x0 = y[0]; + const z = [x0]; + const x1 = z[0]; + x1.key = 'value'; + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{a: 0, b: 1}], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/mutate-through-identity-function-expression.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/mutate-through-identity-function-expression.expect.md new file mode 100644 index 0000000000000..83e593dbd4149 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/mutate-through-identity-function-expression.expect.md @@ -0,0 +1,93 @@ + +## Input + +```javascript +import {useMemo} from 'react'; +import {identity, ValidateMemoization} from 'shared-runtime'; + +function Component({a, b}) { + const x = useMemo(() => ({a}), [a, b]); + const f = () => { + return identity(x); + }; + const x2 = f(); + x2.b = b; + + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{a: 0, b: 0}], + sequentialRenders: [ + {a: 0, b: 0}, + {a: 0, b: 1}, + {a: 1, b: 1}, + {a: 0, b: 0}, + ], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; +import { useMemo } from "react"; +import { identity, ValidateMemoization } from "shared-runtime"; + +function Component(t0) { + const $ = _c(9); + const { a, b } = t0; + let x; + if ($[0] !== a || $[1] !== b) { + x = { a }; + const f = () => identity(x); + + const x2 = f(); + x2.b = b; + $[0] = a; + $[1] = b; + $[2] = x; + } else { + x = $[2]; + } + let t1; + if ($[3] !== a || $[4] !== b) { + t1 = [a, b]; + $[3] = a; + $[4] = b; + $[5] = t1; + } else { + t1 = $[5]; + } + let t2; + if ($[6] !== t1 || $[7] !== x) { + t2 = ; + $[6] = t1; + $[7] = x; + $[8] = t2; + } else { + t2 = $[8]; + } + return t2; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ a: 0, b: 0 }], + sequentialRenders: [ + { a: 0, b: 0 }, + { a: 0, b: 1 }, + { a: 1, b: 1 }, + { a: 0, b: 0 }, + ], +}; + +``` + +### Eval output +(kind: ok)
{"inputs":[0,0],"output":{"a":0,"b":0}}
+
{"inputs":[0,1],"output":{"a":0,"b":1}}
+
{"inputs":[1,1],"output":{"a":1,"b":1}}
+
{"inputs":[0,0],"output":{"a":0,"b":0}}
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/mutate-through-identity-function-expression.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/mutate-through-identity-function-expression.js new file mode 100644 index 0000000000000..c7770ffcdce2b --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/mutate-through-identity-function-expression.js @@ -0,0 +1,24 @@ +import {useMemo} from 'react'; +import {identity, ValidateMemoization} from 'shared-runtime'; + +function Component({a, b}) { + const x = useMemo(() => ({a}), [a, b]); + const f = () => { + return identity(x); + }; + const x2 = f(); + x2.b = b; + + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{a: 0, b: 0}], + sequentialRenders: [ + {a: 0, b: 0}, + {a: 0, b: 1}, + {a: 1, b: 1}, + {a: 0, b: 0}, + ], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/mutate-through-identity.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/mutate-through-identity.expect.md new file mode 100644 index 0000000000000..78cb6697fc7ac --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/mutate-through-identity.expect.md @@ -0,0 +1,88 @@ + +## Input + +```javascript +import {useMemo} from 'react'; +import {identity, ValidateMemoization} from 'shared-runtime'; + +function Component({a, b}) { + const x = useMemo(() => ({a}), [a, b]); + const x2 = identity(x); + x2.b = b; + + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{a: 0, b: 0}], + sequentialRenders: [ + {a: 0, b: 0}, + {a: 0, b: 1}, + {a: 1, b: 1}, + {a: 0, b: 0}, + ], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; +import { useMemo } from "react"; +import { identity, ValidateMemoization } from "shared-runtime"; + +function Component(t0) { + const $ = _c(9); + const { a, b } = t0; + let x; + if ($[0] !== a || $[1] !== b) { + x = { a }; + const x2 = identity(x); + x2.b = b; + $[0] = a; + $[1] = b; + $[2] = x; + } else { + x = $[2]; + } + let t1; + if ($[3] !== a || $[4] !== b) { + t1 = [a, b]; + $[3] = a; + $[4] = b; + $[5] = t1; + } else { + t1 = $[5]; + } + let t2; + if ($[6] !== t1 || $[7] !== x) { + t2 = ; + $[6] = t1; + $[7] = x; + $[8] = t2; + } else { + t2 = $[8]; + } + return t2; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ a: 0, b: 0 }], + sequentialRenders: [ + { a: 0, b: 0 }, + { a: 0, b: 1 }, + { a: 1, b: 1 }, + { a: 0, b: 0 }, + ], +}; + +``` + +### Eval output +(kind: ok)
{"inputs":[0,0],"output":{"a":0,"b":0}}
+
{"inputs":[0,1],"output":{"a":0,"b":1}}
+
{"inputs":[1,1],"output":{"a":1,"b":1}}
+
{"inputs":[0,0],"output":{"a":0,"b":0}}
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/mutate-through-identity.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/mutate-through-identity.js new file mode 100644 index 0000000000000..bd928634a29bf --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/mutate-through-identity.js @@ -0,0 +1,21 @@ +import {useMemo} from 'react'; +import {identity, ValidateMemoization} from 'shared-runtime'; + +function Component({a, b}) { + const x = useMemo(() => ({a}), [a, b]); + const x2 = identity(x); + x2.b = b; + + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{a: 0, b: 0}], + sequentialRenders: [ + {a: 0, b: 0}, + {a: 0, b: 1}, + {a: 1, b: 1}, + {a: 0, b: 0}, + ], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/mutate-through-propertyload.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/mutate-through-propertyload.expect.md new file mode 100644 index 0000000000000..34345951ed7fa --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/mutate-through-propertyload.expect.md @@ -0,0 +1,39 @@ + +## Input + +```javascript +// @enableNewMutationAliasingModel +function Component({a, b}) { + const x = {}; + const y = {x}; + const z = y.x; + z.true = false; + return
{z}
; +} + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel +function Component(t0) { + const $ = _c(1); + let t1; + if ($[0] === Symbol.for("react.memo_cache_sentinel")) { + const x = {}; + const y = { x }; + const z = y.x; + z.true = false; + t1 =
{z}
; + $[0] = t1; + } else { + t1 = $[0]; + } + return t1; +} + +``` + +### 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/new-mutability/mutate-through-propertyload.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/mutate-through-propertyload.js new file mode 100644 index 0000000000000..bff1ea4c35046 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/mutate-through-propertyload.js @@ -0,0 +1,8 @@ +// @enableNewMutationAliasingModel +function Component({a, b}) { + const x = {}; + const y = {x}; + const z = y.x; + z.true = false; + return
{z}
; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/nullable-objects-assume-invoked-direct-call.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/nullable-objects-assume-invoked-direct-call.expect.md new file mode 100644 index 0000000000000..5033da8eac440 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/nullable-objects-assume-invoked-direct-call.expect.md @@ -0,0 +1,75 @@ + +## Input + +```javascript +// @enableNewMutationAliasingModel +import {useState} from 'react'; +import {useIdentity} from 'shared-runtime'; + +function useMakeCallback({obj}: {obj: {value: number}}) { + const [state, setState] = useState(0); + const cb = () => { + if (obj.value !== state) setState(obj.value); + }; + useIdentity(); + cb(); + return [cb]; +} +export const FIXTURE_ENTRYPOINT = { + fn: useMakeCallback, + params: [{obj: {value: 1}}], + sequentialRenders: [{obj: {value: 1}}, {obj: {value: 2}}], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel +import { useState } from "react"; +import { useIdentity } from "shared-runtime"; + +function useMakeCallback(t0) { + const $ = _c(5); + const { obj } = t0; + const [state, setState] = useState(0); + let t1; + if ($[0] !== obj.value || $[1] !== state) { + t1 = () => { + if (obj.value !== state) { + setState(obj.value); + } + }; + $[0] = obj.value; + $[1] = state; + $[2] = t1; + } else { + t1 = $[2]; + } + const cb = t1; + + useIdentity(); + cb(); + let t2; + if ($[3] !== cb) { + t2 = [cb]; + $[3] = cb; + $[4] = t2; + } else { + t2 = $[4]; + } + return t2; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useMakeCallback, + params: [{ obj: { value: 1 } }], + sequentialRenders: [{ obj: { value: 1 } }, { obj: { value: 2 } }], +}; + +``` + +### Eval output +(kind: ok) ["[[ function params=0 ]]"] +["[[ function params=0 ]]"] \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/nullable-objects-assume-invoked-direct-call.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/nullable-objects-assume-invoked-direct-call.js new file mode 100644 index 0000000000000..1f2d69d93193f --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/nullable-objects-assume-invoked-direct-call.js @@ -0,0 +1,18 @@ +// @enableNewMutationAliasingModel +import {useState} from 'react'; +import {useIdentity} from 'shared-runtime'; + +function useMakeCallback({obj}: {obj: {value: number}}) { + const [state, setState] = useState(0); + const cb = () => { + if (obj.value !== state) setState(obj.value); + }; + useIdentity(); + cb(); + return [cb]; +} +export const FIXTURE_ENTRYPOINT = { + fn: useMakeCallback, + params: [{obj: {value: 1}}], + sequentialRenders: [{obj: {value: 1}}, {obj: {value: 2}}], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/object-expression-computed-key-object-mutated-later.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/object-expression-computed-key-object-mutated-later.expect.md new file mode 100644 index 0000000000000..5c73ce6d77adf --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/object-expression-computed-key-object-mutated-later.expect.md @@ -0,0 +1,54 @@ + +## Input + +```javascript +// @enableNewMutationAliasingModel +import {identity, mutate} from 'shared-runtime'; + +function Component(props) { + const key = {}; + const context = { + [key]: identity([props.value]), + }; + mutate(key); + return context; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{value: 42}], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel +import { identity, mutate } from "shared-runtime"; + +function Component(props) { + const $ = _c(2); + let context; + if ($[0] !== props.value) { + const key = {}; + context = { [key]: identity([props.value]) }; + + mutate(key); + $[0] = props.value; + $[1] = context; + } else { + context = $[1]; + } + return context; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ value: 42 }], +}; + +``` + +### Eval output +(kind: ok) {"[object Object]":[42]} \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/object-expression-computed-key-object-mutated-later.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/object-expression-computed-key-object-mutated-later.js new file mode 100644 index 0000000000000..923733b9c238d --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/object-expression-computed-key-object-mutated-later.js @@ -0,0 +1,16 @@ +// @enableNewMutationAliasingModel +import {identity, mutate} from 'shared-runtime'; + +function Component(props) { + const key = {}; + const context = { + [key]: identity([props.value]), + }; + mutate(key); + return context; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{value: 42}], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/object-expression-computed-member.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/object-expression-computed-member.expect.md new file mode 100644 index 0000000000000..1ef3ed157f9fa --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/object-expression-computed-member.expect.md @@ -0,0 +1,65 @@ + +## Input + +```javascript +// @enableNewMutationAliasingModel +import {identity, mutate, mutateAndReturn} from 'shared-runtime'; + +function Component(props) { + const key = {a: 'key'}; + const context = { + [key.a]: identity([props.value]), + }; + mutate(key); + return context; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{value: 42}], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel +import { identity, mutate, mutateAndReturn } from "shared-runtime"; + +function Component(props) { + const $ = _c(4); + let context; + if ($[0] !== props.value) { + const key = { a: "key" }; + + const t0 = key.a; + const t1 = identity([props.value]); + let t2; + if ($[2] !== t1) { + t2 = { [t0]: t1 }; + $[2] = t1; + $[3] = t2; + } else { + t2 = $[3]; + } + context = t2; + + mutate(key); + $[0] = props.value; + $[1] = context; + } else { + context = $[1]; + } + return context; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ value: 42 }], +}; + +``` + +### Eval output +(kind: ok) {"key":[42]} \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/object-expression-computed-member.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/object-expression-computed-member.js new file mode 100644 index 0000000000000..516fdc1dbcf41 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/object-expression-computed-member.js @@ -0,0 +1,16 @@ +// @enableNewMutationAliasingModel +import {identity, mutate, mutateAndReturn} from 'shared-runtime'; + +function Component(props) { + const key = {a: 'key'}; + const context = { + [key.a]: identity([props.value]), + }; + mutate(key); + return context; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{value: 42}], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/potential-mutation-in-function-expression.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/potential-mutation-in-function-expression.expect.md new file mode 100644 index 0000000000000..a5cfc790ebc06 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/potential-mutation-in-function-expression.expect.md @@ -0,0 +1,64 @@ + +## Input + +```javascript +// @enableNewMutationAliasingModel +function Component({a, b, c}) { + const x = [a, b]; + const f = () => { + maybeMutate(x); + // different dependency to force this not to merge with x's scope + console.log(c); + }; + return ; +} + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel +function Component(t0) { + const $ = _c(9); + const { a, b, c } = t0; + let t1; + if ($[0] !== a || $[1] !== b) { + t1 = [a, b]; + $[0] = a; + $[1] = b; + $[2] = t1; + } else { + t1 = $[2]; + } + const x = t1; + let t2; + if ($[3] !== c || $[4] !== x) { + t2 = () => { + maybeMutate(x); + + console.log(c); + }; + $[3] = c; + $[4] = x; + $[5] = t2; + } else { + t2 = $[5]; + } + const f = t2; + let t3; + if ($[6] !== f || $[7] !== x) { + t3 = ; + $[6] = f; + $[7] = x; + $[8] = t3; + } else { + t3 = $[8]; + } + return t3; +} + +``` + +### 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/new-mutability/potential-mutation-in-function-expression.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/potential-mutation-in-function-expression.js new file mode 100644 index 0000000000000..096f4f17ea545 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/potential-mutation-in-function-expression.js @@ -0,0 +1,10 @@ +// @enableNewMutationAliasingModel +function Component({a, b, c}) { + const x = [a, b]; + const f = () => { + maybeMutate(x); + // different dependency to force this not to merge with x's scope + console.log(c); + }; + return ; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/reactive-ref.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/reactive-ref.expect.md new file mode 100644 index 0000000000000..26757db1a3c28 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/reactive-ref.expect.md @@ -0,0 +1,54 @@ + +## Input + +```javascript +// @enableNewMutationAliasingModel +function ReactiveRefInEffect(props) { + const ref1 = useRef('initial value'); + const ref2 = useRef('initial value'); + let ref; + if (props.foo) { + ref = ref1; + } else { + ref = ref2; + } + useEffect(() => print(ref)); +} + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel +function ReactiveRefInEffect(props) { + const $ = _c(4); + const ref1 = useRef("initial value"); + const ref2 = useRef("initial value"); + let ref; + if ($[0] !== props.foo) { + if (props.foo) { + ref = ref1; + } else { + ref = ref2; + } + $[0] = props.foo; + $[1] = ref; + } else { + ref = $[1]; + } + let t0; + if ($[2] !== ref) { + t0 = () => print(ref); + $[2] = ref; + $[3] = t0; + } else { + t0 = $[3]; + } + useEffect(t0); +} + +``` + +### 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/new-mutability/reactive-ref.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/reactive-ref.js new file mode 100644 index 0000000000000..3ae653c962034 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/reactive-ref.js @@ -0,0 +1,12 @@ +// @enableNewMutationAliasingModel +function ReactiveRefInEffect(props) { + const ref1 = useRef('initial value'); + const ref2 = useRef('initial value'); + let ref; + if (props.foo) { + ref = ref1; + } else { + ref = ref2; + } + useEffect(() => print(ref)); +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/reactive-setState.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/reactive-setState.expect.md new file mode 100644 index 0000000000000..9f9786e4be830 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/reactive-setState.expect.md @@ -0,0 +1,66 @@ + +## Input + +```javascript +// @inferEffectDependencies @enableNewMutationAliasingModel +import {useEffect, useState, AUTODEPS} from 'react'; +import {print} from 'shared-runtime'; + +/* + * setState types are not enough to determine to omit from deps. Must also take reactivity into account. + */ +function ReactiveRefInEffect(props) { + const [_state1, setState1] = useRef('initial value'); + const [_state2, setState2] = useRef('initial value'); + let setState; + if (props.foo) { + setState = setState1; + } else { + setState = setState2; + } + useEffect(() => print(setState), AUTODEPS); +} + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; // @inferEffectDependencies @enableNewMutationAliasingModel +import { useEffect, useState, AUTODEPS } from "react"; +import { print } from "shared-runtime"; + +/* + * setState types are not enough to determine to omit from deps. Must also take reactivity into account. + */ +function ReactiveRefInEffect(props) { + const $ = _c(4); + const [, setState1] = useRef("initial value"); + const [, setState2] = useRef("initial value"); + let setState; + if ($[0] !== props.foo) { + if (props.foo) { + setState = setState1; + } else { + setState = setState2; + } + $[0] = props.foo; + $[1] = setState; + } else { + setState = $[1]; + } + let t0; + if ($[2] !== setState) { + t0 = () => print(setState); + $[2] = setState; + $[3] = t0; + } else { + t0 = $[3]; + } + useEffect(t0, [setState]); +} + +``` + +### 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/new-mutability/reactive-setState.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/reactive-setState.js new file mode 100644 index 0000000000000..d351998032437 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/reactive-setState.js @@ -0,0 +1,18 @@ +// @inferEffectDependencies @enableNewMutationAliasingModel +import {useEffect, useState, AUTODEPS} from 'react'; +import {print} from 'shared-runtime'; + +/* + * setState types are not enough to determine to omit from deps. Must also take reactivity into account. + */ +function ReactiveRefInEffect(props) { + const [_state1, setState1] = useRef('initial value'); + const [_state2, setState2] = useRef('initial value'); + let setState; + if (props.foo) { + setState = setState1; + } else { + setState = setState2; + } + useEffect(() => print(setState), AUTODEPS); +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/repro-compiler-infinite-loop.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/repro-compiler-infinite-loop.expect.md new file mode 100644 index 0000000000000..dfc4ed988309a --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/repro-compiler-infinite-loop.expect.md @@ -0,0 +1,54 @@ + +## Input + +```javascript +// @flow @enableNewMutationAliasingModel + +import fbt from 'fbt'; + +component Component() { + const sections = Object.keys(items); + + for (let i = 0; i < sections.length; i += 3) { + chunks.push( + sections.slice(i, i + 3).map(section => { + return ; + }) + ); + } + + return ; +} + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; + +import fbt from "fbt"; + +function Component() { + const $ = _c(1); + const sections = Object.keys(items); + for (let i = 0; i < sections.length; i = i + 3, i) { + chunks.push(sections.slice(i, i + 3).map(_temp)); + } + let t0; + if ($[0] === Symbol.for("react.memo_cache_sentinel")) { + t0 = ; + $[0] = t0; + } else { + t0 = $[0]; + } + return t0; +} +function _temp(section) { + return ; +} + +``` + +### 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/new-mutability/repro-compiler-infinite-loop.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/repro-compiler-infinite-loop.js new file mode 100644 index 0000000000000..d03a44618ea01 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/repro-compiler-infinite-loop.js @@ -0,0 +1,17 @@ +// @flow @enableNewMutationAliasingModel + +import fbt from 'fbt'; + +component Component() { + const sections = Object.keys(items); + + for (let i = 0; i < sections.length; i += 3) { + chunks.push( + sections.slice(i, i + 3).map(section => { + return ; + }) + ); + } + + return ; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/repro-function-expression-effects-stack-overflow.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/repro-function-expression-effects-stack-overflow.expect.md new file mode 100644 index 0000000000000..9d168c9e5c1ea --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/repro-function-expression-effects-stack-overflow.expect.md @@ -0,0 +1,60 @@ + +## Input + +```javascript +function Component() { + const x = {}; + const fn = () => { + new Object() + .build(x) + .build({}) + .build({}) + .build({}) + .build({}) + .build({}) + .build({}); + }; + return ; +} + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; +function Component() { + const $ = _c(2); + let t0; + if ($[0] === Symbol.for("react.memo_cache_sentinel")) { + t0 = {}; + $[0] = t0; + } else { + t0 = $[0]; + } + const x = t0; + let t1; + if ($[1] === Symbol.for("react.memo_cache_sentinel")) { + const fn = () => { + new Object() + .build(x) + .build({}) + .build({}) + .build({}) + .build({}) + .build({}) + .build({}); + }; + + t1 = ; + $[1] = t1; + } else { + t1 = $[1]; + } + return t1; +} + +``` + +### 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/new-mutability/repro-function-expression-effects-stack-overflow.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/repro-function-expression-effects-stack-overflow.js new file mode 100644 index 0000000000000..6e67ed7bab695 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/repro-function-expression-effects-stack-overflow.js @@ -0,0 +1,14 @@ +function Component() { + const x = {}; + const fn = () => { + new Object() + .build(x) + .build({}) + .build({}) + .build({}) + .build({}) + .build({}) + .build({}); + }; + return ; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/repro-internal-compiler-shared-mutablerange-bug.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/repro-internal-compiler-shared-mutablerange-bug.expect.md new file mode 100644 index 0000000000000..9a0c82a3ccca0 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/repro-internal-compiler-shared-mutablerange-bug.expect.md @@ -0,0 +1,80 @@ + +## Input + +```javascript +//@flow @validatePreserveExistingMemoizationGuarantees @enableNewMutationAliasingModel +component Component( + onAsyncSubmit?: (() => void) => void, + onClose: (isConfirmed: boolean) => void +) { + // When running inferReactiveScopeVariables, + // onAsyncSubmit and onClose update to share + // a mutableRange instance. + const onSubmit = useCallback(() => { + if (onAsyncSubmit) { + onAsyncSubmit(() => { + onClose(true); + }); + return; + } + }, [onAsyncSubmit, onClose]); + // When running inferReactiveScopeVariables here, + // first the existing range gets updated (affecting + // onAsyncSubmit) and then onClose gets assigned a + // different mutable range instance, which is the + // one reset after AnalyzeFunctions. + // The fix is to fully reset mutable ranges *instances* + // after AnalyzeFunctions visit a function expression + return onClose(false)} />; +} + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; +function Component(t0) { + const $ = _c(8); + const { onAsyncSubmit, onClose } = t0; + let t1; + if ($[0] !== onAsyncSubmit || $[1] !== onClose) { + t1 = () => { + if (onAsyncSubmit) { + onAsyncSubmit(() => { + onClose(true); + }); + return; + } + }; + $[0] = onAsyncSubmit; + $[1] = onClose; + $[2] = t1; + } else { + t1 = $[2]; + } + const onSubmit = t1; + let t2; + if ($[3] !== onClose) { + t2 = () => onClose(false); + $[3] = onClose; + $[4] = t2; + } else { + t2 = $[4]; + } + let t3; + if ($[5] !== onSubmit || $[6] !== t2) { + t3 = ; + $[5] = onSubmit; + $[6] = t2; + $[7] = t3; + } else { + t3 = $[7]; + } + return t3; +} + +``` + +### 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/new-mutability/repro-internal-compiler-shared-mutablerange-bug.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/repro-internal-compiler-shared-mutablerange-bug.js new file mode 100644 index 0000000000000..20cad06e97e2e --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/repro-internal-compiler-shared-mutablerange-bug.js @@ -0,0 +1,25 @@ +//@flow @validatePreserveExistingMemoizationGuarantees @enableNewMutationAliasingModel +component Component( + onAsyncSubmit?: (() => void) => void, + onClose: (isConfirmed: boolean) => void +) { + // When running inferReactiveScopeVariables, + // onAsyncSubmit and onClose update to share + // a mutableRange instance. + const onSubmit = useCallback(() => { + if (onAsyncSubmit) { + onAsyncSubmit(() => { + onClose(true); + }); + return; + } + }, [onAsyncSubmit, onClose]); + // When running inferReactiveScopeVariables here, + // first the existing range gets updated (affecting + // onAsyncSubmit) and then onClose gets assigned a + // different mutable range instance, which is the + // one reset after AnalyzeFunctions. + // The fix is to fully reset mutable ranges *instances* + // after AnalyzeFunctions visit a function expression + return onClose(false)} />; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/repro-invalid-function-expression-effects-phi.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/repro-invalid-function-expression-effects-phi.expect.md new file mode 100644 index 0000000000000..73cf419be14d3 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/repro-invalid-function-expression-effects-phi.expect.md @@ -0,0 +1,60 @@ + +## Input + +```javascript +function Component({a, b}) { + const y = {a}; + const x = {b}; + const f = () => { + let z = null; + while (z == null) { + z = x; + } + // z is a phi with a backedge, and we don't realize it could be x, + // and therefore fail to record a Capture x <- y effect for this + // function expression + z.y = y; + }; + f(); + mutate(x); + return
{x}
; +} + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; +function Component(t0) { + const $ = _c(3); + const { a, b } = t0; + let t1; + if ($[0] !== a || $[1] !== b) { + const y = { a }; + const x = { b }; + const f = () => { + let z = null; + while (z == null) { + z = x; + } + + z.y = y; + }; + + f(); + mutate(x); + t1 =
{x}
; + $[0] = a; + $[1] = b; + $[2] = t1; + } else { + t1 = $[2]; + } + return t1; +} + +``` + +### 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/new-mutability/repro-invalid-function-expression-effects-phi.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/repro-invalid-function-expression-effects-phi.js new file mode 100644 index 0000000000000..31a51b45aa384 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/repro-invalid-function-expression-effects-phi.js @@ -0,0 +1,17 @@ +function Component({a, b}) { + const y = {a}; + const x = {b}; + const f = () => { + let z = null; + while (z == null) { + z = x; + } + // z is a phi with a backedge, and we don't realize it could be x, + // and therefore fail to record a Capture x <- y effect for this + // function expression + z.y = y; + }; + f(); + mutate(x); + return
{x}
; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/repro-jsx-captures-value-mutated-later.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/repro-jsx-captures-value-mutated-later.expect.md new file mode 100644 index 0000000000000..109219e03ada0 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/repro-jsx-captures-value-mutated-later.expect.md @@ -0,0 +1,53 @@ + +## Input + +```javascript +// @flow @enableNewMutationAliasingModel + +import {identity, Stringify, useFragment} from 'shared-runtime'; + +component Example() { + const data = useFragment(); + + const {a, b} = identity(data); + + const el = ; + + identity(a.at(0)); + + return ; +} + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; + +import { identity, Stringify, useFragment } from "shared-runtime"; + +function Example() { + const $ = _c(2); + const data = useFragment(); + let t0; + if ($[0] !== data) { + const { a, b } = identity(data); + + const el = ; + + identity(a.at(0)); + + t0 = ; + $[0] = data; + $[1] = t0; + } else { + t0 = $[1]; + } + return t0; +} + +``` + +### 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/new-mutability/repro-jsx-captures-value-mutated-later.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/repro-jsx-captures-value-mutated-later.js new file mode 100644 index 0000000000000..7ab6dbc30ab2c --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/repro-jsx-captures-value-mutated-later.js @@ -0,0 +1,15 @@ +// @flow @enableNewMutationAliasingModel + +import {identity, Stringify, useFragment} from 'shared-runtime'; + +component Example() { + const data = useFragment(); + + const {a, b} = identity(data); + + const el = ; + + identity(a.at(0)); + + return ; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/repro-mutate-new-set-of-frozen-items-in-callback.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/repro-mutate-new-set-of-frozen-items-in-callback.expect.md new file mode 100644 index 0000000000000..28fc8b601f7ff --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/repro-mutate-new-set-of-frozen-items-in-callback.expect.md @@ -0,0 +1,74 @@ + +## Input + +```javascript +// @enableNewMutationAliasingModel:true + +export const App = () => { + const [selected, setSelected] = useState(new Set()); + const onSelectedChange = (value: string) => { + const newSelected = new Set(selected); + if (newSelected.has(value)) { + // This should not count as a mutation of `selected` + newSelected.delete(value); + } else { + // This should not count as a mutation of `selected` + newSelected.add(value); + } + setSelected(newSelected); + }; + + return ; +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel:true + +export const App = () => { + const $ = _c(6); + let t0; + if ($[0] === Symbol.for("react.memo_cache_sentinel")) { + t0 = new Set(); + $[0] = t0; + } else { + t0 = $[0]; + } + const [selected, setSelected] = useState(t0); + let t1; + if ($[1] !== selected) { + t1 = (value) => { + const newSelected = new Set(selected); + if (newSelected.has(value)) { + newSelected.delete(value); + } else { + newSelected.add(value); + } + + setSelected(newSelected); + }; + $[1] = selected; + $[2] = t1; + } else { + t1 = $[2]; + } + const onSelectedChange = t1; + let t2; + if ($[3] !== onSelectedChange || $[4] !== selected) { + t2 = ; + $[3] = onSelectedChange; + $[4] = selected; + $[5] = t2; + } else { + t2 = $[5]; + } + 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/new-mutability/repro-mutate-new-set-of-frozen-items-in-callback.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/repro-mutate-new-set-of-frozen-items-in-callback.js new file mode 100644 index 0000000000000..c5a404a66c371 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/repro-mutate-new-set-of-frozen-items-in-callback.js @@ -0,0 +1,18 @@ +// @enableNewMutationAliasingModel:true + +export const App = () => { + const [selected, setSelected] = useState(new Set()); + const onSelectedChange = (value: string) => { + const newSelected = new Set(selected); + if (newSelected.has(value)) { + // This should not count as a mutation of `selected` + newSelected.delete(value); + } else { + // This should not count as a mutation of `selected` + newSelected.add(value); + } + setSelected(newSelected); + }; + + return ; +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/retry-no-emit.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/retry-no-emit.expect.md new file mode 100644 index 0000000000000..7d490de8c6ae3 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/retry-no-emit.expect.md @@ -0,0 +1,66 @@ + +## Input + +```javascript +// @inferEffectDependencies @noEmit @panicThreshold:"none" @loggerTestOnly @enableNewMutationAliasingModel +import {print} from 'shared-runtime'; +import useEffectWrapper from 'useEffectWrapper'; +import {AUTODEPS} from 'react'; + +function Foo({propVal}) { + const arr = [propVal]; + useEffectWrapper(() => print(arr), AUTODEPS); + + const arr2 = []; + useEffectWrapper(() => arr2.push(propVal), AUTODEPS); + arr2.push(2); + return {arr, arr2}; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [{propVal: 1}], + sequentialRenders: [{propVal: 1}, {propVal: 2}], +}; + +``` + +## Code + +```javascript +// @inferEffectDependencies @noEmit @panicThreshold:"none" @loggerTestOnly @enableNewMutationAliasingModel +import { print } from "shared-runtime"; +import useEffectWrapper from "useEffectWrapper"; +import { AUTODEPS } from "react"; + +function Foo({ propVal }) { + const arr = [propVal]; + useEffectWrapper(() => print(arr), AUTODEPS); + + const arr2 = []; + useEffectWrapper(() => arr2.push(propVal), AUTODEPS); + arr2.push(2); + return { arr, arr2 }; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [{ propVal: 1 }], + sequentialRenders: [{ propVal: 1 }, { propVal: 2 }], +}; + +``` + +## Logs + +``` +{"kind":"CompileError","fnLoc":{"start":{"line":6,"column":0,"index":227},"end":{"line":14,"column":1,"index":441},"filename":"retry-no-emit.ts"},"detail":{"options":{"category":"Immutability","severity":"InvalidReact","reason":"This value cannot be modified","description":"Modifying a value previously passed as an argument to a hook is not allowed. Consider moving the modification before calling the hook.","details":[{"kind":"error","loc":{"start":{"line":12,"column":2,"index":404},"end":{"line":12,"column":6,"index":408},"filename":"retry-no-emit.ts","identifierName":"arr2"},"message":"value cannot be modified"}]}}} +{"kind":"AutoDepsDecorations","fnLoc":{"start":{"line":8,"column":2,"index":280},"end":{"line":8,"column":46,"index":324},"filename":"retry-no-emit.ts"},"decorations":[{"start":{"line":8,"column":31,"index":309},"end":{"line":8,"column":34,"index":312},"filename":"retry-no-emit.ts","identifierName":"arr"}]} +{"kind":"AutoDepsDecorations","fnLoc":{"start":{"line":11,"column":2,"index":348},"end":{"line":11,"column":54,"index":400},"filename":"retry-no-emit.ts"},"decorations":[{"start":{"line":11,"column":25,"index":371},"end":{"line":11,"column":29,"index":375},"filename":"retry-no-emit.ts","identifierName":"arr2"},{"start":{"line":11,"column":25,"index":371},"end":{"line":11,"column":29,"index":375},"filename":"retry-no-emit.ts","identifierName":"arr2"},{"start":{"line":11,"column":35,"index":381},"end":{"line":11,"column":42,"index":388},"filename":"retry-no-emit.ts","identifierName":"propVal"}]} +{"kind":"CompileSuccess","fnLoc":{"start":{"line":6,"column":0,"index":227},"end":{"line":14,"column":1,"index":441},"filename":"retry-no-emit.ts"},"fnName":"Foo","memoSlots":0,"memoBlocks":0,"memoValues":0,"prunedMemoBlocks":0,"prunedMemoValues":0} +``` + +### Eval output +(kind: ok) {"arr":[1],"arr2":[2]} +{"arr":[2],"arr2":[2]} +logs: [[ 1 ],[ 2 ]] \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/retry-no-emit.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/retry-no-emit.js new file mode 100644 index 0000000000000..2815a9a28d12e --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/retry-no-emit.js @@ -0,0 +1,20 @@ +// @inferEffectDependencies @noEmit @panicThreshold:"none" @loggerTestOnly @enableNewMutationAliasingModel +import {print} from 'shared-runtime'; +import useEffectWrapper from 'useEffectWrapper'; +import {AUTODEPS} from 'react'; + +function Foo({propVal}) { + const arr = [propVal]; + useEffectWrapper(() => print(arr), AUTODEPS); + + const arr2 = []; + useEffectWrapper(() => arr2.push(propVal), AUTODEPS); + arr2.push(2); + return {arr, arr2}; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [{propVal: 1}], + sequentialRenders: [{propVal: 1}, {propVal: 2}], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/set-add-mutate.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/set-add-mutate.expect.md new file mode 100644 index 0000000000000..955c4e0705797 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/set-add-mutate.expect.md @@ -0,0 +1,54 @@ + +## Input + +```javascript +// @enableNewMutationAliasingModel +function useHook({el1, el2}) { + const s = new Set(); + const arr = makeArray(el1); + s.add(arr); + // Mutate after store + arr.push(el2); + + s.add(makeArray(el2)); + return s.size; +} + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel +function useHook(t0) { + const $ = _c(5); + const { el1, el2 } = t0; + let s; + if ($[0] !== el1 || $[1] !== el2) { + s = new Set(); + const arr = makeArray(el1); + s.add(arr); + + arr.push(el2); + let t1; + if ($[3] !== el2) { + t1 = makeArray(el2); + $[3] = el2; + $[4] = t1; + } else { + t1 = $[4]; + } + s.add(t1); + $[0] = el1; + $[1] = el2; + $[2] = s; + } else { + s = $[2]; + } + return s.size; +} + +``` + +### 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/new-mutability/set-add-mutate.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/set-add-mutate.js new file mode 100644 index 0000000000000..3afbd93f84b17 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/set-add-mutate.js @@ -0,0 +1,11 @@ +// @enableNewMutationAliasingModel +function useHook({el1, el2}) { + const s = new Set(); + const arr = makeArray(el1); + s.add(arr); + // Mutate after store + arr.push(el2); + + s.add(makeArray(el2)); + return s.size; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/shared-hook-calls.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/shared-hook-calls.expect.md new file mode 100644 index 0000000000000..39bd61aaf34a7 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/shared-hook-calls.expect.md @@ -0,0 +1,80 @@ + +## Input + +```javascript +// @enableFire @enableNewMutationAliasingModel +import {fire} from 'react'; + +function Component({bar, baz}) { + const foo = () => { + console.log(bar); + }; + useEffect(() => { + fire(foo(bar)); + fire(baz(bar)); + }); + + useEffect(() => { + fire(foo(bar)); + }); + + return null; +} + +``` + +## Code + +```javascript +import { c as _c, useFire } from "react/compiler-runtime"; // @enableFire @enableNewMutationAliasingModel +import { fire } from "react"; + +function Component(t0) { + const $ = _c(9); + const { bar, baz } = t0; + let t1; + if ($[0] !== bar) { + t1 = () => { + console.log(bar); + }; + $[0] = bar; + $[1] = t1; + } else { + t1 = $[1]; + } + const foo = t1; + const t2 = useFire(foo); + const t3 = useFire(baz); + let t4; + if ($[2] !== bar || $[3] !== t2 || $[4] !== t3) { + t4 = () => { + t2(bar); + t3(bar); + }; + $[2] = bar; + $[3] = t2; + $[4] = t3; + $[5] = t4; + } else { + t4 = $[5]; + } + useEffect(t4); + let t5; + if ($[6] !== bar || $[7] !== t2) { + t5 = () => { + t2(bar); + }; + $[6] = bar; + $[7] = t2; + $[8] = t5; + } else { + t5 = $[8]; + } + useEffect(t5); + return null; +} + +``` + +### 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/new-mutability/shared-hook-calls.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/shared-hook-calls.js new file mode 100644 index 0000000000000..54d4cf83fe310 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/shared-hook-calls.js @@ -0,0 +1,18 @@ +// @enableFire @enableNewMutationAliasingModel +import {fire} from 'react'; + +function Component({bar, baz}) { + const foo = () => { + console.log(bar); + }; + useEffect(() => { + fire(foo(bar)); + fire(baz(bar)); + }); + + useEffect(() => { + fire(foo(bar)); + }); + + return null; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/ssa-renaming-ternary-destruction.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/ssa-renaming-ternary-destruction.expect.md new file mode 100644 index 0000000000000..4c04ae197292f --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/ssa-renaming-ternary-destruction.expect.md @@ -0,0 +1,70 @@ + +## Input + +```javascript +// @enablePropagateDepsInHIR @enableNewMutationAliasingModel +function useFoo(props) { + let x = []; + x.push(props.bar); + // todo: the below should memoize separately from the above + // my guess is that the phi causes the different `x` identifiers + // to get added to an alias group. this is where we need to track + // the actual state of the alias groups at the time of the mutation + props.cond ? (({x} = {x: {}}), ([x] = [[]]), x.push(props.foo)) : null; + return x; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [{cond: false, foo: 2, bar: 55}], + sequentialRenders: [ + {cond: false, foo: 2, bar: 55}, + {cond: false, foo: 3, bar: 55}, + {cond: true, foo: 3, bar: 55}, + ], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; // @enablePropagateDepsInHIR @enableNewMutationAliasingModel +function useFoo(props) { + const $ = _c(5); + let x; + if ($[0] !== props.bar) { + x = []; + x.push(props.bar); + $[0] = props.bar; + $[1] = x; + } else { + x = $[1]; + } + if ($[2] !== props.cond || $[3] !== props.foo) { + props.cond ? (([x] = [[]]), x.push(props.foo)) : null; + $[2] = props.cond; + $[3] = props.foo; + $[4] = x; + } else { + x = $[4]; + } + return x; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [{ cond: false, foo: 2, bar: 55 }], + sequentialRenders: [ + { cond: false, foo: 2, bar: 55 }, + { cond: false, foo: 3, bar: 55 }, + { cond: true, foo: 3, bar: 55 }, + ], +}; + +``` + +### Eval output +(kind: ok) [55] +[55] +[3] \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/ssa-renaming-ternary-destruction.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/ssa-renaming-ternary-destruction.js new file mode 100644 index 0000000000000..923d0b59bb810 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/ssa-renaming-ternary-destruction.js @@ -0,0 +1,21 @@ +// @enablePropagateDepsInHIR @enableNewMutationAliasingModel +function useFoo(props) { + let x = []; + x.push(props.bar); + // todo: the below should memoize separately from the above + // my guess is that the phi causes the different `x` identifiers + // to get added to an alias group. this is where we need to track + // the actual state of the alias groups at the time of the mutation + props.cond ? (({x} = {x: {}}), ([x] = [[]]), x.push(props.foo)) : null; + return x; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [{cond: false, foo: 2, bar: 55}], + sequentialRenders: [ + {cond: false, foo: 2, bar: 55}, + {cond: false, foo: 3, bar: 55}, + {cond: true, foo: 3, bar: 55}, + ], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/todo-control-flow-sensitive-mutation.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/todo-control-flow-sensitive-mutation.expect.md new file mode 100644 index 0000000000000..0a31e02ae25e7 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/todo-control-flow-sensitive-mutation.expect.md @@ -0,0 +1,157 @@ + +## Input + +```javascript +import {useMemo} from 'react'; +import { + mutate, + typedCapture, + typedCreateFrom, + typedMutate, + ValidateMemoization, +} from 'shared-runtime'; + +function Component({a, b, c}: {a: number; b: number; c: number}) { + const x = useMemo(() => [{value: a}], [a, b, c]); + if (b === 0) { + // This object should only depend on c, it cannot be affected by the later mutation + x.push({value: c}); + } else { + // This mutation shouldn't affect the object in the consequent + mutate(x); + } + + return ( + <> + ; + {/* TODO: should only depend on c */} + ; + + ); +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{a: 0, b: 0, c: 0}], + sequentialRenders: [ + {a: 0, b: 0, c: 0}, + {a: 0, b: 1, c: 0}, + {a: 1, b: 1, c: 0}, + {a: 1, b: 1, c: 1}, + {a: 1, b: 1, c: 0}, + {a: 1, b: 0, c: 0}, + {a: 0, b: 0, c: 0}, + ], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; +import { useMemo } from "react"; +import { + mutate, + typedCapture, + typedCreateFrom, + typedMutate, + ValidateMemoization, +} from "shared-runtime"; + +function Component(t0) { + const $ = _c(21); + const { a, b, c } = t0; + let x; + if ($[0] !== a || $[1] !== b || $[2] !== c) { + x = [{ value: a }]; + if (b === 0) { + x.push({ value: c }); + } else { + mutate(x); + } + $[0] = a; + $[1] = b; + $[2] = c; + $[3] = x; + } else { + x = $[3]; + } + let t1; + if ($[4] !== a || $[5] !== b || $[6] !== c) { + t1 = [a, b, c]; + $[4] = a; + $[5] = b; + $[6] = c; + $[7] = t1; + } else { + t1 = $[7]; + } + let t2; + if ($[8] !== t1 || $[9] !== x) { + t2 = ; + $[8] = t1; + $[9] = x; + $[10] = t2; + } else { + t2 = $[10]; + } + let t3; + if ($[11] !== a || $[12] !== b || $[13] !== c) { + t3 = [a, b, c]; + $[11] = a; + $[12] = b; + $[13] = c; + $[14] = t3; + } else { + t3 = $[14]; + } + let t4; + if ($[15] !== t3 || $[16] !== x[0]) { + t4 = ; + $[15] = t3; + $[16] = x[0]; + $[17] = t4; + } else { + t4 = $[17]; + } + let t5; + if ($[18] !== t2 || $[19] !== t4) { + t5 = ( + <> + {t2};{t4}; + + ); + $[18] = t2; + $[19] = t4; + $[20] = t5; + } else { + t5 = $[20]; + } + return t5; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ a: 0, b: 0, c: 0 }], + sequentialRenders: [ + { a: 0, b: 0, c: 0 }, + { a: 0, b: 1, c: 0 }, + { a: 1, b: 1, c: 0 }, + { a: 1, b: 1, c: 1 }, + { a: 1, b: 1, c: 0 }, + { a: 1, b: 0, c: 0 }, + { a: 0, b: 0, c: 0 }, + ], +}; + +``` + +### Eval output +(kind: ok)
{"inputs":[0,0,0],"output":[{"value":0},{"value":0}]}
;
{"inputs":[0,0,0],"output":{"value":0}}
; +
{"inputs":[0,1,0],"output":[{"value":0},"joe"]}
;
{"inputs":[0,1,0],"output":{"value":0}}
; +
{"inputs":[1,1,0],"output":[{"value":1},"joe"]}
;
{"inputs":[1,1,0],"output":{"value":1}}
; +
{"inputs":[1,1,1],"output":[{"value":1},"joe"]}
;
{"inputs":[1,1,1],"output":{"value":1}}
; +
{"inputs":[1,1,0],"output":[{"value":1},"joe"]}
;
{"inputs":[1,1,0],"output":{"value":1}}
; +
{"inputs":[1,0,0],"output":[{"value":1},{"value":0}]}
;
{"inputs":[1,0,0],"output":{"value":1}}
; +
{"inputs":[0,0,0],"output":[{"value":0},{"value":0}]}
;
{"inputs":[0,0,0],"output":{"value":0}}
; \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/todo-control-flow-sensitive-mutation.tsx b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/todo-control-flow-sensitive-mutation.tsx new file mode 100644 index 0000000000000..61f8c47e453d4 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/todo-control-flow-sensitive-mutation.tsx @@ -0,0 +1,41 @@ +import {useMemo} from 'react'; +import { + mutate, + typedCapture, + typedCreateFrom, + typedMutate, + ValidateMemoization, +} from 'shared-runtime'; + +function Component({a, b, c}: {a: number; b: number; c: number}) { + const x = useMemo(() => [{value: a}], [a, b, c]); + if (b === 0) { + // This object should only depend on c, it cannot be affected by the later mutation + x.push({value: c}); + } else { + // This mutation shouldn't affect the object in the consequent + mutate(x); + } + + return ( + <> + ; + {/* TODO: should only depend on c */} + ; + + ); +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{a: 0, b: 0, c: 0}], + sequentialRenders: [ + {a: 0, b: 0, c: 0}, + {a: 0, b: 1, c: 0}, + {a: 1, b: 1, c: 0}, + {a: 1, b: 1, c: 1}, + {a: 1, b: 1, c: 0}, + {a: 1, b: 0, c: 0}, + {a: 0, b: 0, c: 0}, + ], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/todo-transitivity-createfrom-capture-lambda.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/todo-transitivity-createfrom-capture-lambda.expect.md new file mode 100644 index 0000000000000..c985809353e45 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/todo-transitivity-createfrom-capture-lambda.expect.md @@ -0,0 +1,112 @@ + +## Input + +```javascript +import {useMemo} from 'react'; +import { + typedCapture, + typedCreateFrom, + typedMutate, + ValidateMemoization, +} from 'shared-runtime'; + +function Component({a, b}) { + const x = useMemo(() => [{a}], [a]); + const f = () => { + const y = typedCreateFrom(x); + const z = typedCapture(y); + return z; + }; + const z = f(); + // does not mutate x, so x should not depend on b + typedMutate(z, b); + + // TODO: this *should* only depend on `a` + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{a: 0, b: 0}], + sequentialRenders: [ + {a: 0, b: 0}, + {a: 0, b: 1}, + {a: 1, b: 1}, + {a: 0, b: 0}, + ], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; +import { useMemo } from "react"; +import { + typedCapture, + typedCreateFrom, + typedMutate, + ValidateMemoization, +} from "shared-runtime"; + +function Component(t0) { + const $ = _c(9); + const { a, b } = t0; + let x; + if ($[0] !== a || $[1] !== b) { + x = [{ a }]; + const f = () => { + const y = typedCreateFrom(x); + const z = typedCapture(y); + return z; + }; + + const z_0 = f(); + + typedMutate(z_0, b); + $[0] = a; + $[1] = b; + $[2] = x; + } else { + x = $[2]; + } + let t1; + if ($[3] !== a || $[4] !== b) { + t1 = [a, b]; + $[3] = a; + $[4] = b; + $[5] = t1; + } else { + t1 = $[5]; + } + let t2; + if ($[6] !== t1 || $[7] !== x) { + t2 = ; + $[6] = t1; + $[7] = x; + $[8] = t2; + } else { + t2 = $[8]; + } + return t2; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ a: 0, b: 0 }], + sequentialRenders: [ + { a: 0, b: 0 }, + { a: 0, b: 1 }, + { a: 1, b: 1 }, + { a: 0, b: 0 }, + ], +}; + +``` + +### Eval output +(kind: ok)
{"inputs":[0,0],"output":[{"a":0}]}
+
{"inputs":[0,1],"output":[{"a":0}]}
+
{"inputs":[1,1],"output":[{"a":1}]}
+
{"inputs":[0,0],"output":[{"a":0}]}
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/todo-transitivity-createfrom-capture-lambda.tsx b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/todo-transitivity-createfrom-capture-lambda.tsx new file mode 100644 index 0000000000000..d6bd1690f6557 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/todo-transitivity-createfrom-capture-lambda.tsx @@ -0,0 +1,33 @@ +import {useMemo} from 'react'; +import { + typedCapture, + typedCreateFrom, + typedMutate, + ValidateMemoization, +} from 'shared-runtime'; + +function Component({a, b}) { + const x = useMemo(() => [{a}], [a]); + const f = () => { + const y = typedCreateFrom(x); + const z = typedCapture(y); + return z; + }; + const z = f(); + // does not mutate x, so x should not depend on b + typedMutate(z, b); + + // TODO: this *should* only depend on `a` + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{a: 0, b: 0}], + sequentialRenders: [ + {a: 0, b: 0}, + {a: 0, b: 1}, + {a: 1, b: 1}, + {a: 0, b: 0}, + ], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/transitive-mutation-before-capturing-value-created-earlier.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/transitive-mutation-before-capturing-value-created-earlier.expect.md new file mode 100644 index 0000000000000..09c4e3eaf3332 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/transitive-mutation-before-capturing-value-created-earlier.expect.md @@ -0,0 +1,50 @@ + +## Input + +```javascript +// @enableNewMutationAliasingModel +function Component({a, b}) { + const x = [a]; + const y = {b}; + mutate(y); + y.x = x; + return
{y}
; +} + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel +function Component(t0) { + const $ = _c(5); + const { a, b } = t0; + let t1; + if ($[0] !== a) { + t1 = [a]; + $[0] = a; + $[1] = t1; + } else { + t1 = $[1]; + } + const x = t1; + let t2; + if ($[2] !== b || $[3] !== x) { + const y = { b }; + mutate(y); + y.x = x; + t2 =
{y}
; + $[2] = b; + $[3] = x; + $[4] = t2; + } else { + t2 = $[4]; + } + 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/new-mutability/transitive-mutation-before-capturing-value-created-earlier.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/transitive-mutation-before-capturing-value-created-earlier.js new file mode 100644 index 0000000000000..e6e2e17bc0b96 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/transitive-mutation-before-capturing-value-created-earlier.js @@ -0,0 +1,8 @@ +// @enableNewMutationAliasingModel +function Component({a, b}) { + const x = [a]; + const y = {b}; + mutate(y); + y.x = x; + return
{y}
; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/transitivity-add-captured-array-to-itself.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/transitivity-add-captured-array-to-itself.expect.md new file mode 100644 index 0000000000000..4f665646241fa --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/transitivity-add-captured-array-to-itself.expect.md @@ -0,0 +1,147 @@ + +## Input + +```javascript +import {useMemo} from 'react'; +import { + typedCapture, + typedCreateFrom, + typedMutate, + ValidateMemoization, +} from 'shared-runtime'; + +function Component({a, b}) { + const o: any = useMemo(() => ({a}), [a]); + const x: Array = useMemo(() => [o], [o, b]); + const y = typedCapture(x); + const z = typedCapture(y); + x.push(z); + x.push(b); + + return ( + <> + ; + ; + + ); +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{a: 0, b: 0}], + sequentialRenders: [ + {a: 0, b: 0}, + {a: 0, b: 1}, + {a: 1, b: 1}, + {a: 0, b: 0}, + ], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; +import { useMemo } from "react"; +import { + typedCapture, + typedCreateFrom, + typedMutate, + ValidateMemoization, +} from "shared-runtime"; + +function Component(t0) { + const $ = _c(19); + const { a, b } = t0; + let t1; + if ($[0] !== a) { + t1 = { a }; + $[0] = a; + $[1] = t1; + } else { + t1 = $[1]; + } + const o = t1; + let x; + if ($[2] !== b || $[3] !== o) { + x = [o]; + const y = typedCapture(x); + const z = typedCapture(y); + x.push(z); + x.push(b); + $[2] = b; + $[3] = o; + $[4] = x; + } else { + x = $[4]; + } + let t2; + if ($[5] !== a) { + t2 = [a]; + $[5] = a; + $[6] = t2; + } else { + t2 = $[6]; + } + let t3; + if ($[7] !== o || $[8] !== t2) { + t3 = ; + $[7] = o; + $[8] = t2; + $[9] = t3; + } else { + t3 = $[9]; + } + let t4; + if ($[10] !== a || $[11] !== b) { + t4 = [a, b]; + $[10] = a; + $[11] = b; + $[12] = t4; + } else { + t4 = $[12]; + } + let t5; + if ($[13] !== t4 || $[14] !== x) { + t5 = ; + $[13] = t4; + $[14] = x; + $[15] = t5; + } else { + t5 = $[15]; + } + let t6; + if ($[16] !== t3 || $[17] !== t5) { + t6 = ( + <> + {t3};{t5}; + + ); + $[16] = t3; + $[17] = t5; + $[18] = t6; + } else { + t6 = $[18]; + } + return t6; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ a: 0, b: 0 }], + sequentialRenders: [ + { a: 0, b: 0 }, + { a: 0, b: 1 }, + { a: 1, b: 1 }, + { a: 0, b: 0 }, + ], +}; + +``` + +### Eval output +(kind: ok)
{"inputs":[0],"output":{"a":0}}
;
{"inputs":[0,0],"output":[{"a":0},[["[[ cyclic ref *2 ]]"]],0]}
; +
{"inputs":[0],"output":{"a":0}}
;
{"inputs":[0,1],"output":[{"a":0},[["[[ cyclic ref *2 ]]"]],1]}
; +
{"inputs":[1],"output":{"a":1}}
;
{"inputs":[1,1],"output":[{"a":1},[["[[ cyclic ref *2 ]]"]],1]}
; +
{"inputs":[0],"output":{"a":0}}
;
{"inputs":[0,0],"output":[{"a":0},[["[[ cyclic ref *2 ]]"]],0]}
; \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/transitivity-add-captured-array-to-itself.tsx b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/transitivity-add-captured-array-to-itself.tsx new file mode 100644 index 0000000000000..d81c069e336ba --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/transitivity-add-captured-array-to-itself.tsx @@ -0,0 +1,34 @@ +import {useMemo} from 'react'; +import { + typedCapture, + typedCreateFrom, + typedMutate, + ValidateMemoization, +} from 'shared-runtime'; + +function Component({a, b}) { + const o: any = useMemo(() => ({a}), [a]); + const x: Array = useMemo(() => [o], [o, b]); + const y = typedCapture(x); + const z = typedCapture(y); + x.push(z); + x.push(b); + + return ( + <> + ; + ; + + ); +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{a: 0, b: 0}], + sequentialRenders: [ + {a: 0, b: 0}, + {a: 0, b: 1}, + {a: 1, b: 1}, + {a: 0, b: 0}, + ], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/transitivity-capture-createfrom-lambda.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/transitivity-capture-createfrom-lambda.expect.md new file mode 100644 index 0000000000000..2cffd06f07b75 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/transitivity-capture-createfrom-lambda.expect.md @@ -0,0 +1,111 @@ + +## Input + +```javascript +import {useMemo} from 'react'; +import { + typedCapture, + typedCreateFrom, + typedMutate, + ValidateMemoization, +} from 'shared-runtime'; + +function Component({a, b}: {a: number; b: number}) { + const x = useMemo(() => ({a}), [a, b]); + const f = () => { + const y = typedCapture(x); + const z = typedCreateFrom(y); + return z; + }; + const z = f(); + // mutates x + typedMutate(z, b); + + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{a: 0, b: 0}], + sequentialRenders: [ + {a: 0, b: 0}, + {a: 0, b: 1}, + {a: 1, b: 1}, + {a: 0, b: 0}, + ], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; +import { useMemo } from "react"; +import { + typedCapture, + typedCreateFrom, + typedMutate, + ValidateMemoization, +} from "shared-runtime"; + +function Component(t0) { + const $ = _c(9); + const { a, b } = t0; + let x; + if ($[0] !== a || $[1] !== b) { + x = { a }; + const f = () => { + const y = typedCapture(x); + const z = typedCreateFrom(y); + return z; + }; + + const z_0 = f(); + + typedMutate(z_0, b); + $[0] = a; + $[1] = b; + $[2] = x; + } else { + x = $[2]; + } + let t1; + if ($[3] !== a || $[4] !== b) { + t1 = [a, b]; + $[3] = a; + $[4] = b; + $[5] = t1; + } else { + t1 = $[5]; + } + let t2; + if ($[6] !== t1 || $[7] !== x) { + t2 = ; + $[6] = t1; + $[7] = x; + $[8] = t2; + } else { + t2 = $[8]; + } + return t2; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ a: 0, b: 0 }], + sequentialRenders: [ + { a: 0, b: 0 }, + { a: 0, b: 1 }, + { a: 1, b: 1 }, + { a: 0, b: 0 }, + ], +}; + +``` + +### Eval output +(kind: ok)
{"inputs":[0,0],"output":{"a":0,"property":0}}
+
{"inputs":[0,1],"output":{"a":0,"property":1}}
+
{"inputs":[1,1],"output":{"a":1,"property":1}}
+
{"inputs":[0,0],"output":{"a":0,"property":0}}
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/transitivity-capture-createfrom-lambda.tsx b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/transitivity-capture-createfrom-lambda.tsx new file mode 100644 index 0000000000000..72289eb833571 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/transitivity-capture-createfrom-lambda.tsx @@ -0,0 +1,32 @@ +import {useMemo} from 'react'; +import { + typedCapture, + typedCreateFrom, + typedMutate, + ValidateMemoization, +} from 'shared-runtime'; + +function Component({a, b}: {a: number; b: number}) { + const x = useMemo(() => ({a}), [a, b]); + const f = () => { + const y = typedCapture(x); + const z = typedCreateFrom(y); + return z; + }; + const z = f(); + // mutates x + typedMutate(z, b); + + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{a: 0, b: 0}], + sequentialRenders: [ + {a: 0, b: 0}, + {a: 0, b: 1}, + {a: 1, b: 1}, + {a: 0, b: 0}, + ], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/transitivity-capture-createfrom.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/transitivity-capture-createfrom.expect.md new file mode 100644 index 0000000000000..458b75dff94a8 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/transitivity-capture-createfrom.expect.md @@ -0,0 +1,102 @@ + +## Input + +```javascript +import {useMemo} from 'react'; +import { + typedCapture, + typedCreateFrom, + typedMutate, + ValidateMemoization, +} from 'shared-runtime'; + +function Component({a, b}: {a: number; b: number}) { + const x = useMemo(() => ({a}), [a, b]); + const y = typedCapture(x); + const z = typedCreateFrom(y); + // mutates x + typedMutate(z, b); + + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{a: 0, b: 0}], + sequentialRenders: [ + {a: 0, b: 0}, + {a: 0, b: 1}, + {a: 1, b: 1}, + {a: 0, b: 0}, + ], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; +import { useMemo } from "react"; +import { + typedCapture, + typedCreateFrom, + typedMutate, + ValidateMemoization, +} from "shared-runtime"; + +function Component(t0) { + const $ = _c(9); + const { a, b } = t0; + let x; + if ($[0] !== a || $[1] !== b) { + x = { a }; + const y = typedCapture(x); + const z = typedCreateFrom(y); + + typedMutate(z, b); + $[0] = a; + $[1] = b; + $[2] = x; + } else { + x = $[2]; + } + let t1; + if ($[3] !== a || $[4] !== b) { + t1 = [a, b]; + $[3] = a; + $[4] = b; + $[5] = t1; + } else { + t1 = $[5]; + } + let t2; + if ($[6] !== t1 || $[7] !== x) { + t2 = ; + $[6] = t1; + $[7] = x; + $[8] = t2; + } else { + t2 = $[8]; + } + return t2; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ a: 0, b: 0 }], + sequentialRenders: [ + { a: 0, b: 0 }, + { a: 0, b: 1 }, + { a: 1, b: 1 }, + { a: 0, b: 0 }, + ], +}; + +``` + +### Eval output +(kind: ok)
{"inputs":[0,0],"output":{"a":0,"property":0}}
+
{"inputs":[0,1],"output":{"a":0,"property":1}}
+
{"inputs":[1,1],"output":{"a":1,"property":1}}
+
{"inputs":[0,0],"output":{"a":0,"property":0}}
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/transitivity-capture-createfrom.tsx b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/transitivity-capture-createfrom.tsx new file mode 100644 index 0000000000000..d06ad11eb5753 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/transitivity-capture-createfrom.tsx @@ -0,0 +1,28 @@ +import {useMemo} from 'react'; +import { + typedCapture, + typedCreateFrom, + typedMutate, + ValidateMemoization, +} from 'shared-runtime'; + +function Component({a, b}: {a: number; b: number}) { + const x = useMemo(() => ({a}), [a, b]); + const y = typedCapture(x); + const z = typedCreateFrom(y); + // mutates x + typedMutate(z, b); + + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{a: 0, b: 0}], + sequentialRenders: [ + {a: 0, b: 0}, + {a: 0, b: 1}, + {a: 1, b: 1}, + {a: 0, b: 0}, + ], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/transitivity-createfrom-capture.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/transitivity-createfrom-capture.expect.md new file mode 100644 index 0000000000000..42f34bff4144a --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/transitivity-createfrom-capture.expect.md @@ -0,0 +1,101 @@ + +## Input + +```javascript +import {useMemo} from 'react'; +import { + typedCapture, + typedCreateFrom, + typedMutate, + ValidateMemoization, +} from 'shared-runtime'; + +function Component({a, b}) { + const x = useMemo(() => [{a}], [a]); + const y = typedCreateFrom(x); + const z = typedCapture(y); + // does not mutate x, so x should not depend on b + typedMutate(z, b); + + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{a: 0, b: 0}], + sequentialRenders: [ + {a: 0, b: 0}, + {a: 0, b: 1}, + {a: 1, b: 1}, + {a: 0, b: 0}, + ], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; +import { useMemo } from "react"; +import { + typedCapture, + typedCreateFrom, + typedMutate, + ValidateMemoization, +} from "shared-runtime"; + +function Component(t0) { + const $ = _c(7); + const { a, b } = t0; + let t1; + if ($[0] !== a) { + t1 = [{ a }]; + $[0] = a; + $[1] = t1; + } else { + t1 = $[1]; + } + const x = t1; + const y = typedCreateFrom(x); + const z = typedCapture(y); + + typedMutate(z, b); + let t2; + if ($[2] !== a) { + t2 = [a]; + $[2] = a; + $[3] = t2; + } else { + t2 = $[3]; + } + let t3; + if ($[4] !== t2 || $[5] !== x) { + t3 = ; + $[4] = t2; + $[5] = x; + $[6] = t3; + } else { + t3 = $[6]; + } + return t3; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ a: 0, b: 0 }], + sequentialRenders: [ + { a: 0, b: 0 }, + { a: 0, b: 1 }, + { a: 1, b: 1 }, + { a: 0, b: 0 }, + ], +}; + +``` + +### Eval output +(kind: ok)
{"inputs":[0],"output":[{"a":0}]}
+
{"inputs":[0],"output":[{"a":0}]}
+
{"inputs":[1],"output":[{"a":1}]}
+
{"inputs":[0],"output":[{"a":0}]}
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/transitivity-createfrom-capture.tsx b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/transitivity-createfrom-capture.tsx new file mode 100644 index 0000000000000..32d65e61e01ec --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/transitivity-createfrom-capture.tsx @@ -0,0 +1,28 @@ +import {useMemo} from 'react'; +import { + typedCapture, + typedCreateFrom, + typedMutate, + ValidateMemoization, +} from 'shared-runtime'; + +function Component({a, b}) { + const x = useMemo(() => [{a}], [a]); + const y = typedCreateFrom(x); + const z = typedCapture(y); + // does not mutate x, so x should not depend on b + typedMutate(z, b); + + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{a: 0, b: 0}], + sequentialRenders: [ + {a: 0, b: 0}, + {a: 0, b: 1}, + {a: 1, b: 1}, + {a: 0, b: 0}, + ], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/transitivity-phi-assign-or-capture.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/transitivity-phi-assign-or-capture.expect.md new file mode 100644 index 0000000000000..0786bac4341f6 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/transitivity-phi-assign-or-capture.expect.md @@ -0,0 +1,118 @@ + +## Input + +```javascript +import {useMemo} from 'react'; +import { + typedCapture, + typedCreateFrom, + typedMutate, + ValidateMemoization, +} from 'shared-runtime'; + +function Component({a, b}) { + const x = useMemo(() => [{a}], [a, b]); + let z: any; + if (b) { + z = x; + } else { + z = typedCapture(x); + } + // could mutate x + typedMutate(z, b); + + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{a: 0, b: 0}], + sequentialRenders: [ + {a: 0, b: 0}, + {a: 0, b: 1}, + {a: 1, b: 1}, + {a: 0, b: 0}, + ], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; +import { useMemo } from "react"; +import { + typedCapture, + typedCreateFrom, + typedMutate, + ValidateMemoization, +} from "shared-runtime"; + +function Component(t0) { + const $ = _c(11); + const { a, b } = t0; + let t1; + if ($[0] !== a) { + t1 = { a }; + $[0] = a; + $[1] = t1; + } else { + t1 = $[1]; + } + let x; + if ($[2] !== b || $[3] !== t1) { + x = [t1]; + let z; + if (b) { + z = x; + } else { + z = typedCapture(x); + } + + typedMutate(z, b); + $[2] = b; + $[3] = t1; + $[4] = x; + } else { + x = $[4]; + } + let t2; + if ($[5] !== a || $[6] !== b) { + t2 = [a, b]; + $[5] = a; + $[6] = b; + $[7] = t2; + } else { + t2 = $[7]; + } + let t3; + if ($[8] !== t2 || $[9] !== x) { + t3 = ; + $[8] = t2; + $[9] = x; + $[10] = t3; + } else { + t3 = $[10]; + } + return t3; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ a: 0, b: 0 }], + sequentialRenders: [ + { a: 0, b: 0 }, + { a: 0, b: 1 }, + { a: 1, b: 1 }, + { a: 0, b: 0 }, + ], +}; + +``` + +### Eval output +(kind: ok)
{"inputs":[0,0],"output":[{"a":0}]}
+
{"inputs":[0,1],"output":[{"a":0}]}
+
{"inputs":[1,1],"output":[{"a":1}]}
+
{"inputs":[0,0],"output":[{"a":0}]}
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/transitivity-phi-assign-or-capture.tsx b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/transitivity-phi-assign-or-capture.tsx new file mode 100644 index 0000000000000..90b7597694607 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/transitivity-phi-assign-or-capture.tsx @@ -0,0 +1,32 @@ +import {useMemo} from 'react'; +import { + typedCapture, + typedCreateFrom, + typedMutate, + ValidateMemoization, +} from 'shared-runtime'; + +function Component({a, b}) { + const x = useMemo(() => [{a}], [a, b]); + let z: any; + if (b) { + z = x; + } else { + z = typedCapture(x); + } + // could mutate x + typedMutate(z, b); + + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{a: 0, b: 0}], + sequentialRenders: [ + {a: 0, b: 0}, + {a: 0, b: 1}, + {a: 1, b: 1}, + {a: 0, b: 0}, + ], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/typed-identity-function-frozen-input.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/typed-identity-function-frozen-input.expect.md new file mode 100644 index 0000000000000..d3378b4d482bb --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/typed-identity-function-frozen-input.expect.md @@ -0,0 +1,119 @@ + +## Input + +```javascript +// @enableNewMutationAliasingModel + +import {useMemo} from 'react'; +import { + identity, + makeObject_Primitives, + typedIdentity, + useIdentity, + ValidateMemoization, +} from 'shared-runtime'; + +function Component({a, b}) { + // create a mutable value with input `a` + const x = useMemo(() => makeObject_Primitives(a), [a]); + + // freeze the value + useIdentity(x); + + // known to pass-through via aliasing signature + const x2 = typedIdentity(x); + + // Unknown function so we assume it conditionally mutates, + // but x2 is frozen so this downgrades to a read. + // x should *not* take b as a dependency + identity(x2, b); + + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{a: 0, b: 0}], + sequentialRenders: [ + {a: 0, b: 0}, + {a: 1, b: 0}, + {a: 1, b: 1}, + {a: 0, b: 1}, + {a: 0, b: 0}, + ], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel + +import { useMemo } from "react"; +import { + identity, + makeObject_Primitives, + typedIdentity, + useIdentity, + ValidateMemoization, +} from "shared-runtime"; + +function Component(t0) { + const $ = _c(7); + const { a, b } = t0; + let t1; + if ($[0] !== a) { + t1 = makeObject_Primitives(a); + $[0] = a; + $[1] = t1; + } else { + t1 = $[1]; + } + const x = t1; + + useIdentity(x); + + const x2 = typedIdentity(x); + + identity(x2, b); + let t2; + if ($[2] !== a) { + t2 = [a]; + $[2] = a; + $[3] = t2; + } else { + t2 = $[3]; + } + let t3; + if ($[4] !== t2 || $[5] !== x) { + t3 = ; + $[4] = t2; + $[5] = x; + $[6] = t3; + } else { + t3 = $[6]; + } + return t3; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ a: 0, b: 0 }], + sequentialRenders: [ + { a: 0, b: 0 }, + { a: 1, b: 0 }, + { a: 1, b: 1 }, + { a: 0, b: 1 }, + { a: 0, b: 0 }, + ], +}; + +``` + +### Eval output +(kind: ok)
{"inputs":[0],"output":{"a":0,"b":"value1","c":true}}
+
{"inputs":[1],"output":{"a":0,"b":"value1","c":true}}
+
{"inputs":[1],"output":{"a":0,"b":"value1","c":true}}
+
{"inputs":[0],"output":{"a":0,"b":"value1","c":true}}
+
{"inputs":[0],"output":{"a":0,"b":"value1","c":true}}
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/typed-identity-function-frozen-input.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/typed-identity-function-frozen-input.js new file mode 100644 index 0000000000000..d0f677ee4df17 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/typed-identity-function-frozen-input.js @@ -0,0 +1,40 @@ +// @enableNewMutationAliasingModel + +import {useMemo} from 'react'; +import { + identity, + makeObject_Primitives, + typedIdentity, + useIdentity, + ValidateMemoization, +} from 'shared-runtime'; + +function Component({a, b}) { + // create a mutable value with input `a` + const x = useMemo(() => makeObject_Primitives(a), [a]); + + // freeze the value + useIdentity(x); + + // known to pass-through via aliasing signature + const x2 = typedIdentity(x); + + // Unknown function so we assume it conditionally mutates, + // but x2 is frozen so this downgrades to a read. + // x should *not* take b as a dependency + identity(x2, b); + + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{a: 0, b: 0}], + sequentialRenders: [ + {a: 0, b: 0}, + {a: 1, b: 0}, + {a: 1, b: 1}, + {a: 0, b: 1}, + {a: 0, b: 0}, + ], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/typed-identity-function-mutable-input.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/typed-identity-function-mutable-input.expect.md new file mode 100644 index 0000000000000..17fed05d93d4d --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/typed-identity-function-mutable-input.expect.md @@ -0,0 +1,112 @@ + +## Input + +```javascript +// @enableNewMutationAliasingModel + +import { + identity, + makeObject_Primitives, + typedIdentity, + useIdentity, + ValidateMemoization, +} from 'shared-runtime'; + +function Component({a, b}) { + // create a mutable value with input `a` + const x = makeObject_Primitives(a); + + // known to pass-through via aliasing signature + const x2 = typedIdentity(x); + + // Unknown function so we assume it conditionally mutates, + // and x is still mutable so + identity(x2, b); + + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{a: 0, b: 0}], + sequentialRenders: [ + {a: 0, b: 0}, + {a: 1, b: 0}, + {a: 1, b: 1}, + {a: 0, b: 1}, + {a: 0, b: 0}, + ], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel + +import { + identity, + makeObject_Primitives, + typedIdentity, + useIdentity, + ValidateMemoization, +} from "shared-runtime"; + +function Component(t0) { + const $ = _c(9); + const { a, b } = t0; + let x; + if ($[0] !== a || $[1] !== b) { + x = makeObject_Primitives(a); + + const x2 = typedIdentity(x); + + identity(x2, b); + $[0] = a; + $[1] = b; + $[2] = x; + } else { + x = $[2]; + } + let t1; + if ($[3] !== a || $[4] !== b) { + t1 = [a, b]; + $[3] = a; + $[4] = b; + $[5] = t1; + } else { + t1 = $[5]; + } + let t2; + if ($[6] !== t1 || $[7] !== x) { + t2 = ; + $[6] = t1; + $[7] = x; + $[8] = t2; + } else { + t2 = $[8]; + } + return t2; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ a: 0, b: 0 }], + sequentialRenders: [ + { a: 0, b: 0 }, + { a: 1, b: 0 }, + { a: 1, b: 1 }, + { a: 0, b: 1 }, + { a: 0, b: 0 }, + ], +}; + +``` + +### Eval output +(kind: ok)
{"inputs":[0,0],"output":{"a":0,"b":"value1","c":true}}
+
{"inputs":[1,0],"output":{"a":0,"b":"value1","c":true}}
+
{"inputs":[1,1],"output":{"a":0,"b":"value1","c":true}}
+
{"inputs":[0,1],"output":{"a":0,"b":"value1","c":true}}
+
{"inputs":[0,0],"output":{"a":0,"b":"value1","c":true}}
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/typed-identity-function-mutable-input.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/typed-identity-function-mutable-input.js new file mode 100644 index 0000000000000..719c89d11de2f --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/typed-identity-function-mutable-input.js @@ -0,0 +1,35 @@ +// @enableNewMutationAliasingModel + +import { + identity, + makeObject_Primitives, + typedIdentity, + useIdentity, + ValidateMemoization, +} from 'shared-runtime'; + +function Component({a, b}) { + // create a mutable value with input `a` + const x = makeObject_Primitives(a); + + // known to pass-through via aliasing signature + const x2 = typedIdentity(x); + + // Unknown function so we assume it conditionally mutates, + // and x is still mutable so + identity(x2, b); + + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{a: 0, b: 0}], + sequentialRenders: [ + {a: 0, b: 0}, + {a: 1, b: 0}, + {a: 1, b: 1}, + {a: 0, b: 1}, + {a: 0, b: 0}, + ], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/useCallback-reordering-deplist-controlflow.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/useCallback-reordering-deplist-controlflow.expect.md new file mode 100644 index 0000000000000..e33f52396d5e5 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/useCallback-reordering-deplist-controlflow.expect.md @@ -0,0 +1,102 @@ + +## Input + +```javascript +// @enableNewMutationAliasingModel +import {useCallback} from 'react'; +import {Stringify} from 'shared-runtime'; + +function Foo({arr1, arr2, foo}) { + const x = [arr1]; + + let y = []; + + const getVal1 = useCallback(() => { + return {x: 2}; + }, []); + + const getVal2 = useCallback(() => { + return [y]; + }, [foo ? (y = x.concat(arr2)) : y]); + + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [{arr1: [1, 2], arr2: [3, 4], foo: true}], + sequentialRenders: [ + {arr1: [1, 2], arr2: [3, 4], foo: true}, + {arr1: [1, 2], arr2: [3, 4], foo: false}, + ], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel +import { useCallback } from "react"; +import { Stringify } from "shared-runtime"; + +function Foo(t0) { + const $ = _c(10); + const { arr1, arr2, foo } = t0; + let t1; + if ($[0] !== arr1) { + t1 = [arr1]; + $[0] = arr1; + $[1] = t1; + } else { + t1 = $[1]; + } + const x = t1; + let getVal1; + let t2; + if ($[2] !== arr2 || $[3] !== foo || $[4] !== x) { + let y = []; + + getVal1 = _temp; + + t2 = () => [y]; + foo ? (y = x.concat(arr2)) : y; + $[2] = arr2; + $[3] = foo; + $[4] = x; + $[5] = getVal1; + $[6] = t2; + } else { + getVal1 = $[5]; + t2 = $[6]; + } + const getVal2 = t2; + let t3; + if ($[7] !== getVal1 || $[8] !== getVal2) { + t3 = ; + $[7] = getVal1; + $[8] = getVal2; + $[9] = t3; + } else { + t3 = $[9]; + } + return t3; +} +function _temp() { + return { x: 2 }; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [{ arr1: [1, 2], arr2: [3, 4], foo: true }], + sequentialRenders: [ + { arr1: [1, 2], arr2: [3, 4], foo: true }, + { arr1: [1, 2], arr2: [3, 4], foo: false }, + ], +}; + +``` + +### Eval output +(kind: ok)
{"val1":{"kind":"Function","result":{"x":2}},"val2":{"kind":"Function","result":[[[1,2],3,4]]},"shouldInvokeFns":true}
+
{"val1":{"kind":"Function","result":{"x":2}},"val2":{"kind":"Function","result":[[]]},"shouldInvokeFns":true}
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/useCallback-reordering-deplist-controlflow.tsx b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/useCallback-reordering-deplist-controlflow.tsx new file mode 100644 index 0000000000000..08b9e4b2faa6c --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/useCallback-reordering-deplist-controlflow.tsx @@ -0,0 +1,28 @@ +// @enableNewMutationAliasingModel +import {useCallback} from 'react'; +import {Stringify} from 'shared-runtime'; + +function Foo({arr1, arr2, foo}) { + const x = [arr1]; + + let y = []; + + const getVal1 = useCallback(() => { + return {x: 2}; + }, []); + + const getVal2 = useCallback(() => { + return [y]; + }, [foo ? (y = x.concat(arr2)) : y]); + + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [{arr1: [1, 2], arr2: [3, 4], foo: true}], + sequentialRenders: [ + {arr1: [1, 2], arr2: [3, 4], foo: true}, + {arr1: [1, 2], arr2: [3, 4], foo: false}, + ], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/useCallback-reordering-depslist-assignment.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/useCallback-reordering-depslist-assignment.expect.md new file mode 100644 index 0000000000000..d37762bbac530 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/useCallback-reordering-depslist-assignment.expect.md @@ -0,0 +1,85 @@ + +## Input + +```javascript +// @enableNewMutationAliasingModel +import {useCallback} from 'react'; +import {Stringify} from 'shared-runtime'; + +// We currently produce invalid output (incorrect scoping for `y` declaration) +function useFoo(arr1, arr2) { + const x = [arr1]; + + let y; + const getVal = useCallback(() => { + return {y}; + }, [((y = x.concat(arr2)), y)]); + + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [ + [1, 2], + [3, 4], + ], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel +import { useCallback } from "react"; +import { Stringify } from "shared-runtime"; + +// We currently produce invalid output (incorrect scoping for `y` declaration) +function useFoo(arr1, arr2) { + const $ = _c(7); + let t0; + if ($[0] !== arr1) { + t0 = [arr1]; + $[0] = arr1; + $[1] = t0; + } else { + t0 = $[1]; + } + const x = t0; + let t1; + if ($[2] !== arr2 || $[3] !== x) { + let y; + t1 = () => ({ y }); + + (y = x.concat(arr2)), y; + $[2] = arr2; + $[3] = x; + $[4] = t1; + } else { + t1 = $[4]; + } + const getVal = t1; + let t2; + if ($[5] !== getVal) { + t2 = ; + $[5] = getVal; + $[6] = t2; + } else { + t2 = $[6]; + } + return t2; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [ + [1, 2], + [3, 4], + ], +}; + +``` + +### Eval output +(kind: ok)
{"getVal":{"kind":"Function","result":{"y":[[1,2],3,4]}},"shouldInvokeFns":true}
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/useCallback-reordering-depslist-assignment.tsx b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/useCallback-reordering-depslist-assignment.tsx new file mode 100644 index 0000000000000..43e2dfbb0504a --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/useCallback-reordering-depslist-assignment.tsx @@ -0,0 +1,23 @@ +// @enableNewMutationAliasingModel +import {useCallback} from 'react'; +import {Stringify} from 'shared-runtime'; + +// We currently produce invalid output (incorrect scoping for `y` declaration) +function useFoo(arr1, arr2) { + const x = [arr1]; + + let y; + const getVal = useCallback(() => { + return {y}; + }, [((y = x.concat(arr2)), y)]); + + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [ + [1, 2], + [3, 4], + ], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/useMemo-reordering-depslist-assignment.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/useMemo-reordering-depslist-assignment.expect.md new file mode 100644 index 0000000000000..926887a7a448b --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/useMemo-reordering-depslist-assignment.expect.md @@ -0,0 +1,75 @@ + +## Input + +```javascript +// @enableNewMutationAliasingModel +import {useMemo} from 'react'; + +function useFoo(arr1, arr2) { + const x = [arr1]; + + let y; + return useMemo(() => { + return {y}; + }, [((y = x.concat(arr2)), y)]); +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [ + [1, 2], + [3, 4], + ], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel +import { useMemo } from "react"; + +function useFoo(arr1, arr2) { + const $ = _c(7); + let t0; + if ($[0] !== arr1) { + t0 = [arr1]; + $[0] = arr1; + $[1] = t0; + } else { + t0 = $[1]; + } + const x = t0; + let y; + if ($[2] !== arr2 || $[3] !== x) { + (y = x.concat(arr2)), y; + $[2] = arr2; + $[3] = x; + $[4] = y; + } else { + y = $[4]; + } + let t1; + if ($[5] !== y) { + t1 = { y }; + $[5] = y; + $[6] = t1; + } else { + t1 = $[6]; + } + return t1; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [ + [1, 2], + [3, 4], + ], +}; + +``` + +### Eval output +(kind: ok) {"y":[[1,2],3,4]} \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/useMemo-reordering-depslist-assignment.ts b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/useMemo-reordering-depslist-assignment.ts new file mode 100644 index 0000000000000..5b7d799d68b13 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/useMemo-reordering-depslist-assignment.ts @@ -0,0 +1,19 @@ +// @enableNewMutationAliasingModel +import {useMemo} from 'react'; + +function useFoo(arr1, arr2) { + const x = [arr1]; + + let y; + return useMemo(() => { + return {y}; + }, [((y = x.concat(arr2)), y)]); +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [ + [1, 2], + [3, 4], + ], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/object-access-assignment.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/object-access-assignment.expect.md new file mode 100644 index 0000000000000..8b4dbc8f863d6 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/object-access-assignment.expect.md @@ -0,0 +1,83 @@ + +## Input + +```javascript +function Component({a, b, c}) { + // This is an object version of array-access-assignment.js + // Meant to confirm that object expressions and PropertyStore/PropertyLoad with strings + // works equivalently to array expressions and property accesses with numeric indices + const x = {zero: a}; + const y = {zero: null, one: b}; + const z = {zero: {}, one: {}, two: {zero: c}}; + x.zero = y.one; + z.zero.zero = x.zero; + return {zero: x, one: z}; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{a: 1, b: 20, c: 300}], + sequentialRenders: [ + {a: 2, b: 20, c: 300}, + {a: 3, b: 20, c: 300}, + {a: 3, b: 21, c: 300}, + {a: 3, b: 22, c: 300}, + {a: 3, b: 22, c: 301}, + ], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; +function Component(t0) { + const $ = _c(6); + const { a, b, c } = t0; + let t1; + if ($[0] !== a || $[1] !== b || $[2] !== c) { + const x = { zero: a }; + let t2; + if ($[4] !== b) { + t2 = { zero: null, one: b }; + $[4] = b; + $[5] = t2; + } else { + t2 = $[5]; + } + const y = t2; + const z = { zero: {}, one: {}, two: { zero: c } }; + x.zero = y.one; + z.zero.zero = x.zero; + t1 = { zero: x, one: z }; + $[0] = a; + $[1] = b; + $[2] = c; + $[3] = t1; + } else { + t1 = $[3]; + } + return t1; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ a: 1, b: 20, c: 300 }], + sequentialRenders: [ + { a: 2, b: 20, c: 300 }, + { a: 3, b: 20, c: 300 }, + { a: 3, b: 21, c: 300 }, + { a: 3, b: 22, c: 300 }, + { a: 3, b: 22, c: 301 }, + ], +}; + +``` + +### Eval output +(kind: ok) {"zero":{"zero":20},"one":{"zero":{"zero":20},"one":{},"two":{"zero":300}}} +{"zero":{"zero":20},"one":{"zero":{"zero":20},"one":{},"two":{"zero":300}}} +{"zero":{"zero":21},"one":{"zero":{"zero":21},"one":{},"two":{"zero":300}}} +{"zero":{"zero":22},"one":{"zero":{"zero":22},"one":{},"two":{"zero":300}}} +{"zero":{"zero":22},"one":{"zero":{"zero":22},"one":{},"two":{"zero":301}}} \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/object-access-assignment.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/object-access-assignment.js new file mode 100644 index 0000000000000..ef047238e7f84 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/object-access-assignment.js @@ -0,0 +1,23 @@ +function Component({a, b, c}) { + // This is an object version of array-access-assignment.js + // Meant to confirm that object expressions and PropertyStore/PropertyLoad with strings + // works equivalently to array expressions and property accesses with numeric indices + const x = {zero: a}; + const y = {zero: null, one: b}; + const z = {zero: {}, one: {}, two: {zero: c}}; + x.zero = y.one; + z.zero.zero = x.zero; + return {zero: x, one: z}; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{a: 1, b: 20, c: 300}], + sequentialRenders: [ + {a: 2, b: 20, c: 300}, + {a: 3, b: 20, c: 300}, + {a: 3, b: 21, c: 300}, + {a: 3, b: 22, c: 300}, + {a: 3, b: 22, c: 301}, + ], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/object-entries-mutation.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/object-entries-mutation.expect.md new file mode 100644 index 0000000000000..bc541b47f1f5d --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/object-entries-mutation.expect.md @@ -0,0 +1,57 @@ + +## Input + +```javascript +import {makeObject_Primitives, Stringify} from 'shared-runtime'; + +function Component(props) { + const object = {object: props.object}; + const entries = Object.entries(object); + entries.map(([, value]) => { + value.updated = true; + }); + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{object: {key: makeObject_Primitives()}}], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; +import { makeObject_Primitives, Stringify } from "shared-runtime"; + +function Component(props) { + const $ = _c(2); + let t0; + if ($[0] !== props.object) { + const object = { object: props.object }; + const entries = Object.entries(object); + entries.map(_temp); + t0 = ; + $[0] = props.object; + $[1] = t0; + } else { + t0 = $[1]; + } + return t0; +} +function _temp(t0) { + const [, value] = t0; + value.updated = true; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ object: { key: makeObject_Primitives() } }], +}; + +``` + +### Eval output +(kind: ok)
{"entries":[["object",{"key":{"a":0,"b":"value1","c":true},"updated":true}]]}
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/object-entries-mutation.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/object-entries-mutation.js new file mode 100644 index 0000000000000..2902cffd014fe --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/object-entries-mutation.js @@ -0,0 +1,15 @@ +import {makeObject_Primitives, Stringify} from 'shared-runtime'; + +function Component(props) { + const object = {object: props.object}; + const entries = Object.entries(object); + entries.map(([, value]) => { + value.updated = true; + }); + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{object: {key: makeObject_Primitives()}}], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/object-expression-captures-function-with-global-mutation.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/object-expression-captures-function-with-global-mutation.expect.md new file mode 100644 index 0000000000000..9d970ef9e6752 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/object-expression-captures-function-with-global-mutation.expect.md @@ -0,0 +1,49 @@ + +## Input + +```javascript +function Foo() { + const x = () => { + window.href = 'foo'; + }; + const y = {x}; + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; +function Foo() { + const $ = _c(1); + const x = _temp; + let t0; + if ($[0] === Symbol.for("react.memo_cache_sentinel")) { + const y = { x }; + t0 = ; + $[0] = t0; + } else { + t0 = $[0]; + } + return t0; +} +function _temp() { + window.href = "foo"; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [], +}; + +``` + +### Eval output +(kind: exception) Bar is not defined \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.object-capture-global-mutation.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/object-expression-captures-function-with-global-mutation.js similarity index 100% rename from compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.object-capture-global-mutation.js rename to compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/object-expression-captures-function-with-global-mutation.js diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/object-expression-computed-key-object-mutated-later.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/object-expression-computed-key-object-mutated-later.expect.md index bf0f9da6b1da1..f187c8c79d369 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/object-expression-computed-key-object-mutated-later.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/object-expression-computed-key-object-mutated-later.expect.md @@ -27,34 +27,18 @@ import { c as _c } from "react/compiler-runtime"; import { identity, mutate } from "shared-runtime"; function Component(props) { - const $ = _c(5); - let t0; - if ($[0] === Symbol.for("react.memo_cache_sentinel")) { - t0 = {}; - $[0] = t0; + const $ = _c(2); + let context; + if ($[0] !== props.value) { + const key = {}; + context = { [key]: identity([props.value]) }; + + mutate(key); + $[0] = props.value; + $[1] = context; } else { - t0 = $[0]; + context = $[1]; } - const key = t0; - let t1; - if ($[1] !== props.value) { - t1 = identity([props.value]); - $[1] = props.value; - $[2] = t1; - } else { - t1 = $[2]; - } - let t2; - if ($[3] !== t1) { - t2 = { [key]: t1 }; - $[3] = t1; - $[4] = t2; - } else { - t2 = $[4]; - } - const context = t2; - - mutate(key); return context; } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/object-expression-computed-member.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/object-expression-computed-member.expect.md index 810b03e529e77..01d91de8d4fb7 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/object-expression-computed-member.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/object-expression-computed-member.expect.md @@ -27,11 +27,22 @@ import { c as _c } from "react/compiler-runtime"; import { identity, mutate, mutateAndReturn } from "shared-runtime"; function Component(props) { - const $ = _c(2); + const $ = _c(4); let context; if ($[0] !== props.value) { const key = { a: "key" }; - context = { [key.a]: identity([props.value]) }; + + const t0 = key.a; + const t1 = identity([props.value]); + let t2; + if ($[2] !== t1) { + t2 = { [t0]: t1 }; + $[2] = t1; + $[3] = t2; + } else { + t2 = $[3]; + } + context = t2; mutate(key); $[0] = props.value; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/object-keys.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/object-keys.expect.md new file mode 100644 index 0000000000000..f076d9032db0e --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/object-keys.expect.md @@ -0,0 +1,108 @@ + +## Input + +```javascript +// @validatePreserveExistingMemoizationGuarantees +import {useMemo} from 'react'; +import {Stringify} from 'shared-runtime'; + +// derived from https://github.com/facebook/react/issues/32261 +function Component({items}) { + const record = useMemo( + () => + Object.fromEntries( + items.map(item => [item.id, ref => ]) + ), + [items] + ); + + // Without a declaration for Object.entries(), this would be assumed to mutate + // `record`, meaning existing memoization couldn't be preserved + return ( +
+ {Object.keys(record).map(id => ( + + ))} +
+ ); +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [ + { + items: [ + {id: '0', name: 'Hello'}, + {id: '1', name: 'World!'}, + ], + }, + ], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; // @validatePreserveExistingMemoizationGuarantees +import { useMemo } from "react"; +import { Stringify } from "shared-runtime"; + +// derived from https://github.com/facebook/react/issues/32261 +function Component(t0) { + const $ = _c(7); + const { items } = t0; + let t1; + if ($[0] !== items) { + t1 = Object.fromEntries(items.map(_temp)); + $[0] = items; + $[1] = t1; + } else { + t1 = $[1]; + } + const record = t1; + let t2; + if ($[2] !== record) { + t2 = Object.keys(record); + $[2] = record; + $[3] = t2; + } else { + t2 = $[3]; + } + let t3; + if ($[4] !== record || $[5] !== t2) { + t3 = ( +
+ {t2.map((id) => ( + + ))} +
+ ); + $[4] = record; + $[5] = t2; + $[6] = t3; + } else { + t3 = $[6]; + } + return t3; +} +function _temp(item) { + return [item.id, (ref) => ]; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [ + { + items: [ + { id: "0", name: "Hello" }, + { id: "1", name: "World!" }, + ], + }, + ], +}; + +``` + +### Eval output +(kind: ok)
{"render":"[[ function params=1 ]]"}
{"render":"[[ function params=1 ]]"}
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/object-keys.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/object-keys.js new file mode 100644 index 0000000000000..38ae97ab95643 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/object-keys.js @@ -0,0 +1,36 @@ +// @validatePreserveExistingMemoizationGuarantees +import {useMemo} from 'react'; +import {Stringify} from 'shared-runtime'; + +// derived from https://github.com/facebook/react/issues/32261 +function Component({items}) { + const record = useMemo( + () => + Object.fromEntries( + items.map(item => [item.id, ref => ]) + ), + [items] + ); + + // Without a declaration for Object.entries(), this would be assumed to mutate + // `record`, meaning existing memoization couldn't be preserved + return ( +
+ {Object.keys(record).map(id => ( + + ))} +
+ ); +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [ + { + items: [ + {id: '0', name: 'Hello'}, + {id: '1', name: 'World!'}, + ], + }, + ], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/object-method-shorthand-3.expect.md~051f3e57 ([hir] Do not memoize object methods separately) b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/object-method-shorthand-3.expect.md~051f3e57 ([hir] Do not memoize object methods separately) deleted file mode 100644 index f4354f427cf87..0000000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/object-method-shorthand-3.expect.md~051f3e57 ([hir] Do not memoize object methods separately) +++ /dev/null @@ -1,47 +0,0 @@ - -## Input - -```javascript -import { mutate } from "shared-runtime"; - -function Component(a) { - const x = { a }; - let obj = { - method() { - mutate(x); - return x; - }, - }; - return obj.method(); -} - -export const FIXTURE_ENTRYPOINT = { - fn: Component, - params: [{ x: 1 }, { a: 2 }, { b: 2 }], -}; - -``` - -## Code - -```javascript -import { mutate } from "shared-runtime"; - -function Component(a) { - const x = { a }; - const obj = { - method() { - mutate(x); - return x; - }, - }; - return obj.method(); -} - -export const FIXTURE_ENTRYPOINT = { - fn: Component, - params: [{ x: 1 }, { a: 2 }, { b: 2 }], -}; - -``` - \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/object-values-mutation.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/object-values-mutation.expect.md new file mode 100644 index 0000000000000..bc541b47f1f5d --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/object-values-mutation.expect.md @@ -0,0 +1,57 @@ + +## Input + +```javascript +import {makeObject_Primitives, Stringify} from 'shared-runtime'; + +function Component(props) { + const object = {object: props.object}; + const entries = Object.entries(object); + entries.map(([, value]) => { + value.updated = true; + }); + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{object: {key: makeObject_Primitives()}}], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; +import { makeObject_Primitives, Stringify } from "shared-runtime"; + +function Component(props) { + const $ = _c(2); + let t0; + if ($[0] !== props.object) { + const object = { object: props.object }; + const entries = Object.entries(object); + entries.map(_temp); + t0 = ; + $[0] = props.object; + $[1] = t0; + } else { + t0 = $[1]; + } + return t0; +} +function _temp(t0) { + const [, value] = t0; + value.updated = true; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ object: { key: makeObject_Primitives() } }], +}; + +``` + +### Eval output +(kind: ok)
{"entries":[["object",{"key":{"a":0,"b":"value1","c":true},"updated":true}]]}
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/object-values-mutation.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/object-values-mutation.js new file mode 100644 index 0000000000000..2902cffd014fe --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/object-values-mutation.js @@ -0,0 +1,15 @@ +import {makeObject_Primitives, Stringify} from 'shared-runtime'; + +function Component(props) { + const object = {object: props.object}; + const entries = Object.entries(object); + entries.map(([, value]) => { + value.updated = true; + }); + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{object: {key: makeObject_Primitives()}}], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/object-values.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/object-values.expect.md new file mode 100644 index 0000000000000..900399066413c --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/object-values.expect.md @@ -0,0 +1,103 @@ + +## Input + +```javascript +// @validatePreserveExistingMemoizationGuarantees +import {useMemo} from 'react'; +import {Stringify} from 'shared-runtime'; + +// derived from https://github.com/facebook/react/issues/32261 +function Component({items}) { + const record = useMemo( + () => + Object.fromEntries( + items.map(item => [ + item.id, + {id: item.id, render: ref => }, + ]) + ), + [items] + ); + + // Without a declaration for Object.entries(), this would be assumed to mutate + // `record`, meaning existing memoization couldn't be preserved + return ( +
+ {Object.values(record).map(({id, render}) => ( + + ))} +
+ ); +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [ + { + items: [ + {id: '0', name: 'Hello'}, + {id: '1', name: 'World!'}, + ], + }, + ], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; // @validatePreserveExistingMemoizationGuarantees +import { useMemo } from "react"; +import { Stringify } from "shared-runtime"; + +// derived from https://github.com/facebook/react/issues/32261 +function Component(t0) { + const $ = _c(4); + const { items } = t0; + let t1; + if ($[0] !== items) { + t1 = Object.fromEntries(items.map(_temp)); + $[0] = items; + $[1] = t1; + } else { + t1 = $[1]; + } + const record = t1; + let t2; + if ($[2] !== record) { + t2 =
{Object.values(record).map(_temp2)}
; + $[2] = record; + $[3] = t2; + } else { + t2 = $[3]; + } + return t2; +} +function _temp2(t0) { + const { id, render } = t0; + return ; +} +function _temp(item) { + return [ + item.id, + { id: item.id, render: (ref) => }, + ]; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [ + { + items: [ + { id: "0", name: "Hello" }, + { id: "1", name: "World!" }, + ], + }, + ], +}; + +``` + +### Eval output +(kind: ok)
{"render":"[[ function params=1 ]]"}
{"render":"[[ function params=1 ]]"}
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/object-values.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/object-values.js new file mode 100644 index 0000000000000..4cf229c379a56 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/object-values.js @@ -0,0 +1,39 @@ +// @validatePreserveExistingMemoizationGuarantees +import {useMemo} from 'react'; +import {Stringify} from 'shared-runtime'; + +// derived from https://github.com/facebook/react/issues/32261 +function Component({items}) { + const record = useMemo( + () => + Object.fromEntries( + items.map(item => [ + item.id, + {id: item.id, render: ref => }, + ]) + ), + [items] + ); + + // Without a declaration for Object.entries(), this would be assumed to mutate + // `record`, meaning existing memoization couldn't be preserved + return ( +
+ {Object.values(record).map(({id, render}) => ( + + ))} +
+ ); +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [ + { + items: [ + {id: '0', name: 'Hello'}, + {id: '1', name: 'World!'}, + ], + }, + ], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-member-expression-as-memo-dep.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-member-expression-as-memo-dep.expect.md index 3dd8e730325ea..4c2bced216ff4 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-member-expression-as-memo-dep.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-member-expression-as-memo-dep.expect.md @@ -42,36 +42,34 @@ function Component(t0) { arg?.items.edges?.nodes; let t1; - let t2; if ($[0] !== arg?.items.edges?.nodes) { - t2 = arg?.items.edges?.nodes.map(identity); + t1 = arg?.items.edges?.nodes.map(identity); $[0] = arg?.items.edges?.nodes; - $[1] = t2; + $[1] = t1; } else { - t2 = $[1]; + t1 = $[1]; } - t1 = t2; const data = t1; - const t3 = arg?.items.edges?.nodes; - let t4; - if ($[2] !== t3) { - t4 = [t3]; - $[2] = t3; - $[3] = t4; + const t2 = arg?.items.edges?.nodes; + let t3; + if ($[2] !== t2) { + t3 = [t2]; + $[2] = t2; + $[3] = t3; } else { - t4 = $[3]; + t3 = $[3]; } - let t5; - if ($[4] !== data || $[5] !== t4) { - t5 = ; + let t4; + if ($[4] !== data || $[5] !== t3) { + t4 = ; $[4] = data; - $[5] = t4; - $[6] = t5; + $[5] = t3; + $[6] = t4; } else { - t5 = $[6]; + t4 = $[6]; } - return t5; + return t4; } export const FIXTURE_ENTRYPOINT = { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-member-expression-inverted-optionals-parallel-paths.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-member-expression-inverted-optionals-parallel-paths.expect.md index 98fcfbe7f0f6f..73c88e2c493c1 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-member-expression-inverted-optionals-parallel-paths.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-member-expression-inverted-optionals-parallel-paths.expect.md @@ -23,21 +23,19 @@ import { c as _c } from "react/compiler-runtime"; // @validatePreserveExistingMe import { ValidateMemoization } from "shared-runtime"; function Component(props) { const $ = _c(2); - let t0; const x$0 = []; x$0.push(props?.a.b?.c.d?.e); x$0.push(props.a?.b.c?.d.e); - t0 = x$0; - let t1; + let t0; if ($[0] !== props.a.b.c.d.e) { - t1 = ; + t0 = ; $[0] = props.a.b.c.d.e; - $[1] = t1; + $[1] = t0; } else { - t1 = $[1]; + t0 = $[1]; } - return t1; + return t0; } ``` diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-member-expression-single-with-unconditional.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-member-expression-single-with-unconditional.expect.md index 3cd9877813c58..95ebf3f95f9e7 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-member-expression-single-with-unconditional.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-member-expression-single-with-unconditional.expect.md @@ -23,7 +23,6 @@ import { c as _c } from "react/compiler-runtime"; // @validatePreserveExistingMe import { ValidateMemoization } from "shared-runtime"; function Component(props) { const $ = _c(7); - let t0; let x; if ($[0] !== props.items) { x = []; @@ -34,26 +33,25 @@ function Component(props) { } else { x = $[1]; } - t0 = x; - const data = t0; - let t1; + const data = x; + let t0; if ($[2] !== props.items) { - t1 = [props.items]; + t0 = [props.items]; $[2] = props.items; - $[3] = t1; + $[3] = t0; } else { - t1 = $[3]; + t0 = $[3]; } - let t2; - if ($[4] !== data || $[5] !== t1) { - t2 = ; + let t1; + if ($[4] !== data || $[5] !== t0) { + t1 = ; $[4] = data; - $[5] = t1; - $[6] = t2; + $[5] = t0; + $[6] = t1; } else { - t2 = $[6]; + t1 = $[6]; } - return t2; + return t1; } ``` diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-member-expression-single.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-member-expression-single.expect.md index 60a6171ab1ea1..43476f1604b49 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-member-expression-single.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-member-expression-single.expect.md @@ -38,7 +38,6 @@ function Component(t0) { const { arg } = t0; arg?.items; - let t1; let x; if ($[0] !== arg?.items) { x = []; @@ -48,27 +47,26 @@ function Component(t0) { } else { x = $[1]; } - t1 = x; - const data = t1; - const t2 = arg?.items; - let t3; - if ($[2] !== t2) { - t3 = [t2]; - $[2] = t2; - $[3] = t3; + const data = x; + const t1 = arg?.items; + let t2; + if ($[2] !== t1) { + t2 = [t1]; + $[2] = t1; + $[3] = t2; } else { - t3 = $[3]; + t2 = $[3]; } - let t4; - if ($[4] !== data || $[5] !== t3) { - t4 = ; + let t3; + if ($[4] !== data || $[5] !== t2) { + t3 = ; $[4] = data; - $[5] = t3; - $[6] = t4; + $[5] = t2; + $[6] = t3; } else { - t4 = $[6]; + t3 = $[6]; } - return t4; + return t3; } export const FIXTURE_ENTRYPOINT = { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-existing-memoization-guarantees/lambda-with-fbt-preserve-memoization.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-existing-memoization-guarantees/lambda-with-fbt-preserve-memoization.expect.md new file mode 100644 index 0000000000000..bafbb5c5ef37a --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-existing-memoization-guarantees/lambda-with-fbt-preserve-memoization.expect.md @@ -0,0 +1,86 @@ + +## Input + +```javascript +// @enablePreserveExistingMemoizationGuarantees +import {fbt} from 'fbt'; + +function Component() { + const buttonLabel = () => { + if (!someCondition) { + return {'Purchase as a gift'}; + } else if ( + !iconOnly && + showPrice && + item?.current_gift_offer?.price?.formatted != null + ) { + return ( + + {'Gift | '} + + {item?.current_gift_offer?.price?.formatted} + + + ); + } else if (!iconOnly && !showPrice) { + return {'Gift'}; + } + }; + + return ( + + ; 11 | }); diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/rules-of-hooks/todo.error.invalid-rules-of-hooks-8566f9a360e2.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/rules-of-hooks/todo.error.invalid-rules-of-hooks-8566f9a360e2.expect.md index fabbf9b089a22..520a8e4097d14 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/rules-of-hooks/todo.error.invalid-rules-of-hooks-8566f9a360e2.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/rules-of-hooks/todo.error.invalid-rules-of-hooks-8566f9a360e2.expect.md @@ -20,10 +20,15 @@ const MemoizedButton = memo(function (props) { ## Error ``` +Found 1 error: + +Error: Hooks must always be called in a consistent order, and may not be called conditionally. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning) + +todo.error.invalid-rules-of-hooks-8566f9a360e2.ts:8:4 6 | const MemoizedButton = memo(function (props) { 7 | if (props.fancy) { > 8 | useCustomHook(); - | ^^^^^^^^^^^^^ InvalidReact: Hooks must always be called in a consistent order, and may not be called conditionally. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning) (8:8) + | ^^^^^^^^^^^^^ Hooks must always be called in a consistent order, and may not be called conditionally. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning) 9 | } 10 | return ; 11 | }); diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/rules-of-hooks/todo.error.invalid-rules-of-hooks-a0058f0b446d.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/rules-of-hooks/todo.error.invalid-rules-of-hooks-a0058f0b446d.expect.md index b6e240e26c37d..acd4ff9395fc6 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/rules-of-hooks/todo.error.invalid-rules-of-hooks-a0058f0b446d.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/rules-of-hooks/todo.error.invalid-rules-of-hooks-a0058f0b446d.expect.md @@ -19,10 +19,15 @@ function ComponentWithConditionalHook() { ## Error ``` +Found 1 error: + +Error: Hooks must always be called in a consistent order, and may not be called conditionally. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning) + +todo.error.invalid-rules-of-hooks-a0058f0b446d.ts:8:4 6 | function ComponentWithConditionalHook() { 7 | if (cond) { > 8 | Namespace.useConditionalHook(); - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ InvalidReact: Hooks must always be called in a consistent order, and may not be called conditionally. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning) (8:8) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Hooks must always be called in a consistent order, and may not be called conditionally. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning) 9 | } 10 | } 11 | diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/rules-of-hooks/todo.error.rules-of-hooks-27c18dc8dad2.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/rules-of-hooks/todo.error.rules-of-hooks-27c18dc8dad2.expect.md index 83e94b7616692..8f2783f96909e 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/rules-of-hooks/todo.error.rules-of-hooks-27c18dc8dad2.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/rules-of-hooks/todo.error.rules-of-hooks-27c18dc8dad2.expect.md @@ -20,10 +20,15 @@ const FancyButton = React.forwardRef((props, ref) => { ## Error ``` +Found 1 error: + +Error: Hooks must always be called in a consistent order, and may not be called conditionally. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning) + +todo.error.rules-of-hooks-27c18dc8dad2.ts:8:4 6 | const FancyButton = React.forwardRef((props, ref) => { 7 | if (props.fancy) { > 8 | useCustomHook(); - | ^^^^^^^^^^^^^ InvalidReact: Hooks must always be called in a consistent order, and may not be called conditionally. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning) (8:8) + | ^^^^^^^^^^^^^ Hooks must always be called in a consistent order, and may not be called conditionally. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning) 9 | } 10 | return ; 11 | }); diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/rules-of-hooks/todo.error.rules-of-hooks-d0935abedc42.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/rules-of-hooks/todo.error.rules-of-hooks-d0935abedc42.expect.md index a96e8e0878bc3..343c51787e259 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/rules-of-hooks/todo.error.rules-of-hooks-d0935abedc42.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/rules-of-hooks/todo.error.rules-of-hooks-d0935abedc42.expect.md @@ -19,10 +19,15 @@ React.unknownFunction((foo, bar) => { ## Error ``` +Found 1 error: + +Error: Hooks must always be called in a consistent order, and may not be called conditionally. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning) + +todo.error.rules-of-hooks-d0935abedc42.ts:8:4 6 | React.unknownFunction((foo, bar) => { 7 | if (foo) { > 8 | useNotAHook(bar); - | ^^^^^^^^^^^ InvalidReact: Hooks must always be called in a consistent order, and may not be called conditionally. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning) (8:8) + | ^^^^^^^^^^^ Hooks must always be called in a consistent order, and may not be called conditionally. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning) 9 | } 10 | }); 11 | diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/rules-of-hooks/todo.error.rules-of-hooks-e29c874aa913.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/rules-of-hooks/todo.error.rules-of-hooks-e29c874aa913.expect.md index 6ce7fc2c8bcdf..a9960ad44dd68 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/rules-of-hooks/todo.error.rules-of-hooks-e29c874aa913.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/rules-of-hooks/todo.error.rules-of-hooks-e29c874aa913.expect.md @@ -20,10 +20,15 @@ function useHook() { ## Error ``` +Found 1 error: + +Error: Hooks must always be called in a consistent order, and may not be called conditionally. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning) + +todo.error.rules-of-hooks-e29c874aa913.ts:9:4 7 | try { 8 | f(); > 9 | useState(); - | ^^^^^^^^ InvalidReact: Hooks must always be called in a consistent order, and may not be called conditionally. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning) (9:9) + | ^^^^^^^^ Hooks must always be called in a consistent order, and may not be called conditionally. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning) 10 | } catch {} 11 | } 12 | diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssa-switch.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssa-switch.expect.md index 4796fbdcc2bcf..48a765d3f3631 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssa-switch.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssa-switch.expect.md @@ -41,8 +41,7 @@ function foo() { case 2: { break bb0; } - default: { - } + default: } } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/static-components/invalid-conditionally-assigned-dynamically-constructed-component-in-render.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/static-components/invalid-conditionally-assigned-dynamically-constructed-component-in-render.expect.md index af8103b7ae515..c0cac509c8854 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/static-components/invalid-conditionally-assigned-dynamically-constructed-component-in-render.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/static-components/invalid-conditionally-assigned-dynamically-constructed-component-in-render.expect.md @@ -50,8 +50,7 @@ function Example(props) { ## Logs ``` -{"kind":"CompileError","detail":{"options":{"reason":"Components created during render will reset their state each time they are created. Declare components outside of render. ","description":null,"severity":"InvalidReact","suggestions":null,"loc":{"start":{"line":9,"column":10,"index":202},"end":{"line":9,"column":19,"index":211},"filename":"invalid-conditionally-assigned-dynamically-constructed-component-in-render.ts"}}},"fnLoc":null} -{"kind":"CompileError","detail":{"options":{"reason":"The component may be created during render","description":null,"severity":"InvalidReact","suggestions":null,"loc":{"start":{"line":5,"column":16,"index":124},"end":{"line":5,"column":33,"index":141},"filename":"invalid-conditionally-assigned-dynamically-constructed-component-in-render.ts"}}},"fnLoc":null} +{"kind":"CompileError","detail":{"options":{"category":"StaticComponents","severity":"InvalidReact","reason":"Cannot create components during render","description":"Components created during render will reset their state each time they are created. Declare components outside of render. ","details":[{"kind":"error","loc":{"start":{"line":9,"column":10,"index":202},"end":{"line":9,"column":19,"index":211},"filename":"invalid-conditionally-assigned-dynamically-constructed-component-in-render.ts"},"message":"This component is created during render"},{"kind":"error","loc":{"start":{"line":5,"column":16,"index":124},"end":{"line":5,"column":33,"index":141},"filename":"invalid-conditionally-assigned-dynamically-constructed-component-in-render.ts"},"message":"The component is created during render here"}]}},"fnLoc":null} {"kind":"CompileSuccess","fnLoc":{"start":{"line":2,"column":0,"index":45},"end":{"line":10,"column":1,"index":217},"filename":"invalid-conditionally-assigned-dynamically-constructed-component-in-render.ts"},"fnName":"Example","memoSlots":3,"memoBlocks":2,"memoValues":2,"prunedMemoBlocks":0,"prunedMemoValues":0} ``` diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/static-components/invalid-dynamically-construct-component-in-render.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/static-components/invalid-dynamically-construct-component-in-render.expect.md index 7720863da34c2..337997caa594d 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/static-components/invalid-dynamically-construct-component-in-render.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/static-components/invalid-dynamically-construct-component-in-render.expect.md @@ -32,8 +32,7 @@ function Example(props) { ## Logs ``` -{"kind":"CompileError","detail":{"options":{"reason":"Components created during render will reset their state each time they are created. Declare components outside of render. ","description":null,"severity":"InvalidReact","suggestions":null,"loc":{"start":{"line":4,"column":10,"index":120},"end":{"line":4,"column":19,"index":129},"filename":"invalid-dynamically-construct-component-in-render.ts"}}},"fnLoc":null} -{"kind":"CompileError","detail":{"options":{"reason":"The component may be created during render","description":null,"severity":"InvalidReact","suggestions":null,"loc":{"start":{"line":3,"column":20,"index":91},"end":{"line":3,"column":37,"index":108},"filename":"invalid-dynamically-construct-component-in-render.ts"}}},"fnLoc":null} +{"kind":"CompileError","detail":{"options":{"category":"StaticComponents","severity":"InvalidReact","reason":"Cannot create components during render","description":"Components created during render will reset their state each time they are created. Declare components outside of render. ","details":[{"kind":"error","loc":{"start":{"line":4,"column":10,"index":120},"end":{"line":4,"column":19,"index":129},"filename":"invalid-dynamically-construct-component-in-render.ts"},"message":"This component is created during render"},{"kind":"error","loc":{"start":{"line":3,"column":20,"index":91},"end":{"line":3,"column":37,"index":108},"filename":"invalid-dynamically-construct-component-in-render.ts"},"message":"The component is created during render here"}]}},"fnLoc":null} {"kind":"CompileSuccess","fnLoc":{"start":{"line":2,"column":0,"index":45},"end":{"line":5,"column":1,"index":135},"filename":"invalid-dynamically-construct-component-in-render.ts"},"fnName":"Example","memoSlots":1,"memoBlocks":1,"memoValues":1,"prunedMemoBlocks":0,"prunedMemoValues":0} ``` diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/static-components/invalid-dynamically-constructed-component-function.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/static-components/invalid-dynamically-constructed-component-function.expect.md index 8d218bf24b0de..019beccdc86f5 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/static-components/invalid-dynamically-constructed-component-function.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/static-components/invalid-dynamically-constructed-component-function.expect.md @@ -37,8 +37,7 @@ function Example(props) { ## Logs ``` -{"kind":"CompileError","detail":{"options":{"reason":"Components created during render will reset their state each time they are created. Declare components outside of render. ","description":null,"severity":"InvalidReact","suggestions":null,"loc":{"start":{"line":6,"column":10,"index":130},"end":{"line":6,"column":19,"index":139},"filename":"invalid-dynamically-constructed-component-function.ts"}}},"fnLoc":null} -{"kind":"CompileError","detail":{"options":{"reason":"The component may be created during render","description":null,"severity":"InvalidReact","suggestions":null,"loc":{"start":{"line":3,"column":2,"index":73},"end":{"line":5,"column":3,"index":119},"filename":"invalid-dynamically-constructed-component-function.ts"}}},"fnLoc":null} +{"kind":"CompileError","detail":{"options":{"category":"StaticComponents","severity":"InvalidReact","reason":"Cannot create components during render","description":"Components created during render will reset their state each time they are created. Declare components outside of render. ","details":[{"kind":"error","loc":{"start":{"line":6,"column":10,"index":130},"end":{"line":6,"column":19,"index":139},"filename":"invalid-dynamically-constructed-component-function.ts"},"message":"This component is created during render"},{"kind":"error","loc":{"start":{"line":3,"column":2,"index":73},"end":{"line":5,"column":3,"index":119},"filename":"invalid-dynamically-constructed-component-function.ts"},"message":"The component is created during render here"}]}},"fnLoc":null} {"kind":"CompileSuccess","fnLoc":{"start":{"line":2,"column":0,"index":45},"end":{"line":7,"column":1,"index":145},"filename":"invalid-dynamically-constructed-component-function.ts"},"fnName":"Example","memoSlots":1,"memoBlocks":1,"memoValues":1,"prunedMemoBlocks":0,"prunedMemoValues":0} ``` diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/static-components/invalid-dynamically-constructed-component-method-call.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/static-components/invalid-dynamically-constructed-component-method-call.expect.md index e3bc7a5eb5796..f673b27779859 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/static-components/invalid-dynamically-constructed-component-method-call.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/static-components/invalid-dynamically-constructed-component-method-call.expect.md @@ -41,8 +41,7 @@ function Example(props) { ## Logs ``` -{"kind":"CompileError","detail":{"options":{"reason":"Components created during render will reset their state each time they are created. Declare components outside of render. ","description":null,"severity":"InvalidReact","suggestions":null,"loc":{"start":{"line":4,"column":10,"index":118},"end":{"line":4,"column":19,"index":127},"filename":"invalid-dynamically-constructed-component-method-call.ts"}}},"fnLoc":null} -{"kind":"CompileError","detail":{"options":{"reason":"The component may be created during render","description":null,"severity":"InvalidReact","suggestions":null,"loc":{"start":{"line":3,"column":20,"index":91},"end":{"line":3,"column":35,"index":106},"filename":"invalid-dynamically-constructed-component-method-call.ts"}}},"fnLoc":null} +{"kind":"CompileError","detail":{"options":{"category":"StaticComponents","severity":"InvalidReact","reason":"Cannot create components during render","description":"Components created during render will reset their state each time they are created. Declare components outside of render. ","details":[{"kind":"error","loc":{"start":{"line":4,"column":10,"index":118},"end":{"line":4,"column":19,"index":127},"filename":"invalid-dynamically-constructed-component-method-call.ts"},"message":"This component is created during render"},{"kind":"error","loc":{"start":{"line":3,"column":20,"index":91},"end":{"line":3,"column":35,"index":106},"filename":"invalid-dynamically-constructed-component-method-call.ts"},"message":"The component is created during render here"}]}},"fnLoc":null} {"kind":"CompileSuccess","fnLoc":{"start":{"line":2,"column":0,"index":45},"end":{"line":5,"column":1,"index":133},"filename":"invalid-dynamically-constructed-component-method-call.ts"},"fnName":"Example","memoSlots":4,"memoBlocks":2,"memoValues":2,"prunedMemoBlocks":0,"prunedMemoValues":0} ``` diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/static-components/invalid-dynamically-constructed-component-new.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/static-components/invalid-dynamically-constructed-component-new.expect.md index 02e9f4f4a4b8d..a44cf6a9f0e0c 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/static-components/invalid-dynamically-constructed-component-new.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/static-components/invalid-dynamically-constructed-component-new.expect.md @@ -32,8 +32,7 @@ function Example(props) { ## Logs ``` -{"kind":"CompileError","detail":{"options":{"reason":"Components created during render will reset their state each time they are created. Declare components outside of render. ","description":null,"severity":"InvalidReact","suggestions":null,"loc":{"start":{"line":4,"column":10,"index":125},"end":{"line":4,"column":19,"index":134},"filename":"invalid-dynamically-constructed-component-new.ts"}}},"fnLoc":null} -{"kind":"CompileError","detail":{"options":{"reason":"The component may be created during render","description":null,"severity":"InvalidReact","suggestions":null,"loc":{"start":{"line":3,"column":20,"index":91},"end":{"line":3,"column":42,"index":113},"filename":"invalid-dynamically-constructed-component-new.ts"}}},"fnLoc":null} +{"kind":"CompileError","detail":{"options":{"category":"StaticComponents","severity":"InvalidReact","reason":"Cannot create components during render","description":"Components created during render will reset their state each time they are created. Declare components outside of render. ","details":[{"kind":"error","loc":{"start":{"line":4,"column":10,"index":125},"end":{"line":4,"column":19,"index":134},"filename":"invalid-dynamically-constructed-component-new.ts"},"message":"This component is created during render"},{"kind":"error","loc":{"start":{"line":3,"column":20,"index":91},"end":{"line":3,"column":42,"index":113},"filename":"invalid-dynamically-constructed-component-new.ts"},"message":"The component is created during render here"}]}},"fnLoc":null} {"kind":"CompileSuccess","fnLoc":{"start":{"line":2,"column":0,"index":45},"end":{"line":5,"column":1,"index":140},"filename":"invalid-dynamically-constructed-component-new.ts"},"fnName":"Example","memoSlots":1,"memoBlocks":1,"memoValues":1,"prunedMemoBlocks":0,"prunedMemoValues":0} ``` diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/switch-with-fallthrough.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/switch-with-fallthrough.expect.md index c54631092c0ac..6fd911c432716 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/switch-with-fallthrough.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/switch-with-fallthrough.expect.md @@ -43,22 +43,17 @@ export const FIXTURE_ENTRYPOINT = { ```javascript function foo(x) { bb0: switch (x) { - case 0: { - } - case 1: { - } + case 0: + case 1: case 2: { break bb0; } case 3: { break bb0; } - case 4: { - } - case 5: { - } - default: { - } + case 4: + case 5: + default: } } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ternary-expression.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ternary-expression.expect.md index 171a57f3ea5ed..6e99be2e6f4cc 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ternary-expression.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ternary-expression.expect.md @@ -3,7 +3,7 @@ ```javascript function ternary(props) { - const a = props.a && props.b ? props.c || props.d : props.e ?? props.f; + const a = props.a && props.b ? props.c || props.d : (props.e ?? props.f); const b = props.a ? (props.b && props.c ? props.d : props.e) : props.f; return a ? b : null; } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ternary-expression.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ternary-expression.js index 8a741ccb12f25..2a39d90bbcd6d 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ternary-expression.js +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ternary-expression.js @@ -1,5 +1,5 @@ function ternary(props) { - const a = props.a && props.b ? props.c || props.d : props.e ?? props.f; + const a = props.a && props.b ? props.c || props.d : (props.e ?? props.f); const b = props.a ? (props.b && props.c ? props.d : props.e) : props.f; return a ? b : null; } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/todo.error.object-pattern-computed-key.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/todo.error.object-pattern-computed-key.expect.md index 1856784ce0eb7..7bc1e49069b6a 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/todo.error.object-pattern-computed-key.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/todo.error.object-pattern-computed-key.expect.md @@ -21,10 +21,15 @@ export const FIXTURE_ENTRYPOINT = { ## Error ``` +Found 1 error: + +Todo: (BuildHIR::lowerAssignment) Handle computed properties in ObjectPattern + +todo.error.object-pattern-computed-key.ts:5:9 3 | const SCALE = 2; 4 | function Component(props) { > 5 | const {[props.name]: value} = props; - | ^^^^^^^^^^^^^^^^^^^ Todo: (BuildHIR::lowerAssignment) Handle computed properties in ObjectPattern (5:5) + | ^^^^^^^^^^^^^^^^^^^ (BuildHIR::lowerAssignment) Handle computed properties in ObjectPattern 6 | return value; 7 | } 8 | diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/bailout-retry/error.todo-syntax.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/bailout-retry/error.todo-syntax.expect.md index aa3d989296a77..006d2a49c0203 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/bailout-retry/error.todo-syntax.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/bailout-retry/error.todo-syntax.expect.md @@ -29,10 +29,17 @@ function Component({prop1}) { ## Error ``` +Found 1 error: + +Error: [Fire] Untransformed reference to compiler-required feature. + + Todo: (BuildHIR::lowerStatement) Handle TryStatement without a catch clause (11:4) + +error.todo-syntax.ts:18:4 16 | }; 17 | useEffect(() => { > 18 | fire(foo()); - | ^^^^ InvalidReact: [Fire] Untransformed reference to compiler-required feature. Either remove this `fire` call or ensure it is successfully transformed by the compiler. (Bailout reason: Todo: (BuildHIR::lowerStatement) Handle TryStatement without a catch clause (11:15)) (18:18) + | ^^^^ Untransformed `fire` call 19 | }); 20 | } 21 | diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/bailout-retry/error.untransformed-fire-reference.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/bailout-retry/error.untransformed-fire-reference.expect.md index 0141ffb8adb9a..8481ed2c576b0 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/bailout-retry/error.untransformed-fire-reference.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/bailout-retry/error.untransformed-fire-reference.expect.md @@ -13,10 +13,17 @@ console.log(fire == null); ## Error ``` +Found 1 error: + +Error: [Fire] Untransformed reference to compiler-required feature. + + null + +error.untransformed-fire-reference.ts:4:12 2 | import {fire} from 'react'; 3 | > 4 | console.log(fire == null); - | ^^^^ InvalidReact: [Fire] Untransformed reference to compiler-required feature. Either remove this `fire` call or ensure it is successfully transformed by the compiler (4:4) + | ^^^^ Untransformed `fire` call 5 | ``` diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/bailout-retry/error.use-no-memo.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/bailout-retry/error.use-no-memo.expect.md index 275012351c25b..f84686bc36bd2 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/bailout-retry/error.use-no-memo.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/bailout-retry/error.use-no-memo.expect.md @@ -30,10 +30,17 @@ function Component({props, bar}) { ## Error ``` +Found 1 error: + +Error: [Fire] Untransformed reference to compiler-required feature. + + null + +error.use-no-memo.ts:15:4 13 | }; 14 | useEffect(() => { > 15 | fire(foo(props)); - | ^^^^ InvalidReact: [Fire] Untransformed reference to compiler-required feature. Either remove this `fire` call or ensure it is successfully transformed by the compiler (15:15) + | ^^^^ Untransformed `fire` call 16 | fire(foo()); 17 | fire(bar()); 18 | }); diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/bailout-retry/infer-deps-on-retry.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/bailout-retry/infer-deps-on-retry.expect.md index 36ed7a3d3689b..29201225afb2c 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/bailout-retry/infer-deps-on-retry.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/bailout-retry/infer-deps-on-retry.expect.md @@ -3,7 +3,7 @@ ```javascript // @inferEffectDependencies @panicThreshold:"none" -import {useRef} from 'react'; +import {useRef, AUTODEPS} from 'react'; import {useSpecialEffect} from 'shared-runtime'; /** @@ -14,9 +14,13 @@ import {useSpecialEffect} from 'shared-runtime'; function useFoo({cond}) { const ref = useRef(); const derived = cond ? ref.current : makeObject(); - useSpecialEffect(() => { - log(derived); - }, [derived]); + useSpecialEffect( + () => { + log(derived); + }, + [derived], + AUTODEPS + ); return ref; } @@ -26,7 +30,7 @@ function useFoo({cond}) { ```javascript // @inferEffectDependencies @panicThreshold:"none" -import { useRef } from "react"; +import { useRef, AUTODEPS } from "react"; import { useSpecialEffect } from "shared-runtime"; /** diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/bailout-retry/infer-deps-on-retry.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/bailout-retry/infer-deps-on-retry.js index 4f6b908b5397a..75d8186501804 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/bailout-retry/infer-deps-on-retry.js +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/bailout-retry/infer-deps-on-retry.js @@ -1,5 +1,5 @@ // @inferEffectDependencies @panicThreshold:"none" -import {useRef} from 'react'; +import {useRef, AUTODEPS} from 'react'; import {useSpecialEffect} from 'shared-runtime'; /** @@ -10,8 +10,12 @@ import {useSpecialEffect} from 'shared-runtime'; function useFoo({cond}) { const ref = useRef(); const derived = cond ? ref.current : makeObject(); - useSpecialEffect(() => { - log(derived); - }, [derived]); + useSpecialEffect( + () => { + log(derived); + }, + [derived], + AUTODEPS + ); return ref; } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-mix-fire-and-no-fire.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-mix-fire-and-no-fire.expect.md index e73451a896ee4..1eb6bf66e9642 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-mix-fire-and-no-fire.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-mix-fire-and-no-fire.expect.md @@ -27,10 +27,17 @@ function Component(props) { ## Error ``` +Found 1 error: + +Error: Cannot compile `fire` + +All uses of foo must be either used with a fire() call in this effect or not used with a fire() call at all. foo was used with fire() on line 10:10 in this effect. + +error.invalid-mix-fire-and-no-fire.ts:11:6 9 | function nested() { 10 | fire(foo(props)); > 11 | foo(props); - | ^^^ InvalidReact: Cannot compile `fire`. All uses of foo must be either used with a fire() call in this effect or not used with a fire() call at all. foo was used with fire() on line 10:10 in this effect (11:11) + | ^^^ Cannot compile `fire` 12 | } 13 | 14 | nested(); diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-multiple-args.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-multiple-args.expect.md index 8329717cb3939..c519d43fb41c0 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-multiple-args.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-multiple-args.expect.md @@ -22,10 +22,17 @@ function Component({bar, baz}) { ## Error ``` +Found 1 error: + +Error: Cannot compile `fire` + +fire() can only take in a single call expression as an argument but received multiple arguments. + +error.invalid-multiple-args.ts:9:4 7 | }; 8 | useEffect(() => { > 9 | fire(foo(bar), baz); - | ^^^^^^^^^^^^^^^^^^^ InvalidReact: Cannot compile `fire`. fire() can only take in a single call expression as an argument but received multiple arguments (9:9) + | ^^^^^^^^^^^^^^^^^^^ Cannot compile `fire` 10 | }); 11 | 12 | return null; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-nested-use-effect.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-nested-use-effect.expect.md index 580fd6a2a68b8..2e767c3c71e61 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-nested-use-effect.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-nested-use-effect.expect.md @@ -28,10 +28,17 @@ function Component(props) { ## Error ``` +Found 1 error: + +Error: Hooks must be called at the top level in the body of a function component or custom hook, and may not be called within function expressions. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning) + +Cannot call useEffect within a function expression. + +error.invalid-nested-use-effect.ts:9:4 7 | }; 8 | useEffect(() => { > 9 | useEffect(() => { - | ^^^^^^^^^ InvalidReact: Hooks must be called at the top level in the body of a function component or custom hook, and may not be called within function expressions. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning). Cannot call useEffect within a function component (9:9) + | ^^^^^^^^^ Hooks must be called at the top level in the body of a function component or custom hook, and may not be called within function expressions. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning) 10 | function nested() { 11 | fire(foo(props)); 12 | } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-not-call.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-not-call.expect.md index 855c7b7d706cb..40c4bc5394dce 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-not-call.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-not-call.expect.md @@ -22,10 +22,17 @@ function Component(props) { ## Error ``` +Found 1 error: + +Error: Cannot compile `fire` + +`fire()` can only receive a function call such as `fire(fn(a,b)). Method calls and other expressions are not allowed. + +error.invalid-not-call.ts:9:4 7 | }; 8 | useEffect(() => { > 9 | fire(props); - | ^^^^^^^^^^^ InvalidReact: Cannot compile `fire`. `fire()` can only receive a function call such as `fire(fn(a,b)). Method calls and other expressions are not allowed (9:9) + | ^^^^^^^^^^^ Cannot compile `fire` 10 | }); 11 | 12 | return null; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-outside-effect.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-outside-effect.expect.md index 687a21f98cdb4..81c36a362cff0 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-outside-effect.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-outside-effect.expect.md @@ -24,15 +24,33 @@ function Component({props, bar}) { ## Error ``` +Found 2 errors: + +Invariant: Cannot compile `fire` + +Cannot use `fire` outside of a useEffect function. + +error.invalid-outside-effect.ts:8:2 6 | console.log(props); 7 | }; > 8 | fire(foo(props)); - | ^^^^ Invariant: Cannot compile `fire`. Cannot use `fire` outside of a useEffect function (8:8) - -Invariant: Cannot compile `fire`. Cannot use `fire` outside of a useEffect function (11:11) + | ^^^^ Cannot compile `fire` 9 | 10 | useCallback(() => { 11 | fire(foo(props)); + +Invariant: Cannot compile `fire` + +Cannot use `fire` outside of a useEffect function. + +error.invalid-outside-effect.ts:11:4 + 9 | + 10 | useCallback(() => { +> 11 | fire(foo(props)); + | ^^^^ Cannot compile `fire` + 12 | }, [foo, props]); + 13 | + 14 | return null; ``` \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-rewrite-deps-no-array-literal.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-rewrite-deps-no-array-literal.expect.md index dcd9312bb2e53..96cea9c08f0f2 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-rewrite-deps-no-array-literal.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-rewrite-deps-no-array-literal.expect.md @@ -25,10 +25,17 @@ function Component(props) { ## Error ``` +Found 1 error: + +Invariant: Cannot compile `fire` + +You must use an array literal for an effect dependency array when that effect uses `fire()`. + +error.invalid-rewrite-deps-no-array-literal.ts:13:5 11 | useEffect(() => { 12 | fire(foo(props)); > 13 | }, deps); - | ^^^^ Invariant: Cannot compile `fire`. You must use an array literal for an effect dependency array when that effect uses `fire()` (13:13) + | ^^^^ Cannot compile `fire` 14 | 15 | return null; 16 | } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-rewrite-deps-spread.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-rewrite-deps-spread.expect.md index 91c5523564cdd..4dc5336ebe011 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-rewrite-deps-spread.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-rewrite-deps-spread.expect.md @@ -28,10 +28,17 @@ function Component(props) { ## Error ``` +Found 1 error: + +Invariant: Cannot compile `fire` + +You must use an array literal for an effect dependency array when that effect uses `fire()`. + +error.invalid-rewrite-deps-spread.ts:15:7 13 | fire(foo(props)); 14 | }, > 15 | ...deps - | ^^^^ Invariant: Cannot compile `fire`. You must use an array literal for an effect dependency array when that effect uses `fire()` (15:15) + | ^^^^ Cannot compile `fire` 16 | ); 17 | 18 | return null; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-spread.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-spread.expect.md index c0b797fc14471..dcd302bbe104c 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-spread.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-spread.expect.md @@ -22,10 +22,17 @@ function Component(props) { ## Error ``` +Found 1 error: + +Error: Cannot compile `fire` + +fire() can only take in a single call expression as an argument but received a spread argument. + +error.invalid-spread.ts:9:4 7 | }; 8 | useEffect(() => { > 9 | fire(...foo); - | ^^^^^^^^^^^^ InvalidReact: Cannot compile `fire`. fire() can only take in a single call expression as an argument but received a spread argument (9:9) + | ^^^^^^^^^^^^ Cannot compile `fire` 10 | }); 11 | 12 | return null; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.todo-method.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.todo-method.expect.md index 3f237cfc6f364..67410297f3032 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.todo-method.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.todo-method.expect.md @@ -22,10 +22,17 @@ function Component(props) { ## Error ``` +Found 1 error: + +Error: Cannot compile `fire` + +`fire()` can only receive a function call such as `fire(fn(a,b)). Method calls and other expressions are not allowed. + +error.todo-method.ts:9:4 7 | }; 8 | useEffect(() => { > 9 | fire(props.foo()); - | ^^^^^^^^^^^^^^^^^ InvalidReact: Cannot compile `fire`. `fire()` can only receive a function call such as `fire(fn(a,b)). Method calls and other expressions are not allowed (9:9) + | ^^^^^^^^^^^^^^^^^ Cannot compile `fire` 10 | }); 11 | 12 | return null; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/fire-and-autodeps.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/fire-and-autodeps.expect.md deleted file mode 100644 index 46e813bf9d6cb..0000000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/fire-and-autodeps.expect.md +++ /dev/null @@ -1,59 +0,0 @@ - -## Input - -```javascript -// @enableFire @inferEffectDependencies -import {fire, useEffect} from 'react'; - -function Component(props) { - const foo = arg => { - console.log(arg, props.bar); - }; - useEffect(() => { - fire(foo(props)); - }); - - return null; -} - -``` - -## Code - -```javascript -import { c as _c, useFire } from "react/compiler-runtime"; // @enableFire @inferEffectDependencies -import { fire, useEffect } from "react"; - -function Component(props) { - const $ = _c(5); - let t0; - if ($[0] !== props.bar) { - t0 = (arg) => { - console.log(arg, props.bar); - }; - $[0] = props.bar; - $[1] = t0; - } else { - t0 = $[1]; - } - const foo = t0; - const t1 = useFire(foo); - let t2; - if ($[2] !== props || $[3] !== t1) { - t2 = () => { - t1(props); - }; - $[2] = props; - $[3] = t1; - $[4] = t2; - } else { - t2 = $[4]; - } - useEffect(t2, [props]); - return null; -} - -``` - -### 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/transform-fire/fire-and-autodeps.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/fire-and-autodeps.js deleted file mode 100644 index e2a0068a19067..0000000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/fire-and-autodeps.js +++ /dev/null @@ -1,13 +0,0 @@ -// @enableFire @inferEffectDependencies -import {fire, useEffect} from 'react'; - -function Component(props) { - const foo = arg => { - console.log(arg, props.bar); - }; - useEffect(() => { - fire(foo(props)); - }); - - return null; -} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/try-catch-within-function-expression-returns-caught-value.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/try-catch-within-function-expression-returns-caught-value.expect.md index db8877f061bc1..1b45e08393bae 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/try-catch-within-function-expression-returns-caught-value.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/try-catch-within-function-expression-returns-caught-value.expect.md @@ -29,10 +29,10 @@ import { c as _c } from "react/compiler-runtime"; import { throwInput } from "shared-runtime"; function Component(props) { - const $ = _c(4); + const $ = _c(2); let t0; if ($[0] !== props) { - t0 = () => { + const callback = () => { try { throwInput([props.value]); } catch (t1) { @@ -40,21 +40,14 @@ function Component(props) { return e; } }; + + t0 = callback(); $[0] = props; $[1] = t0; } else { t0 = $[1]; } - const callback = t0; - let t1; - if ($[2] !== callback) { - t1 = callback(); - $[2] = callback; - $[3] = t1; - } else { - t1 = $[3]; - } - return t1; + return t0; } export const FIXTURE_ENTRYPOINT = { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ts-as-expression-default-value.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ts-as-expression-default-value.expect.md new file mode 100644 index 0000000000000..505224cf9ed92 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ts-as-expression-default-value.expect.md @@ -0,0 +1,67 @@ + +## Input + +```javascript +type Status = 'pending' | 'success' | 'error'; + +const StatusIndicator = ({status}: {status: Status}) => { + return
Status: {status}
; +}; + +const Component = ({status = 'pending' as Status}) => { + return ; +}; + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{status: 'success'}], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; +type Status = "pending" | "success" | "error"; + +const StatusIndicator = (t0) => { + const $ = _c(3); + const { status } = t0; + const t1 = `status-${status}`; + let t2; + if ($[0] !== status || $[1] !== t1) { + t2 =
Status: {status}
; + $[0] = status; + $[1] = t1; + $[2] = t2; + } else { + t2 = $[2]; + } + return t2; +}; + +const Component = (t0) => { + const $ = _c(2); + const { status: t1 } = t0; + const status = t1 === undefined ? ("pending" as Status) : t1; + let t2; + if ($[0] !== status) { + t2 = ; + $[0] = status; + $[1] = t2; + } else { + t2 = $[1]; + } + return t2; +}; + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ status: "success" }], +}; + +``` + +### Eval output +(kind: ok)
Status: success
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ts-as-expression-default-value.tsx b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ts-as-expression-default-value.tsx new file mode 100644 index 0000000000000..715efd5bdc8d2 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ts-as-expression-default-value.tsx @@ -0,0 +1,14 @@ +type Status = 'pending' | 'success' | 'error'; + +const StatusIndicator = ({status}: {status: Status}) => { + return
Status: {status}
; +}; + +const Component = ({status = 'pending' as Status}) => { + return ; +}; + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{status: 'success'}], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ts-enum-inline.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ts-enum-inline.expect.md new file mode 100644 index 0000000000000..bb31427d0810c --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ts-enum-inline.expect.md @@ -0,0 +1,59 @@ + +## Input + +```javascript +function Component(props) { + enum Bool { + True = 'true', + False = 'false', + } + + let bool: Bool = Bool.False; + if (props.value) { + bool = Bool.True; + } + return
{bool}
; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{value: true}], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; +function Component(props) { + const $ = _c(2); + enum Bool { + True = "true", + False = "false", + } + + let bool = Bool.False; + if (props.value) { + bool = Bool.True; + } + let t0; + if ($[0] !== bool) { + t0 =
{bool}
; + $[0] = bool; + $[1] = t0; + } else { + t0 = $[1]; + } + return t0; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ value: true }], +}; + +``` + +### Eval output +(kind: ok)
true
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ts-enum-inline.tsx b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ts-enum-inline.tsx new file mode 100644 index 0000000000000..7fcec79259ce2 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ts-enum-inline.tsx @@ -0,0 +1,17 @@ +function Component(props) { + enum Bool { + True = 'true', + False = 'false', + } + + let bool: Bool = Bool.False; + if (props.value) { + bool = Bool.True; + } + return
{bool}
; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{value: true}], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ts-non-null-expression-default-value.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ts-non-null-expression-default-value.expect.md new file mode 100644 index 0000000000000..7af6bc996a99a --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ts-non-null-expression-default-value.expect.md @@ -0,0 +1,54 @@ + +## Input + +```javascript +const THEME_MAP: ReadonlyMap = new Map([ + ['default', 'light'], + ['dark', 'dark'], +]); + +export const Component = ({theme = THEME_MAP.get('default')!}) => { + return
User preferences
; +}; + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{status: 'success'}], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; +const THEME_MAP: ReadonlyMap = new Map([ + ["default", "light"], + ["dark", "dark"], +]); + +export const Component = (t0) => { + const $ = _c(2); + const { theme: t1 } = t0; + const theme = t1 === undefined ? THEME_MAP.get("default") : t1; + const t2 = `theme-${theme}`; + let t3; + if ($[0] !== t2) { + t3 =
User preferences
; + $[0] = t2; + $[1] = t3; + } else { + t3 = $[1]; + } + return t3; +}; + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ status: "success" }], +}; + +``` + +### Eval output +(kind: ok)
User preferences
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ts-non-null-expression-default-value.tsx b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ts-non-null-expression-default-value.tsx new file mode 100644 index 0000000000000..c1d835d6f0f06 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ts-non-null-expression-default-value.tsx @@ -0,0 +1,13 @@ +const THEME_MAP: ReadonlyMap = new Map([ + ['default', 'light'], + ['dark', 'dark'], +]); + +export const Component = ({theme = THEME_MAP.get('default')!}) => { + return
User preferences
; +}; + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{status: 'success'}], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/type-annotations/type-annotation-var-array.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/type-annotations/type-annotation-var-array.expect.md index ce8e06fcf925f..fc829218f5a15 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/type-annotations/type-annotation-var-array.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/type-annotations/type-annotation-var-array.expect.md @@ -25,25 +25,17 @@ export const FIXTURE_ENTRYPOINT = { ```javascript import { c as _c } from "react/compiler-runtime"; // @enableUseTypeAnnotations function Component(props) { - const $ = _c(4); + const $ = _c(2); let t0; if ($[0] !== props.id) { - t0 = makeArray(props.id); + const x = makeArray(props.id); + t0 = x.at(0); $[0] = props.id; $[1] = t0; } else { t0 = $[1]; } - const x = t0; - let t1; - if ($[2] !== x) { - t1 = x.at(0); - $[2] = x; - $[3] = t1; - } else { - t1 = $[3]; - } - const y = t1; + const y = t0; return y; } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/type-annotations/type-annotation-var-array_.flow.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/type-annotations/type-annotation-var-array_.flow.expect.md index 03d1f66740bee..5058015fd3239 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/type-annotations/type-annotation-var-array_.flow.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/type-annotations/type-annotation-var-array_.flow.expect.md @@ -29,25 +29,17 @@ import { c as _c } from "react/compiler-runtime"; import { identity } from "shared-runtime"; function Component(props) { - const $ = _c(4); + const $ = _c(2); let t0; if ($[0] !== props.id) { - t0 = makeArray(props.id); + const x = makeArray(props.id); + t0 = x.at(0); $[0] = props.id; $[1] = t0; } else { t0 = $[1]; } - const x = t0; - let t1; - if ($[2] !== x) { - t1 = x.at(0); - $[2] = x; - $[3] = t1; - } else { - t1 = $[3]; - } - const y = t1; + const y = t0; return y; } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/type-provider-log-default-import.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/type-provider-log-default-import.expect.md index c3c45beb86544..dd2eebb606cda 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/type-provider-log-default-import.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/type-provider-log-default-import.expect.md @@ -47,77 +47,73 @@ export function Component(t0) { const $ = _c(17); const { a, b } = t0; let t1; - let t2; if ($[0] !== a) { - t2 = { a }; + t1 = { a }; $[0] = a; - $[1] = t2; + $[1] = t1; } else { - t2 = $[1]; + t1 = $[1]; } - t1 = t2; const item1 = t1; - let t3; - let t4; + let t2; if ($[2] !== b) { - t4 = { b }; + t2 = { b }; $[2] = b; - $[3] = t4; + $[3] = t2; } else { - t4 = $[3]; + t2 = $[3]; } - t3 = t4; - const item2 = t3; + const item2 = t2; typedLog(item1, item2); - let t5; + let t3; if ($[4] !== a) { - t5 = [a]; + t3 = [a]; $[4] = a; - $[5] = t5; + $[5] = t3; } else { - t5 = $[5]; + t3 = $[5]; } - let t6; - if ($[6] !== item1 || $[7] !== t5) { - t6 = ; + let t4; + if ($[6] !== item1 || $[7] !== t3) { + t4 = ; $[6] = item1; - $[7] = t5; - $[8] = t6; + $[7] = t3; + $[8] = t4; } else { - t6 = $[8]; + t4 = $[8]; } - let t7; + let t5; if ($[9] !== b) { - t7 = [b]; + t5 = [b]; $[9] = b; - $[10] = t7; + $[10] = t5; } else { - t7 = $[10]; + t5 = $[10]; } - let t8; - if ($[11] !== item2 || $[12] !== t7) { - t8 = ; + let t6; + if ($[11] !== item2 || $[12] !== t5) { + t6 = ; $[11] = item2; - $[12] = t7; - $[13] = t8; + $[12] = t5; + $[13] = t6; } else { - t8 = $[13]; + t6 = $[13]; } - let t9; - if ($[14] !== t6 || $[15] !== t8) { - t9 = ( + let t7; + if ($[14] !== t4 || $[15] !== t6) { + t7 = ( <> + {t4} {t6} - {t8} ); - $[14] = t6; - $[15] = t8; - $[16] = t9; + $[14] = t4; + $[15] = t6; + $[16] = t7; } else { - t9 = $[16]; + t7 = $[16]; } - return t9; + return t7; } export const FIXTURE_ENTRYPOINT = { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/type-provider-log.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/type-provider-log.expect.md index 4acbd2dfdb3be..ba4c4fe224901 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/type-provider-log.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/type-provider-log.expect.md @@ -45,77 +45,73 @@ export function Component(t0) { const $ = _c(17); const { a, b } = t0; let t1; - let t2; if ($[0] !== a) { - t2 = { a }; + t1 = { a }; $[0] = a; - $[1] = t2; + $[1] = t1; } else { - t2 = $[1]; + t1 = $[1]; } - t1 = t2; const item1 = t1; - let t3; - let t4; + let t2; if ($[2] !== b) { - t4 = { b }; + t2 = { b }; $[2] = b; - $[3] = t4; + $[3] = t2; } else { - t4 = $[3]; + t2 = $[3]; } - t3 = t4; - const item2 = t3; + const item2 = t2; typedLog(item1, item2); - let t5; + let t3; if ($[4] !== a) { - t5 = [a]; + t3 = [a]; $[4] = a; - $[5] = t5; + $[5] = t3; } else { - t5 = $[5]; + t3 = $[5]; } - let t6; - if ($[6] !== item1 || $[7] !== t5) { - t6 = ; + let t4; + if ($[6] !== item1 || $[7] !== t3) { + t4 = ; $[6] = item1; - $[7] = t5; - $[8] = t6; + $[7] = t3; + $[8] = t4; } else { - t6 = $[8]; + t4 = $[8]; } - let t7; + let t5; if ($[9] !== b) { - t7 = [b]; + t5 = [b]; $[9] = b; - $[10] = t7; + $[10] = t5; } else { - t7 = $[10]; + t5 = $[10]; } - let t8; - if ($[11] !== item2 || $[12] !== t7) { - t8 = ; + let t6; + if ($[11] !== item2 || $[12] !== t5) { + t6 = ; $[11] = item2; - $[12] = t7; - $[13] = t8; + $[12] = t5; + $[13] = t6; } else { - t8 = $[13]; + t6 = $[13]; } - let t9; - if ($[14] !== t6 || $[15] !== t8) { - t9 = ( + let t7; + if ($[14] !== t4 || $[15] !== t6) { + t7 = ( <> + {t4} {t6} - {t8} ); - $[14] = t6; - $[15] = t8; - $[16] = t9; + $[14] = t4; + $[15] = t6; + $[16] = t7; } else { - t9 = $[16]; + t7 = $[16]; } - return t9; + return t7; } export const FIXTURE_ENTRYPOINT = { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/type-provider-store-capture-namespace-import.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/type-provider-store-capture-namespace-import.expect.md index 87b5d7a09e28d..1de8d9a170d20 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/type-provider-store-capture-namespace-import.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/type-provider-store-capture-namespace-import.expect.md @@ -51,28 +51,23 @@ export function Component(t0) { const $ = _c(27); const { a, b } = t0; let t1; - let t2; if ($[0] !== a) { - t2 = { a }; + t1 = { a }; $[0] = a; - $[1] = t2; + $[1] = t1; } else { - t2 = $[1]; + t1 = $[1]; } - t1 = t2; const item1 = t1; - let t3; - let t4; + let t2; if ($[2] !== b) { - t4 = { b }; + t2 = { b }; $[2] = b; - $[3] = t4; + $[3] = t2; } else { - t4 = $[3]; + t2 = $[3]; } - t3 = t4; - const item2 = t3; - let t5; + const item2 = t2; let items; if ($[4] !== item1 || $[5] !== item2) { items = []; @@ -84,77 +79,76 @@ export function Component(t0) { } else { items = $[6]; } - t5 = items; - const items_0 = t5; - let t6; + const items_0 = items; + let t3; if ($[7] !== a) { - t6 = [a]; + t3 = [a]; $[7] = a; - $[8] = t6; + $[8] = t3; } else { - t6 = $[8]; + t3 = $[8]; } - let t7; - if ($[9] !== items_0[0] || $[10] !== t6) { - t7 = ; + let t4; + if ($[9] !== items_0[0] || $[10] !== t3) { + t4 = ; $[9] = items_0[0]; - $[10] = t6; - $[11] = t7; + $[10] = t3; + $[11] = t4; } else { - t7 = $[11]; + t4 = $[11]; } - let t8; + let t5; if ($[12] !== b) { - t8 = [b]; + t5 = [b]; $[12] = b; - $[13] = t8; + $[13] = t5; } else { - t8 = $[13]; + t5 = $[13]; } - let t9; - if ($[14] !== items_0[1] || $[15] !== t8) { - t9 = ; + let t6; + if ($[14] !== items_0[1] || $[15] !== t5) { + t6 = ; $[14] = items_0[1]; - $[15] = t8; - $[16] = t9; + $[15] = t5; + $[16] = t6; } else { - t9 = $[16]; + t6 = $[16]; } - let t10; + let t7; if ($[17] !== a || $[18] !== b) { - t10 = [a, b]; + t7 = [a, b]; $[17] = a; $[18] = b; - $[19] = t10; + $[19] = t7; } else { - t10 = $[19]; + t7 = $[19]; } - let t11; - if ($[20] !== items_0 || $[21] !== t10) { - t11 = ; + let t8; + if ($[20] !== items_0 || $[21] !== t7) { + t8 = ; $[20] = items_0; - $[21] = t10; - $[22] = t11; + $[21] = t7; + $[22] = t8; } else { - t11 = $[22]; + t8 = $[22]; } - let t12; - if ($[23] !== t11 || $[24] !== t7 || $[25] !== t9) { - t12 = ( + let t9; + if ($[23] !== t4 || $[24] !== t6 || $[25] !== t8) { + t9 = ( <> - {t7} - {t9} - {t11} + {t4} + {t6} + {t8} ); - $[23] = t11; - $[24] = t7; - $[25] = t9; - $[26] = t12; + $[23] = t4; + $[24] = t6; + $[25] = t8; + $[26] = t9; } else { - t12 = $[26]; + t9 = $[26]; } - return t12; + return t9; } export const FIXTURE_ENTRYPOINT = { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/type-provider-store-capture.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/type-provider-store-capture.expect.md index c71e4c2530fce..a3b34b0274a70 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/type-provider-store-capture.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/type-provider-store-capture.expect.md @@ -51,28 +51,23 @@ export function Component(t0) { const $ = _c(27); const { a, b } = t0; let t1; - let t2; if ($[0] !== a) { - t2 = { a }; + t1 = { a }; $[0] = a; - $[1] = t2; + $[1] = t1; } else { - t2 = $[1]; + t1 = $[1]; } - t1 = t2; const item1 = t1; - let t3; - let t4; + let t2; if ($[2] !== b) { - t4 = { b }; + t2 = { b }; $[2] = b; - $[3] = t4; + $[3] = t2; } else { - t4 = $[3]; + t2 = $[3]; } - t3 = t4; - const item2 = t3; - let t5; + const item2 = t2; let items; if ($[4] !== item1 || $[5] !== item2) { items = []; @@ -84,77 +79,76 @@ export function Component(t0) { } else { items = $[6]; } - t5 = items; - const items_0 = t5; - let t6; + const items_0 = items; + let t3; if ($[7] !== a) { - t6 = [a]; + t3 = [a]; $[7] = a; - $[8] = t6; + $[8] = t3; } else { - t6 = $[8]; + t3 = $[8]; } - let t7; - if ($[9] !== items_0[0] || $[10] !== t6) { - t7 = ; + let t4; + if ($[9] !== items_0[0] || $[10] !== t3) { + t4 = ; $[9] = items_0[0]; - $[10] = t6; - $[11] = t7; + $[10] = t3; + $[11] = t4; } else { - t7 = $[11]; + t4 = $[11]; } - let t8; + let t5; if ($[12] !== b) { - t8 = [b]; + t5 = [b]; $[12] = b; - $[13] = t8; + $[13] = t5; } else { - t8 = $[13]; + t5 = $[13]; } - let t9; - if ($[14] !== items_0[1] || $[15] !== t8) { - t9 = ; + let t6; + if ($[14] !== items_0[1] || $[15] !== t5) { + t6 = ; $[14] = items_0[1]; - $[15] = t8; - $[16] = t9; + $[15] = t5; + $[16] = t6; } else { - t9 = $[16]; + t6 = $[16]; } - let t10; + let t7; if ($[17] !== a || $[18] !== b) { - t10 = [a, b]; + t7 = [a, b]; $[17] = a; $[18] = b; - $[19] = t10; + $[19] = t7; } else { - t10 = $[19]; + t7 = $[19]; } - let t11; - if ($[20] !== items_0 || $[21] !== t10) { - t11 = ; + let t8; + if ($[20] !== items_0 || $[21] !== t7) { + t8 = ; $[20] = items_0; - $[21] = t10; - $[22] = t11; + $[21] = t7; + $[22] = t8; } else { - t11 = $[22]; + t8 = $[22]; } - let t12; - if ($[23] !== t11 || $[24] !== t7 || $[25] !== t9) { - t12 = ( + let t9; + if ($[23] !== t4 || $[24] !== t6 || $[25] !== t8) { + t9 = ( <> - {t7} - {t9} - {t11} + {t4} + {t6} + {t8} ); - $[23] = t11; - $[24] = t7; - $[25] = t9; - $[26] = t12; + $[23] = t4; + $[24] = t6; + $[25] = t8; + $[26] = t9; } else { - t12 = $[26]; + t9 = $[26]; } - return t12; + return t9; } export const FIXTURE_ENTRYPOINT = { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/use-memo-simple.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/use-memo-simple.expect.md index d721128cb7088..b9e7e1bfe1079 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/use-memo-simple.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/use-memo-simple.expect.md @@ -22,25 +22,17 @@ export const FIXTURE_ENTRYPOINT = { import { c as _c } from "react/compiler-runtime"; function Component(props) { "use memo"; - const $ = _c(4); + const $ = _c(2); let t0; if ($[0] !== props.foo) { - t0 = [props.foo]; + const x = [props.foo]; + t0 =
"foo"
; $[0] = props.foo; $[1] = t0; } else { t0 = $[1]; } - const x = t0; - let t1; - if ($[2] !== x) { - t1 =
"foo"
; - $[2] = x; - $[3] = t1; - } else { - t1 = $[3]; - } - return t1; + return t0; } export const FIXTURE_ENTRYPOINT = { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/use-operator-call-expression.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/use-operator-call-expression.expect.md index 7f79cae4a0361..dad37e7dfd94d 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/use-operator-call-expression.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/use-operator-call-expression.expect.md @@ -70,34 +70,32 @@ function Inner(props) { const $ = _c(7); const input = use(FooContext); let t0; - let t1; if ($[0] !== input) { - t1 = [input]; + t0 = [input]; $[0] = input; - $[1] = t1; + $[1] = t0; } else { - t1 = $[1]; + t0 = $[1]; } - t0 = t1; const output = t0; - let t2; + let t1; if ($[2] !== input) { - t2 = [input]; + t1 = [input]; $[2] = input; - $[3] = t2; + $[3] = t1; } else { - t2 = $[3]; + t1 = $[3]; } - let t3; - if ($[4] !== output || $[5] !== t2) { - t3 = ; + let t2; + if ($[4] !== output || $[5] !== t1) { + t2 = ; $[4] = output; - $[5] = t2; - $[6] = t3; + $[5] = t1; + $[6] = t2; } else { - t3 = $[6]; + t2 = $[6]; } - return t3; + return t2; } export const FIXTURE_ENTRYPOINT = { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/use-operator-conditional.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/use-operator-conditional.expect.md index e00a5648164a4..ab645e81e336b 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/use-operator-conditional.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/use-operator-conditional.expect.md @@ -86,34 +86,32 @@ function Inner(props) { input; let t0; - let t1; if ($[0] !== input) { - t1 = [input]; + t0 = [input]; $[0] = input; - $[1] = t1; + $[1] = t0; } else { - t1 = $[1]; + t0 = $[1]; } - t0 = t1; const output = t0; - let t2; + let t1; if ($[2] !== input) { - t2 = [input]; + t1 = [input]; $[2] = input; - $[3] = t2; + $[3] = t1; } else { - t2 = $[3]; + t1 = $[3]; } - let t3; - if ($[4] !== output || $[5] !== t2) { - t3 = ; + let t2; + if ($[4] !== output || $[5] !== t1) { + t2 = ; $[4] = output; - $[5] = t2; - $[6] = t3; + $[5] = t1; + $[6] = t2; } else { - t3 = $[6]; + t2 = $[6]; } - return t3; + return t2; } export const FIXTURE_ENTRYPOINT = { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/use-operator-method-call.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/use-operator-method-call.expect.md index cf0093ea941bf..5eea8e6e19522 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/use-operator-method-call.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/use-operator-method-call.expect.md @@ -72,34 +72,32 @@ function Inner(props) { const $ = _c(7); const input = React.use(FooContext); let t0; - let t1; if ($[0] !== input) { - t1 = [input]; + t0 = [input]; $[0] = input; - $[1] = t1; + $[1] = t0; } else { - t1 = $[1]; + t0 = $[1]; } - t0 = t1; const output = t0; - let t2; + let t1; if ($[2] !== input) { - t2 = [input]; + t1 = [input]; $[2] = input; - $[3] = t2; + $[3] = t1; } else { - t2 = $[3]; + t1 = $[3]; } - let t3; - if ($[4] !== output || $[5] !== t2) { - t3 = ; + let t2; + if ($[4] !== output || $[5] !== t1) { + t2 = ; $[4] = output; - $[5] = t2; - $[6] = t3; + $[5] = t1; + $[6] = t2; } else { - t3 = $[6]; + t2 = $[6]; } - return t3; + return t2; } export const FIXTURE_ENTRYPOINT = { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useContext-read-context-in-callback-if-condition.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useContext-read-context-in-callback-if-condition.expect.md index 1d09cbb3597b4..611606ea38d2c 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useContext-read-context-in-callback-if-condition.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useContext-read-context-in-callback-if-condition.expect.md @@ -39,41 +39,34 @@ import { Stringify } from "shared-runtime"; const FooContext = createContext({ current: true }); function Component(props) { - const $ = _c(6); + const $ = _c(4); const foo = useContext(FooContext); let t0; if ($[0] !== foo.current) { - t0 = () => { + const getValue = () => { if (foo.current) { return {}; } else { return null; } }; + + t0 = getValue(); $[0] = foo.current; $[1] = t0; } else { t0 = $[1]; } - const getValue = t0; + const value = t0; let t1; - if ($[2] !== getValue) { - t1 = getValue(); - $[2] = getValue; + if ($[2] !== value) { + t1 = ; + $[2] = value; $[3] = t1; } else { t1 = $[3]; } - const value = t1; - let t2; - if ($[4] !== value) { - t2 = ; - $[4] = value; - $[5] = t2; - } else { - t2 = $[5]; - } - return t2; + return t1; } 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 aefbaecedb0d7..0a78a544b8ba8 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 @@ -37,23 +37,21 @@ import { useEffect } from "react"; function someGlobal() {} function useFoo() { const $ = _c(2); + const fn = _temp; let t0; - t0 = _temp; - const fn = t0; let t1; - let t2; if ($[0] === Symbol.for("react.memo_cache_sentinel")) { - t1 = () => { + t0 = () => { fn(); }; - t2 = [fn]; - $[0] = t1; - $[1] = t2; + t1 = [fn]; + $[0] = t0; + $[1] = t1; } else { - t1 = $[0]; - t2 = $[1]; + t0 = $[0]; + t1 = $[1]; } - useEffect(t1, t2); + useEffect(t0, t1); return null; } function _temp() { 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 2646478d39427..f3fb51cc58bb3 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 @@ -37,23 +37,21 @@ import * as React from "react"; function someGlobal() {} function useFoo() { const $ = _c(2); + const fn = _temp; let t0; - t0 = _temp; - const fn = t0; let t1; - let t2; if ($[0] === Symbol.for("react.memo_cache_sentinel")) { - t1 = () => { + t0 = () => { fn(); }; - t2 = [fn]; - $[0] = t1; - $[1] = t2; + t1 = [fn]; + $[0] = t0; + $[1] = t1; } else { - t1 = $[0]; - t2 = $[1]; + t0 = $[0]; + t1 = $[1]; } - React.useEffect(t1, t2); + React.useEffect(t0, t1); return null; } function _temp() { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useEffect/derived-state-one-time-init-no-error.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useEffect/derived-state-one-time-init-no-error.expect.md new file mode 100644 index 0000000000000..07a58aeef33ff --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useEffect/derived-state-one-time-init-no-error.expect.md @@ -0,0 +1,87 @@ + +## Input + +```javascript +// @validateNoDerivedComputationsInEffects +import {useEffect, useState} from 'react'; + +function Component({initialName}) { + const [name, setName] = useState(''); + + useEffect(() => { + setName(initialName); + }, []); + + return ( +
+ setName(e.target.value)} /> +
+ ); +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{initialName: 'John'}], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects +import { useEffect, useState } from "react"; + +function Component(t0) { + const $ = _c(6); + const { initialName } = t0; + const [name, setName] = useState(""); + let t1; + if ($[0] !== initialName) { + t1 = () => { + setName(initialName); + }; + $[0] = initialName; + $[1] = t1; + } else { + t1 = $[1]; + } + let t2; + if ($[2] === Symbol.for("react.memo_cache_sentinel")) { + t2 = []; + $[2] = t2; + } else { + t2 = $[2]; + } + useEffect(t1, t2); + let t3; + if ($[3] === Symbol.for("react.memo_cache_sentinel")) { + t3 = (e) => setName(e.target.value); + $[3] = t3; + } else { + t3 = $[3]; + } + let t4; + if ($[4] !== name) { + t4 = ( +
+ +
+ ); + $[4] = name; + $[5] = t4; + } else { + t4 = $[5]; + } + return t4; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ initialName: "John" }], +}; + +``` + +### Eval output +(kind: ok)
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useEffect/derived-state-one-time-init-no-error.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useEffect/derived-state-one-time-init-no-error.js new file mode 100644 index 0000000000000..c6705378a5e68 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useEffect/derived-state-one-time-init-no-error.js @@ -0,0 +1,21 @@ +// @validateNoDerivedComputationsInEffects +import {useEffect, useState} from 'react'; + +function Component({initialName}) { + const [name, setName] = useState(''); + + useEffect(() => { + setName(initialName); + }, []); + + return ( +
+ setName(e.target.value)} /> +
+ ); +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{initialName: 'John'}], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useEffect/derived-state-with-conditional-no-error.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useEffect/derived-state-with-conditional-no-error.expect.md new file mode 100644 index 0000000000000..b7a1c85d52c40 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useEffect/derived-state-with-conditional-no-error.expect.md @@ -0,0 +1,79 @@ + +## Input + +```javascript +// @validateNoDerivedComputationsInEffects +import {useEffect, useState} from 'react'; + +function Component({value, enabled}) { + const [localValue, setLocalValue] = useState(''); + + useEffect(() => { + if (enabled) { + setLocalValue(value); + } else { + setLocalValue('disabled'); + } + }, [value, enabled]); + + return
{localValue}
; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{value: 'test', enabled: true}], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects +import { useEffect, useState } from "react"; + +function Component(t0) { + const $ = _c(6); + const { value, enabled } = t0; + const [localValue, setLocalValue] = useState(""); + let t1; + let t2; + if ($[0] !== enabled || $[1] !== value) { + t1 = () => { + if (enabled) { + setLocalValue(value); + } else { + setLocalValue("disabled"); + } + }; + + t2 = [value, enabled]; + $[0] = enabled; + $[1] = value; + $[2] = t1; + $[3] = t2; + } else { + t1 = $[2]; + t2 = $[3]; + } + useEffect(t1, t2); + let t3; + if ($[4] !== localValue) { + t3 =
{localValue}
; + $[4] = localValue; + $[5] = t3; + } else { + t3 = $[5]; + } + return t3; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ value: "test", enabled: true }], +}; + +``` + +### Eval output +(kind: ok)
test
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useEffect/derived-state-with-conditional-no-error.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useEffect/derived-state-with-conditional-no-error.js new file mode 100644 index 0000000000000..79d83b89256ff --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useEffect/derived-state-with-conditional-no-error.js @@ -0,0 +1,21 @@ +// @validateNoDerivedComputationsInEffects +import {useEffect, useState} from 'react'; + +function Component({value, enabled}) { + const [localValue, setLocalValue] = useState(''); + + useEffect(() => { + if (enabled) { + setLocalValue(value); + } else { + setLocalValue('disabled'); + } + }, [value, enabled]); + + return
{localValue}
; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{value: 'test', enabled: true}], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useEffect/derived-state-with-side-effects-no-error.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useEffect/derived-state-with-side-effects-no-error.expect.md new file mode 100644 index 0000000000000..e0708dd1f7121 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useEffect/derived-state-with-side-effects-no-error.expect.md @@ -0,0 +1,74 @@ + +## Input + +```javascript +// @validateNoDerivedComputationsInEffects +import {useEffect, useState} from 'react'; + +function Component({value}) { + const [localValue, setLocalValue] = useState(''); + + useEffect(() => { + console.log('Value changed:', value); + setLocalValue(value); + document.title = `Value: ${value}`; + }, [value]); + + return
{localValue}
; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{value: 'test'}], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects +import { useEffect, useState } from "react"; + +function Component(t0) { + const $ = _c(5); + const { value } = t0; + const [localValue, setLocalValue] = useState(""); + let t1; + let t2; + if ($[0] !== value) { + t1 = () => { + console.log("Value changed:", value); + setLocalValue(value); + document.title = `Value: ${value}`; + }; + t2 = [value]; + $[0] = value; + $[1] = t1; + $[2] = t2; + } else { + t1 = $[1]; + t2 = $[2]; + } + useEffect(t1, t2); + let t3; + if ($[3] !== localValue) { + t3 =
{localValue}
; + $[3] = localValue; + $[4] = t3; + } else { + t3 = $[4]; + } + return t3; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ value: "test" }], +}; + +``` + +### Eval output +(kind: ok)
test
+logs: ['Value changed:','test'] \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useEffect/derived-state-with-side-effects-no-error.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useEffect/derived-state-with-side-effects-no-error.js new file mode 100644 index 0000000000000..b948dda6cb6aa --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useEffect/derived-state-with-side-effects-no-error.js @@ -0,0 +1,19 @@ +// @validateNoDerivedComputationsInEffects +import {useEffect, useState} from 'react'; + +function Component({value}) { + const [localValue, setLocalValue] = useState(''); + + useEffect(() => { + console.log('Value changed:', value); + setLocalValue(value); + document.title = `Value: ${value}`; + }, [value]); + + return
{localValue}
; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{value: 'test'}], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useEffect/error.bug-derived-state-from-mixed-deps.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useEffect/error.bug-derived-state-from-mixed-deps.expect.md new file mode 100644 index 0000000000000..8124f4b3f32d6 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useEffect/error.bug-derived-state-from-mixed-deps.expect.md @@ -0,0 +1,51 @@ + +## Input + +```javascript +// @validateNoDerivedComputationsInEffects +import {useEffect, useState} from 'react'; + +function Component({prefix}) { + const [name, setName] = useState(''); + const [displayName, setDisplayName] = useState(''); + + useEffect(() => { + setDisplayName(prefix + name); + }, [prefix, name]); + + return ( +
+ setName(e.target.value)} /> +
{displayName}
+
+ ); +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{prefix: 'Hello, '}], +}; + +``` + + +## Error + +``` +Found 1 error: + +Error: You may not need this effect. Values derived from state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state) + +This effect updates state based on other state values. Consider calculating this value directly during render. + +error.bug-derived-state-from-mixed-deps.ts:9:4 + 7 | + 8 | useEffect(() => { +> 9 | setDisplayName(prefix + name); + | ^^^^^^^^^^^^^^ You may not need this effect. Values derived from state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state) + 10 | }, [prefix, name]); + 11 | + 12 | return ( +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useEffect/error.bug-derived-state-from-mixed-deps.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useEffect/error.bug-derived-state-from-mixed-deps.js new file mode 100644 index 0000000000000..0004ab0ebfb47 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useEffect/error.bug-derived-state-from-mixed-deps.js @@ -0,0 +1,23 @@ +// @validateNoDerivedComputationsInEffects +import {useEffect, useState} from 'react'; + +function Component({prefix}) { + const [name, setName] = useState(''); + const [displayName, setDisplayName] = useState(''); + + useEffect(() => { + setDisplayName(prefix + name); + }, [prefix, name]); + + return ( +
+ setName(e.target.value)} /> +
{displayName}
+
+ ); +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{prefix: 'Hello, '}], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useEffect/error.invalid-derived-state-from-props-destructured.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useEffect/error.invalid-derived-state-from-props-destructured.expect.md new file mode 100644 index 0000000000000..26b8b7930b365 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useEffect/error.invalid-derived-state-from-props-destructured.expect.md @@ -0,0 +1,45 @@ + +## Input + +```javascript +// @validateNoDerivedComputationsInEffects +import {useEffect, useState} from 'react'; + +function Component({user: {firstName, lastName}}) { + const [fullName, setFullName] = useState(''); + + useEffect(() => { + setFullName(firstName + ' ' + lastName); + }, [firstName, lastName]); + + return
{fullName}
; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{user: {firstName: 'John', lastName: 'Doe'}}], +}; + +``` + + +## Error + +``` +Found 1 error: + +Error: You may not need this effect. Values derived from state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state) + +This effect updates state based on other state values. Consider calculating this value directly during render. + +error.invalid-derived-state-from-props-destructured.ts:8:4 + 6 | + 7 | useEffect(() => { +> 8 | setFullName(firstName + ' ' + lastName); + | ^^^^^^^^^^^ You may not need this effect. Values derived from state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state) + 9 | }, [firstName, lastName]); + 10 | + 11 | return
{fullName}
; +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useEffect/error.invalid-derived-state-from-props-destructured.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useEffect/error.invalid-derived-state-from-props-destructured.js new file mode 100644 index 0000000000000..966f09ea89da0 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useEffect/error.invalid-derived-state-from-props-destructured.js @@ -0,0 +1,17 @@ +// @validateNoDerivedComputationsInEffects +import {useEffect, useState} from 'react'; + +function Component({firstName, lastName}) { + const [fullName, setFullName] = useState(''); + + useEffect(() => { + setFullName(firstName + ' ' + lastName); + }, [firstName, lastName]); + + return
{fullName}
; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{firstName: 'John', lastName: 'Doe'}], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useEffect/error.invalid-derived-state-from-props-in-effect.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useEffect/error.invalid-derived-state-from-props-in-effect.expect.md new file mode 100644 index 0000000000000..1f7ff8dc5d754 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useEffect/error.invalid-derived-state-from-props-in-effect.expect.md @@ -0,0 +1,45 @@ + +## Input + +```javascript +// @validateNoDerivedComputationsInEffects +import {useEffect, useState} from 'react'; + +function Component({firstName, lastName}) { + const [fullName, setFullName] = useState(''); + + useEffect(() => { + setFullName(firstName + ' ' + lastName); + }, [firstName, lastName]); + + return
{fullName}
; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{firstName: 'John', lastName: 'Doe'}], +}; + +``` + + +## Error + +``` +Found 1 error: + +Error: You may not need this effect. Values derived from state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state) + +This effect updates state based on other state values. Consider calculating this value directly during render. + +error.invalid-derived-state-from-props-in-effect.ts:8:4 + 6 | + 7 | useEffect(() => { +> 8 | setFullName(firstName + ' ' + lastName); + | ^^^^^^^^^^^ You may not need this effect. Values derived from state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state) + 9 | }, [firstName, lastName]); + 10 | + 11 | return
{fullName}
; +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useEffect/error.invalid-derived-state-from-props-in-effect.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useEffect/error.invalid-derived-state-from-props-in-effect.js new file mode 100644 index 0000000000000..966f09ea89da0 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useEffect/error.invalid-derived-state-from-props-in-effect.js @@ -0,0 +1,17 @@ +// @validateNoDerivedComputationsInEffects +import {useEffect, useState} from 'react'; + +function Component({firstName, lastName}) { + const [fullName, setFullName] = useState(''); + + useEffect(() => { + setFullName(firstName + ' ' + lastName); + }, [firstName, lastName]); + + return
{fullName}
; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{firstName: 'John', lastName: 'Doe'}], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useEffect/error.invalid-derived-state-from-state-in-effect.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useEffect/error.invalid-derived-state-from-state-in-effect.expect.md new file mode 100644 index 0000000000000..c5548c970b825 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useEffect/error.invalid-derived-state-from-state-in-effect.expect.md @@ -0,0 +1,53 @@ + +## Input + +```javascript +// @validateNoDerivedComputationsInEffects +import {useEffect, useState} from 'react'; + +function Component() { + const [firstName, setFirstName] = useState('John'); + const [lastName, setLastName] = useState('Doe'); + const [fullName, setFullName] = useState(''); + + useEffect(() => { + setFullName(firstName + ' ' + lastName); + }, [firstName, lastName]); + + return ( +
+ setFirstName(e.target.value)} /> + setLastName(e.target.value)} /> +
{fullName}
+
+ ); +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [], +}; + +``` + + +## Error + +``` +Found 1 error: + +Error: You may not need this effect. Values derived from state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state) + +This effect updates state based on other state values. Consider calculating this value directly during render. + +error.invalid-derived-state-from-state-in-effect.ts:10:4 + 8 | + 9 | useEffect(() => { +> 10 | setFullName(firstName + ' ' + lastName); + | ^^^^^^^^^^^ You may not need this effect. Values derived from state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state) + 11 | }, [firstName, lastName]); + 12 | + 13 | return ( +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useEffect/error.invalid-derived-state-from-state-in-effect.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useEffect/error.invalid-derived-state-from-state-in-effect.js new file mode 100644 index 0000000000000..2b4f9f70669f5 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useEffect/error.invalid-derived-state-from-state-in-effect.js @@ -0,0 +1,25 @@ +// @validateNoDerivedComputationsInEffects +import {useEffect, useState} from 'react'; + +function Component() { + const [firstName, setFirstName] = useState('John'); + const [lastName, setLastName] = useState('Doe'); + const [fullName, setFullName] = useState(''); + + useEffect(() => { + setFullName(firstName + ' ' + lastName); + }, [firstName, lastName]); + + return ( +
+ setFirstName(e.target.value)} /> + setLastName(e.target.value)} /> +
{fullName}
+
+ ); +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useEffect/invalid-derived-state-from-props-computed.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useEffect/invalid-derived-state-from-props-computed.expect.md new file mode 100644 index 0000000000000..3d0c4fe9c8950 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useEffect/invalid-derived-state-from-props-computed.expect.md @@ -0,0 +1,72 @@ + +## Input + +```javascript +// @validateNoDerivedComputationsInEffects +import {useEffect, useState} from 'react'; + +function Component(props) { + const [displayValue, setDisplayValue] = useState(''); + + useEffect(() => { + const computed = props.prefix + props.value + props.suffix; + setDisplayValue(computed); + }, [props.prefix, props.value, props.suffix]); + + return
{displayValue}
; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{prefix: '[', value: 'test', suffix: ']'}], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; // @validateNoDerivedComputationsInEffects +import { useEffect, useState } from "react"; + +function Component(props) { + const $ = _c(7); + const [displayValue, setDisplayValue] = useState(""); + let t0; + let t1; + if ($[0] !== props.prefix || $[1] !== props.suffix || $[2] !== props.value) { + t0 = () => { + const computed = props.prefix + props.value + props.suffix; + setDisplayValue(computed); + }; + t1 = [props.prefix, props.value, props.suffix]; + $[0] = props.prefix; + $[1] = props.suffix; + $[2] = props.value; + $[3] = t0; + $[4] = t1; + } else { + t0 = $[3]; + t1 = $[4]; + } + useEffect(t0, t1); + let t2; + if ($[5] !== displayValue) { + t2 =
{displayValue}
; + $[5] = displayValue; + $[6] = t2; + } else { + t2 = $[6]; + } + return t2; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ prefix: "[", value: "test", suffix: "]" }], +}; + +``` + +### Eval output +(kind: ok)
[test]
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useEffect/invalid-derived-state-from-props-computed.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useEffect/invalid-derived-state-from-props-computed.js new file mode 100644 index 0000000000000..0e726f86ab1ae --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useEffect/invalid-derived-state-from-props-computed.js @@ -0,0 +1,18 @@ +// @validateNoDerivedComputationsInEffects +import {useEffect, useState} from 'react'; + +function Component(props) { + const [displayValue, setDisplayValue] = useState(''); + + useEffect(() => { + const computed = props.prefix + props.value + props.suffix; + setDisplayValue(computed); + }, [props.prefix, props.value, props.suffix]); + + return
{displayValue}
; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{prefix: '[', value: 'test', suffix: ']'}], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-arrow-implicit-return.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-arrow-implicit-return.expect.md new file mode 100644 index 0000000000000..df3dae1d83a0c --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-arrow-implicit-return.expect.md @@ -0,0 +1,40 @@ + +## Input + +```javascript +// @validateNoVoidUseMemo +function Component() { + const value = useMemo(() => computeValue(), []); + return
{value}
; +} + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; // @validateNoVoidUseMemo +function Component() { + const $ = _c(2); + let t0; + if ($[0] === Symbol.for("react.memo_cache_sentinel")) { + t0 = computeValue(); + $[0] = t0; + } else { + t0 = $[0]; + } + const value = t0; + let t1; + if ($[1] === Symbol.for("react.memo_cache_sentinel")) { + t1 =
{value}
; + $[1] = t1; + } else { + t1 = $[1]; + } + return t1; +} + +``` + +### 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/useMemo-arrow-implicit-return.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-arrow-implicit-return.js new file mode 100644 index 0000000000000..0ea121430d1da --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-arrow-implicit-return.js @@ -0,0 +1,5 @@ +// @validateNoVoidUseMemo +function Component() { + const value = useMemo(() => computeValue(), []); + return
{value}
; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-empty-return.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-empty-return.expect.md new file mode 100644 index 0000000000000..7be708ef5017c --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-empty-return.expect.md @@ -0,0 +1,34 @@ + +## Input + +```javascript +// @validateNoVoidUseMemo +function Component() { + const value = useMemo(() => { + return; + }, []); + return
{value}
; +} + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; // @validateNoVoidUseMemo +function Component() { + const $ = _c(1); + let t0; + if ($[0] === Symbol.for("react.memo_cache_sentinel")) { + t0 =
{undefined}
; + $[0] = t0; + } else { + t0 = $[0]; + } + return t0; +} + +``` + +### 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/useMemo-empty-return.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-empty-return.js new file mode 100644 index 0000000000000..7985884d5657c --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-empty-return.js @@ -0,0 +1,7 @@ +// @validateNoVoidUseMemo +function Component() { + const value = useMemo(() => { + return; + }, []); + return
{value}
; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-explicit-null-return.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-explicit-null-return.expect.md new file mode 100644 index 0000000000000..d35213b00800a --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-explicit-null-return.expect.md @@ -0,0 +1,34 @@ + +## Input + +```javascript +// @validateNoVoidUseMemo +function Component() { + const value = useMemo(() => { + return null; + }, []); + return
{value}
; +} + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; // @validateNoVoidUseMemo +function Component() { + const $ = _c(1); + let t0; + if ($[0] === Symbol.for("react.memo_cache_sentinel")) { + t0 =
{null}
; + $[0] = t0; + } else { + t0 = $[0]; + } + return t0; +} + +``` + +### 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/useMemo-explicit-null-return.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-explicit-null-return.js new file mode 100644 index 0000000000000..9b0a1a8253dcb --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-explicit-null-return.js @@ -0,0 +1,7 @@ +// @validateNoVoidUseMemo +function Component() { + const value = useMemo(() => { + return null; + }, []); + return
{value}
; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-independently-memoizeable.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-independently-memoizeable.expect.md index 8b3f03137ee43..479b1b2c8230a 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-independently-memoizeable.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-independently-memoizeable.expect.md @@ -21,45 +21,43 @@ import { c as _c } from "react/compiler-runtime"; function Component(props) { const $ = _c(10); let t0; - let t1; if ($[0] !== props.a) { - t1 = makeObject(props.a); + t0 = makeObject(props.a); $[0] = props.a; - $[1] = t1; + $[1] = t0; } else { - t1 = $[1]; + t0 = $[1]; } - const a = t1; - let t2; + const a = t0; + let t1; if ($[2] !== props.b) { - t2 = makeObject(props.b); + t1 = makeObject(props.b); $[2] = props.b; - $[3] = t2; + $[3] = t1; } else { - t2 = $[3]; + t1 = $[3]; } - const b = t2; - let t3; + const b = t1; + let t2; if ($[4] !== a || $[5] !== b) { - t3 = [a, b]; + t2 = [a, b]; $[4] = a; $[5] = b; - $[6] = t3; + $[6] = t2; } else { - t3 = $[6]; + t2 = $[6]; } - t0 = t3; - const [a_0, b_0] = t0; - let t4; + const [a_0, b_0] = t2; + let t3; if ($[7] !== a_0 || $[8] !== b_0) { - t4 = [a_0, b_0]; + t3 = [a_0, b_0]; $[7] = a_0; $[8] = b_0; - $[9] = t4; + $[9] = t3; } else { - t4 = $[9]; + t3 = $[9]; } - return t4; + return t3; } ``` diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-labeled-statement-unconditional-return.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-labeled-statement-unconditional-return.expect.md index 9e00c1ddb9fd8..dda4a25e9f0e1 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-labeled-statement-unconditional-return.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-labeled-statement-unconditional-return.expect.md @@ -23,10 +23,7 @@ export const FIXTURE_ENTRYPOINT = { ```javascript function Component(props) { - let t0; - - t0 = props.value; - const x = t0; + const x = props.value; return x; } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-logical.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-logical.expect.md index 672e8e45bc354..f944aa454d88d 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-logical.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-logical.expect.md @@ -19,9 +19,7 @@ export const FIXTURE_ENTRYPOINT = { ```javascript function Component(props) { - let t0; - t0 = props.a && props.b; - const x = t0; + const x = props.a && props.b; return x; } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-mabye-modified-free-variable-dont-preserve-memoization-guarantees.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-mabye-modified-free-variable-dont-preserve-memoization-guarantees.expect.md index 666fa49376b8f..8b4de101cacc3 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-mabye-modified-free-variable-dont-preserve-memoization-guarantees.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-mabye-modified-free-variable-dont-preserve-memoization-guarantees.expect.md @@ -51,13 +51,11 @@ function Component(props) { const part = free2.part; useHook(); - let t0; const x = makeObject_Primitives(); x.value = props.value; mutate(x, free, part); - t0 = x; - const object = t0; + const object = x; identity(free); identity(part); diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-mabye-modified-free-variable-preserve-memoization-guarantees.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-mabye-modified-free-variable-preserve-memoization-guarantees.expect.md index 05f33ccd3801b..a9207d39203dc 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-mabye-modified-free-variable-preserve-memoization-guarantees.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-mabye-modified-free-variable-preserve-memoization-guarantees.expect.md @@ -71,7 +71,6 @@ function Component(props) { const part = free2.part; useHook(); - let t2; let x; if ($[2] !== props.value) { x = makeObject_Primitives(); @@ -82,8 +81,7 @@ function Component(props) { } else { x = $[3]; } - t2 = x; - const object = t2; + const object = x; identity(free); identity(part); diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-maybe-modified-later-dont-preserve-memoization-guarantees.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-maybe-modified-later-dont-preserve-memoization-guarantees.expect.md index 7a3834fc6f68a..ac8c52187ed06 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-maybe-modified-later-dont-preserve-memoization-guarantees.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-maybe-modified-later-dont-preserve-memoization-guarantees.expect.md @@ -27,18 +27,14 @@ import { useMemo } from "react"; import { identity, makeObject_Primitives, mutate } from "shared-runtime"; function Component(props) { - const $ = _c(2); - let t0; + const $ = _c(1); let object; if ($[0] === Symbol.for("react.memo_cache_sentinel")) { - t0 = makeObject_Primitives(); - object = t0; + object = makeObject_Primitives(); identity(object); $[0] = object; - $[1] = t0; } else { object = $[0]; - t0 = $[1]; } return object; } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-maybe-modified-later-preserve-memoization-guarantees.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-maybe-modified-later-preserve-memoization-guarantees.expect.md index 7d8d77527e97c..7eddc14c7967b 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-maybe-modified-later-preserve-memoization-guarantees.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-maybe-modified-later-preserve-memoization-guarantees.expect.md @@ -29,14 +29,12 @@ import { identity, makeObject_Primitives, mutate } from "shared-runtime"; function Component(props) { const $ = _c(1); let t0; - let t1; if ($[0] === Symbol.for("react.memo_cache_sentinel")) { - t1 = makeObject_Primitives(); - $[0] = t1; + t0 = makeObject_Primitives(); + $[0] = t0; } else { - t1 = $[0]; + t0 = $[0]; } - t0 = t1; const object = t0; identity(object); return object; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-multiple-if-else.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-multiple-if-else.expect.md index 23b05b548221f..7f4b8af9650f6 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-multiple-if-else.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-multiple-if-else.expect.md @@ -33,17 +33,16 @@ import { c as _c } from "react/compiler-runtime"; import { useMemo } from "react"; function Component(props) { - const $ = _c(6); + const $ = _c(5); let t0; - bb0: { - let y; - if ( - $[0] !== props.a || - $[1] !== props.b || - $[2] !== props.cond || - $[3] !== props.cond2 - ) { - y = []; + if ( + $[0] !== props.a || + $[1] !== props.b || + $[2] !== props.cond || + $[3] !== props.cond2 + ) { + bb0: { + const y = []; if (props.cond) { y.push(props.a); } @@ -53,17 +52,15 @@ function Component(props) { } y.push(props.b); - $[0] = props.a; - $[1] = props.b; - $[2] = props.cond; - $[3] = props.cond2; - $[4] = y; - $[5] = t0; - } else { - y = $[4]; - t0 = $[5]; + t0 = y; } - t0 = y; + $[0] = props.a; + $[1] = props.b; + $[2] = props.cond; + $[3] = props.cond2; + $[4] = t0; + } else { + t0 = $[4]; } const x = t0; return x; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-multiple-returns.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-multiple-returns.expect.md new file mode 100644 index 0000000000000..0fa2700721b4c --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-multiple-returns.expect.md @@ -0,0 +1,51 @@ + +## Input + +```javascript +// @validateNoVoidUseMemo +function Component({items}) { + const value = useMemo(() => { + for (let item of items) { + if (item.match) return item; + } + return null; + }, [items]); + return
{value}
; +} + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; // @validateNoVoidUseMemo +function Component(t0) { + const $ = _c(2); + const { items } = t0; + let t1; + bb0: { + for (const item of items) { + if (item.match) { + t1 = item; + break bb0; + } + } + + t1 = null; + } + const value = t1; + let t2; + if ($[0] !== value) { + t2 =
{value}
; + $[0] = value; + $[1] = t2; + } else { + t2 = $[1]; + } + 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/useMemo-multiple-returns.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-multiple-returns.js new file mode 100644 index 0000000000000..dce32663f1cb4 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-multiple-returns.js @@ -0,0 +1,10 @@ +// @validateNoVoidUseMemo +function Component({items}) { + const value = useMemo(() => { + for (let item of items) { + if (item.match) return item; + } + return null; + }, [items]); + return
{value}
; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-nested-ifs.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-nested-ifs.expect.md index 12f51643dd4d4..f7b3605f4d5a0 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-nested-ifs.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-nested-ifs.expect.md @@ -24,12 +24,10 @@ export const FIXTURE_ENTRYPOINT = { ```javascript function Component(props) { - let t0; if (props.cond) { if (props.cond) { } } - t0 = undefined; } export const FIXTURE_ENTRYPOINT = { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-return-empty.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-return-empty.expect.md index b348ae34b6f56..be20ee39bd4ec 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-return-empty.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-return-empty.expect.md @@ -15,10 +15,7 @@ function component(a) { ```javascript function component(a) { - let t0; - mutate(a); - t0 = undefined; } ``` diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-simple.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-simple.expect.md index 712cb156d8dbb..301d9860f33ba 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-simple.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-simple.expect.md @@ -16,25 +16,23 @@ import { c as _c } from "react/compiler-runtime"; function component(a) { const $ = _c(4); let t0; - let t1; if ($[0] !== a) { - t1 = [a]; + t0 = [a]; $[0] = a; - $[1] = t1; + $[1] = t0; } else { - t1 = $[1]; + t0 = $[1]; } - t0 = t1; const x = t0; - let t2; + let t1; if ($[2] !== x) { - t2 = ; + t1 = ; $[2] = x; - $[3] = t2; + $[3] = t1; } else { - t2 = $[3]; + t1 = $[3]; } - return t2; + return t1; } ``` diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-with-optional.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-with-optional.expect.md new file mode 100644 index 0000000000000..260d695e09d8a --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-with-optional.expect.md @@ -0,0 +1,47 @@ + +## Input + +```javascript +import {useMemo} from 'react'; +function Component(props) { + return ( + useMemo(() => { + return [props.value]; + }) || [] + ); +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{value: 1}], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; +import { useMemo } from "react"; +function Component(props) { + const $ = _c(2); + let t0; + if ($[0] !== props.value) { + t0 = (() => [props.value])() || []; + $[0] = props.value; + $[1] = t0; + } else { + t0 = $[1]; + } + return t0; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ value: 1 }], +}; + +``` + +### Eval output +(kind: ok) [1] \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-with-optional.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-with-optional.js new file mode 100644 index 0000000000000..a96c044a3b86b --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-with-optional.js @@ -0,0 +1,13 @@ +import {useMemo} from 'react'; +function Component(props) { + return ( + useMemo(() => { + return [props.value]; + }) || [] + ); +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{value: 1}], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/valid-setState-in-useEffect-listener-transitive.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/valid-setState-in-useEffect-listener-transitive.expect.md index dd48adcda7158..ac55bd0469935 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/valid-setState-in-useEffect-listener-transitive.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/valid-setState-in-useEffect-listener-transitive.expect.md @@ -2,7 +2,7 @@ ## Input ```javascript -// @validateNoSetStateInPassiveEffects +// @validateNoSetStateInEffects import {useEffect, useState} from 'react'; function Component() { @@ -26,7 +26,7 @@ export const FIXTURE_ENTRYPOINT = { ## Code ```javascript -import { c as _c } from "react/compiler-runtime"; // @validateNoSetStateInPassiveEffects +import { c as _c } from "react/compiler-runtime"; // @validateNoSetStateInEffects import { useEffect, useState } from "react"; function Component() { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/valid-setState-in-useEffect-listener-transitive.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/valid-setState-in-useEffect-listener-transitive.js index 8b1e159071ef3..525f3e97d1911 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/valid-setState-in-useEffect-listener-transitive.js +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/valid-setState-in-useEffect-listener-transitive.js @@ -1,4 +1,4 @@ -// @validateNoSetStateInPassiveEffects +// @validateNoSetStateInEffects import {useEffect, useState} from 'react'; function Component() { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/valid-setState-in-useEffect-listener.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/valid-setState-in-useEffect-listener.expect.md index 7fdd01fd0a76e..a7deed9afb09e 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/valid-setState-in-useEffect-listener.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/valid-setState-in-useEffect-listener.expect.md @@ -2,7 +2,7 @@ ## Input ```javascript -// @validateNoSetStateInPassiveEffects +// @validateNoSetStateInEffects import {useEffect, useState} from 'react'; function Component() { @@ -23,7 +23,7 @@ export const FIXTURE_ENTRYPOINT = { ## Code ```javascript -import { c as _c } from "react/compiler-runtime"; // @validateNoSetStateInPassiveEffects +import { c as _c } from "react/compiler-runtime"; // @validateNoSetStateInEffects import { useEffect, useState } from "react"; function Component() { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/valid-setState-in-useEffect-listener.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/valid-setState-in-useEffect-listener.js index ba9720cba9b70..723e4841f6d3a 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/valid-setState-in-useEffect-listener.js +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/valid-setState-in-useEffect-listener.js @@ -1,4 +1,4 @@ -// @validateNoSetStateInPassiveEffects +// @validateNoSetStateInEffects import {useEffect, useState} from 'react'; function Component() { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/weakmap-constructor.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/weakmap-constructor.expect.md index aebaedf6a8754..a6def457a4194 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/weakmap-constructor.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/weakmap-constructor.expect.md @@ -2,6 +2,7 @@ ## Input ```javascript +import {useMemo} from 'react'; import {ValidateMemoization} from 'shared-runtime'; function Component({a, b, c}) { @@ -13,9 +14,21 @@ function Component({a, b, c}) { return ( <> - - - + + + ); } @@ -44,6 +57,7 @@ export const FIXTURE_ENTRYPOINT = { ```javascript import { c as _c } from "react/compiler-runtime"; +import { useMemo } from "react"; import { ValidateMemoization } from "shared-runtime"; function Component(t0) { @@ -76,7 +90,9 @@ function Component(t0) { } let t2; if ($[7] !== map || $[8] !== t1) { - t2 = ; + t2 = ( + + ); $[7] = map; $[8] = t1; $[9] = t2; @@ -94,7 +110,13 @@ function Component(t0) { } let t4; if ($[13] !== mapAlias || $[14] !== t3) { - t4 = ; + t4 = ( + + ); $[13] = mapAlias; $[14] = t3; $[15] = t4; @@ -119,7 +141,9 @@ function Component(t0) { } let t7; if ($[20] !== t5 || $[21] !== t6) { - t7 = ; + t7 = ( + + ); $[20] = t5; $[21] = t6; $[22] = t7; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/weakmap-constructor.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/weakmap-constructor.js index ecdec6c9e9621..d005c9f271d17 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/weakmap-constructor.js +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/weakmap-constructor.js @@ -1,3 +1,4 @@ +import {useMemo} from 'react'; import {ValidateMemoization} from 'shared-runtime'; function Component({a, b, c}) { @@ -9,9 +10,21 @@ function Component({a, b, c}) { return ( <> - - - + + + ); } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/weakset-constructor.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/weakset-constructor.expect.md index 5ebf32d533c91..94e0c7f05547b 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/weakset-constructor.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/weakset-constructor.expect.md @@ -2,6 +2,7 @@ ## Input ```javascript +import {useMemo} from 'react'; import {ValidateMemoization} from 'shared-runtime'; function Component({a, b, c}) { @@ -13,9 +14,21 @@ function Component({a, b, c}) { return ( <> - - - + + + ); } @@ -44,6 +57,7 @@ export const FIXTURE_ENTRYPOINT = { ```javascript import { c as _c } from "react/compiler-runtime"; +import { useMemo } from "react"; import { ValidateMemoization } from "shared-runtime"; function Component(t0) { @@ -76,7 +90,9 @@ function Component(t0) { } let t2; if ($[7] !== set || $[8] !== t1) { - t2 = ; + t2 = ( + + ); $[7] = set; $[8] = t1; $[9] = t2; @@ -94,7 +110,13 @@ function Component(t0) { } let t4; if ($[13] !== setAlias || $[14] !== t3) { - t4 = ; + t4 = ( + + ); $[13] = setAlias; $[14] = t3; $[15] = t4; @@ -119,7 +141,9 @@ function Component(t0) { } let t7; if ($[20] !== t5 || $[21] !== t6) { - t7 = ; + t7 = ( + + ); $[20] = t5; $[21] = t6; $[22] = t7; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/weakset-constructor.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/weakset-constructor.js index 8c0a7deb186f6..9114233812e55 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/weakset-constructor.js +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/weakset-constructor.js @@ -1,3 +1,4 @@ +import {useMemo} from 'react'; import {ValidateMemoization} from 'shared-runtime'; function Component({a, b, c}) { @@ -9,9 +10,21 @@ function Component({a, b, c}) { return ( <> - - - + + + ); } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/parseConfigPragma-test.ts b/compiler/packages/babel-plugin-react-compiler/src/__tests__/parseConfigPragma-test.ts index 903afe4c20b9f..0ee50a0e761f0 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/parseConfigPragma-test.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/parseConfigPragma-test.ts @@ -15,11 +15,11 @@ describe('parseConfigPragmaForTests()', () => { // Validate defaults first to make sure that the parser is getting the value from the pragma, // and not just missing it and getting the default value expect(defaultConfig.enableUseTypeAnnotations).toBe(false); - expect(defaultConfig.validateNoSetStateInPassiveEffects).toBe(false); + expect(defaultConfig.validateNoSetStateInEffects).toBe(false); expect(defaultConfig.validateNoSetStateInRender).toBe(true); const config = parseConfigPragmaForTests( - '@enableUseTypeAnnotations @validateNoSetStateInPassiveEffects:true @validateNoSetStateInRender:false', + '@enableUseTypeAnnotations @validateNoSetStateInEffects:true @validateNoSetStateInRender:false', {compilationMode: defaultOptions.compilationMode}, ); expect(config).toEqual({ @@ -28,7 +28,7 @@ describe('parseConfigPragmaForTests()', () => { environment: { ...defaultOptions.environment, enableUseTypeAnnotations: true, - validateNoSetStateInPassiveEffects: true, + validateNoSetStateInEffects: true, validateNoSetStateInRender: false, enableResetCacheOnSourceFileChanges: false, }, diff --git a/compiler/packages/babel-plugin-react-compiler/src/index.ts b/compiler/packages/babel-plugin-react-compiler/src/index.ts index 086e010fea581..2830d70d95c8d 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/index.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/index.ts @@ -9,9 +9,14 @@ export {runBabelPluginReactCompiler} from './Babel/RunReactCompilerBabelPlugin'; export { CompilerError, CompilerErrorDetail, + CompilerDiagnostic, CompilerSuggestionOperation, ErrorSeverity, + LintRules, type CompilerErrorDetailOptions, + type CompilerDiagnosticOptions, + type CompilerDiagnosticDetail, + type LintRule, } from './CompilerError'; export { compileFn as compile, @@ -20,7 +25,7 @@ export { OPT_OUT_DIRECTIVES, OPT_IN_DIRECTIVES, ProgramContext, - findDirectiveEnablingMemoization, + tryFindDirectiveEnablingMemoization as findDirectiveEnablingMemoization, findDirectiveDisablingMemoization, type CompilerPipelineValue, type Logger, @@ -30,6 +35,7 @@ export { export { Effect, ValueKind, + ValueReason, printHIR, printFunctionWithOutlined, validateEnvironmentConfig, diff --git a/compiler/packages/eslint-plugin-react-compiler/__tests__/ImpureFunctionCallsRule-test.ts b/compiler/packages/eslint-plugin-react-compiler/__tests__/ImpureFunctionCallsRule-test.ts new file mode 100644 index 0000000000000..a745397078aef --- /dev/null +++ b/compiler/packages/eslint-plugin-react-compiler/__tests__/ImpureFunctionCallsRule-test.ts @@ -0,0 +1,39 @@ +/** + * 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 { + ErrorCategory, + getRuleForCategory, +} from 'babel-plugin-react-compiler/src/CompilerError'; +import {normalizeIndent, testRule, makeTestCaseError} from './shared-utils'; +import {allRules} from '../src/rules/ReactCompilerRule'; + +testRule( + 'no impure function calls rule', + allRules[getRuleForCategory(ErrorCategory.Purity).name], + { + valid: [], + invalid: [ + { + name: 'Known impure function calls are caught', + code: normalizeIndent` + function Component() { + const date = Date.now(); + const now = performance.now(); + const rand = Math.random(); + return ; + } + `, + errors: [ + makeTestCaseError('Cannot call impure function during render'), + makeTestCaseError('Cannot call impure function during render'), + makeTestCaseError('Cannot call impure function during render'), + ], + }, + ], + }, +); diff --git a/compiler/packages/eslint-plugin-react-compiler/__tests__/InvalidHooksRule-test.ts b/compiler/packages/eslint-plugin-react-compiler/__tests__/InvalidHooksRule-test.ts new file mode 100644 index 0000000000000..0ba165011c6b0 --- /dev/null +++ b/compiler/packages/eslint-plugin-react-compiler/__tests__/InvalidHooksRule-test.ts @@ -0,0 +1,100 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { + ErrorCategory, + getRuleForCategory, +} from 'babel-plugin-react-compiler/src/CompilerError'; +import {normalizeIndent, makeTestCaseError, testRule} from './shared-utils'; +import {allRules} from '../src/rules/ReactCompilerRule'; + +testRule( + 'rules-of-hooks', + allRules[getRuleForCategory(ErrorCategory.Hooks).name], + { + valid: [ + { + name: 'Basic example', + code: normalizeIndent` + function Component() { + useHook(); + return
Hello world
; + } + `, + }, + { + name: 'Violation with Flow suppression', + code: ` + // Valid since error already suppressed with flow. + function useHook() { + if (cond) { + // $FlowFixMe[react-rule-hook] + useConditionalHook(); + } + } + `, + }, + { + // OK because invariants are only meant for the compiler team's consumption + name: '[Invariant] Defined after use', + code: normalizeIndent` + function Component(props) { + let y = function () { + m(x); + }; + + let x = { a }; + m(x); + return y; + } + `, + }, + { + name: "Classes don't throw", + code: normalizeIndent` + class Foo { + #bar() {} + } + `, + }, + ], + invalid: [ + { + name: 'Simple violation', + code: normalizeIndent` + function useConditional() { + if (cond) { + useConditionalHook(); + } + } + `, + errors: [ + makeTestCaseError( + 'Hooks must always be called in a consistent order', + ), + ], + }, + { + name: 'Multiple diagnostics within the same function are surfaced', + code: normalizeIndent` + function useConditional() { + cond ?? useConditionalHook(); + props.cond && useConditionalHook(); + return
Hello world
; + }`, + errors: [ + makeTestCaseError( + 'Hooks must always be called in a consistent order', + ), + makeTestCaseError( + 'Hooks must always be called in a consistent order', + ), + ], + }, + ], + }, +); diff --git a/compiler/packages/eslint-plugin-react-compiler/__tests__/NoAmbiguousJsxRule-test.ts b/compiler/packages/eslint-plugin-react-compiler/__tests__/NoAmbiguousJsxRule-test.ts new file mode 100644 index 0000000000000..9a09f4c4e0537 --- /dev/null +++ b/compiler/packages/eslint-plugin-react-compiler/__tests__/NoAmbiguousJsxRule-test.ts @@ -0,0 +1,38 @@ +/** + * 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 { + ErrorCategory, + getRuleForCategory, +} from 'babel-plugin-react-compiler/src/CompilerError'; +import {normalizeIndent, testRule, makeTestCaseError} from './shared-utils'; +import {allRules} from '../src/rules/ReactCompilerRule'; + +testRule( + 'no ambiguous JSX rule', + allRules[getRuleForCategory(ErrorCategory.ErrorBoundaries).name], + { + valid: [], + invalid: [ + { + name: 'JSX in try blocks are warned against', + code: normalizeIndent` + function Component(props) { + let el; + try { + el = ; + } catch { + return null; + } + return el; + } + `, + errors: [makeTestCaseError('Avoid constructing JSX within try/catch')], + }, + ], + }, +); diff --git a/compiler/packages/eslint-plugin-react-compiler/__tests__/NoCapitalizedCallsRule-test.ts b/compiler/packages/eslint-plugin-react-compiler/__tests__/NoCapitalizedCallsRule-test.ts new file mode 100644 index 0000000000000..c361670e97416 --- /dev/null +++ b/compiler/packages/eslint-plugin-react-compiler/__tests__/NoCapitalizedCallsRule-test.ts @@ -0,0 +1,71 @@ +/** + * 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 { + ErrorCategory, + getRuleForCategory, +} from 'babel-plugin-react-compiler/src/CompilerError'; +import {normalizeIndent, makeTestCaseError, testRule} from './shared-utils'; +import {allRules} from '../src/rules/ReactCompilerRule'; + +testRule( + 'no-capitalized-calls', + allRules[getRuleForCategory(ErrorCategory.CapitalizedCalls).name], + { + valid: [], + invalid: [ + { + name: 'Simple violation', + code: normalizeIndent` + import Child from './Child'; + function Component() { + return <> + {Child()} + ; + } + `, + errors: [ + makeTestCaseError( + 'Capitalized functions are reserved for components', + ), + ], + }, + { + name: 'Method call violation', + code: normalizeIndent` + import myModule from './MyModule'; + function Component() { + return <> + {myModule.Child()} + ; + } + `, + errors: [ + makeTestCaseError( + 'Capitalized functions are reserved for components', + ), + ], + }, + { + name: 'Multiple diagnostics within the same function are surfaced', + code: normalizeIndent` + import Child1 from './Child1'; + import MyModule from './MyModule'; + function Component() { + return <> + {Child1()} + {MyModule.Child2()} + ; + }`, + errors: [ + makeTestCaseError( + 'Capitalized functions are reserved for components', + ), + ], + }, + ], + }, +); diff --git a/compiler/packages/eslint-plugin-react-compiler/__tests__/NoRefAccessInRender-tests.ts b/compiler/packages/eslint-plugin-react-compiler/__tests__/NoRefAccessInRender-tests.ts new file mode 100644 index 0000000000000..9042980a807b9 --- /dev/null +++ b/compiler/packages/eslint-plugin-react-compiler/__tests__/NoRefAccessInRender-tests.ts @@ -0,0 +1,34 @@ +/** + * 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 { + ErrorCategory, + getRuleForCategory, +} from 'babel-plugin-react-compiler/src/CompilerError'; +import {normalizeIndent, testRule, makeTestCaseError} from './shared-utils'; +import {allRules} from '../src/rules/ReactCompilerRule'; + +testRule( + 'no ref access in render rule', + allRules[getRuleForCategory(ErrorCategory.Refs).name], + { + valid: [], + invalid: [ + { + name: 'validate against simple ref access in render', + code: normalizeIndent` + function Component(props) { + const ref = useRef(null); + const value = ref.current; + return value; + } + `, + errors: [makeTestCaseError('Cannot access refs during render')], + }, + ], + }, +); diff --git a/compiler/packages/eslint-plugin-react-compiler/__tests__/NoUnusedDirectivesRule-test.ts b/compiler/packages/eslint-plugin-react-compiler/__tests__/NoUnusedDirectivesRule-test.ts new file mode 100644 index 0000000000000..77f6dd93fbf87 --- /dev/null +++ b/compiler/packages/eslint-plugin-react-compiler/__tests__/NoUnusedDirectivesRule-test.ts @@ -0,0 +1,58 @@ +/** + * 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 {NoUnusedDirectivesRule} from '../src/rules/ReactCompilerRule'; +import {normalizeIndent, testRule} from './shared-utils'; + +testRule('no unused directives rule', NoUnusedDirectivesRule, { + valid: [], + invalid: [ + { + name: "Unused 'use no forget' directive is reported when no errors are present on components", + code: normalizeIndent` + function Component() { + 'use no forget'; + return
Hello world
+ } + `, + errors: [ + { + message: "Unused 'use no forget' directive", + suggestions: [ + { + output: + // yuck + '\nfunction Component() {\n \n return
Hello world
\n}\n', + }, + ], + }, + ], + }, + + { + name: "Unused 'use no forget' directive is reported when no errors are present on non-components or hooks", + code: normalizeIndent` + function notacomponent() { + 'use no forget'; + return 1 + 1; + } + `, + errors: [ + { + message: "Unused 'use no forget' directive", + suggestions: [ + { + output: + // yuck + '\nfunction notacomponent() {\n \n return 1 + 1;\n}\n', + }, + ], + }, + ], + }, + ], +}); diff --git a/compiler/packages/eslint-plugin-react-compiler/__tests__/PluginTest-test.ts b/compiler/packages/eslint-plugin-react-compiler/__tests__/PluginTest-test.ts new file mode 100644 index 0000000000000..6efd069aafa07 --- /dev/null +++ b/compiler/packages/eslint-plugin-react-compiler/__tests__/PluginTest-test.ts @@ -0,0 +1,158 @@ +/** + * 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 { + ErrorCategory, + getRuleForCategory, +} from 'babel-plugin-react-compiler/src/CompilerError'; +import { + normalizeIndent, + testRule, + makeTestCaseError, + TestRecommendedRules, +} from './shared-utils'; +import {allRules} from '../src/rules/ReactCompilerRule'; + +testRule('plugin-recommended', TestRecommendedRules, { + valid: [ + { + name: 'Basic example with component syntax', + code: normalizeIndent` + export default component HelloWorld( + text: string = 'Hello!', + onClick: () => void, + ) { + return
{text}
; + } + `, + }, + + { + // OK because invariants are only meant for the compiler team's consumption + name: '[Invariant] Defined after use', + code: normalizeIndent` + function Component(props) { + let y = function () { + m(x); + }; + + let x = { a }; + m(x); + return y; + } + `, + }, + { + name: "Classes don't throw", + code: normalizeIndent` + class Foo { + #bar() {} + } + `, + }, + ], + invalid: [ + { + // TODO: actually return multiple diagnostics in this case + name: 'Multiple diagnostic kinds from the same function are surfaced', + code: normalizeIndent` + import Child from './Child'; + function Component() { + const result = cond ?? useConditionalHook(); + return <> + {Child(result)} + ; + } + `, + errors: [ + makeTestCaseError('Hooks must always be called in a consistent order'), + ], + }, + { + name: 'Multiple diagnostics within the same file are surfaced', + code: normalizeIndent` + function useConditional1() { + 'use memo'; + return cond ?? useConditionalHook(); + } + function useConditional2(props) { + 'use memo'; + return props.cond && useConditionalHook(); + }`, + errors: [ + makeTestCaseError('Hooks must always be called in a consistent order'), + makeTestCaseError('Hooks must always be called in a consistent order'), + ], + }, + { + name: "'use no forget' does not disable eslint rule", + code: normalizeIndent` + let count = 0; + function Component() { + 'use no forget'; + return cond ?? useConditionalHook(); + + } + `, + errors: [ + makeTestCaseError('Hooks must always be called in a consistent order'), + ], + }, + { + name: 'Multiple non-fatal useMemo diagnostics are surfaced', + code: normalizeIndent` + import {useMemo, useState} from 'react'; + + function Component({item, cond}) { + const [prevItem, setPrevItem] = useState(item); + const [state, setState] = useState(0); + + useMemo(() => { + if (cond) { + setPrevItem(item); + setState(0); + } + }, [cond, item, init]); + + return ; + }`, + errors: [makeTestCaseError('useMemo() callbacks must return a value')], + }, + { + name: 'Pipeline errors are reported', + code: normalizeIndent` + import useMyEffect from 'useMyEffect'; + import {AUTODEPS} from 'react'; + function Component({a}) { + 'use no memo'; + useMyEffect(() => console.log(a.b), AUTODEPS); + return
Hello world
; + } + `, + options: [ + { + environment: { + inferEffectDependencies: [ + { + function: { + source: 'useMyEffect', + importSpecifierName: 'default', + }, + autodepsIndex: 1, + }, + ], + }, + }, + ], + errors: [ + { + message: /Cannot infer dependencies of this effect/, + }, + ], + }, + ], +}); diff --git a/compiler/packages/eslint-plugin-react-compiler/__tests__/ReactCompilerRule-test.ts b/compiler/packages/eslint-plugin-react-compiler/__tests__/ReactCompilerRule-test.ts deleted file mode 100644 index 8f1612f20ea7a..0000000000000 --- a/compiler/packages/eslint-plugin-react-compiler/__tests__/ReactCompilerRule-test.ts +++ /dev/null @@ -1,292 +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 {ErrorSeverity} from 'babel-plugin-react-compiler/src'; -import {RuleTester as ESLintTester} from 'eslint'; -import ReactCompilerRule from '../src/rules/ReactCompilerRule'; - -/** - * A string template tag that removes padding from the left side of multi-line strings - * @param {Array} strings array of code strings (only one expected) - */ -function normalizeIndent(strings: TemplateStringsArray): string { - const codeLines = strings[0].split('\n'); - const leftPadding = codeLines[1].match(/\s+/)![0]; - return codeLines.map(line => line.slice(leftPadding.length)).join('\n'); -} - -type CompilerTestCases = { - valid: ESLintTester.ValidTestCase[]; - invalid: ESLintTester.InvalidTestCase[]; -}; - -const tests: CompilerTestCases = { - valid: [ - { - name: 'Basic example', - code: normalizeIndent` - function foo(x, y) { - if (x) { - return foo(false, y); - } - return [y * 10]; - } - `, - }, - { - name: 'Violation with Flow suppression', - code: ` - // Valid since error already suppressed with flow. - function useHookWithHook() { - if (cond) { - // $FlowFixMe[react-rule-hook] - useConditionalHook(); - } - } - `, - }, - { - name: 'Basic example with component syntax', - code: normalizeIndent` - export default component HelloWorld( - text: string = 'Hello!', - onClick: () => void, - ) { - return
{text}
; - } - `, - }, - { - name: 'Unsupported syntax', - code: normalizeIndent` - function foo(x) { - var y = 1; - return y * x; - } - `, - }, - { - // OK because invariants are only meant for the compiler team's consumption - name: '[Invariant] Defined after use', - code: normalizeIndent` - function Component(props) { - let y = function () { - m(x); - }; - - let x = { a }; - m(x); - return y; - } - `, - }, - { - name: "Classes don't throw", - code: normalizeIndent` - class Foo { - #bar() {} - } - `, - }, - ], - invalid: [ - { - name: 'Reportable levels can be configured', - options: [{reportableLevels: new Set([ErrorSeverity.Todo])}], - code: normalizeIndent` - function Foo(x) { - var y = 1; - return
{y * x}
; - }`, - errors: [ - { - message: - '(BuildHIR::lowerStatement) Handle var kinds in VariableDeclaration', - }, - ], - }, - { - name: '[InvalidReact] ESlint suppression', - // Indentation is intentionally weird so it doesn't add extra whitespace - code: normalizeIndent` - function Component(props) { - // eslint-disable-next-line react-hooks/rules-of-hooks - return
{props.foo}
; - }`, - errors: [ - { - message: - 'React Compiler has skipped optimizing this component because one or more React ESLint rules were disabled. React Compiler only works when your components follow all the rules of React, disabling them may result in unexpected or incorrect behavior', - suggestions: [ - { - output: normalizeIndent` - function Component(props) { - - return
{props.foo}
; - }`, - }, - ], - }, - { - message: - "Definition for rule 'react-hooks/rules-of-hooks' was not found.", - }, - ], - }, - { - 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)', - }, - ], - }, - { - name: "'use no forget' does not disable eslint rule", - code: normalizeIndent` - let count = 0; - function Component() { - 'use no forget'; - count = count + 1; - return
Hello world {count}
- } - `, - errors: [ - { - message: - 'Unexpected reassignment of a variable which was defined outside of the component. Components and hooks should be pure and side-effect free, but variable reassignment is a form of side-effect. If this variable is used in rendering, use useState instead. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#side-effects-must-run-outside-of-render)', - }, - ], - }, - { - name: "Unused 'use no forget' directive is reported when no errors are present on components", - code: normalizeIndent` - function Component() { - 'use no forget'; - return
Hello world
- } - `, - errors: [ - { - message: "Unused 'use no forget' directive", - suggestions: [ - { - output: - // yuck - '\nfunction Component() {\n \n return
Hello world
\n}\n', - }, - ], - }, - ], - }, - { - name: "Unused 'use no forget' directive is reported when no errors are present on non-components or hooks", - code: normalizeIndent` - function notacomponent() { - 'use no forget'; - return 1 + 1; - } - `, - errors: [ - { - message: "Unused 'use no forget' directive", - suggestions: [ - { - output: - // yuck - '\nfunction notacomponent() {\n \n return 1 + 1;\n}\n', - }, - ], - }, - ], - }, - { - name: 'Pipeline errors are reported', - code: normalizeIndent` - import useMyEffect from 'useMyEffect'; - function Component({a}) { - 'use no memo'; - useMyEffect(() => console.log(a.b)); - return
Hello world
; - } - `, - options: [ - { - environment: { - inferEffectDependencies: [ - { - function: { - source: 'useMyEffect', - importSpecifierName: 'default', - }, - numRequiredArgs: 1, - }, - ], - }, - }, - ], - errors: [ - { - message: - '[InferEffectDependencies] React Compiler is unable to infer dependencies of this effect. This will break your build! To resolve, either pass your own dependency array or fix reported compiler bailout diagnostics.', - }, - ], - }, - ], -}; - -const eslintTester = new ESLintTester({ - parser: require.resolve('hermes-eslint'), - parserOptions: { - ecmaVersion: 2015, - sourceType: 'module', - enableExperimentalComponentSyntax: true, - }, -}); -eslintTester.run('react-compiler', ReactCompilerRule, tests); diff --git a/compiler/packages/eslint-plugin-react-compiler/__tests__/ReactCompilerRuleTypescript-test.ts b/compiler/packages/eslint-plugin-react-compiler/__tests__/ReactCompilerRuleTypescript-test.ts index f67ff673cbbbb..87baf724e121d 100644 --- a/compiler/packages/eslint-plugin-react-compiler/__tests__/ReactCompilerRuleTypescript-test.ts +++ b/compiler/packages/eslint-plugin-react-compiler/__tests__/ReactCompilerRuleTypescript-test.ts @@ -6,22 +6,11 @@ */ import {RuleTester} from 'eslint'; -import ReactCompilerRule from '../src/rules/ReactCompilerRule'; - -/** - * A string template tag that removes padding from the left side of multi-line strings - * @param {Array} strings array of code strings (only one expected) - */ -function normalizeIndent(strings: TemplateStringsArray): string { - const codeLines = strings[0].split('\n'); - const leftPadding = codeLines[1].match(/\s+/)[0]; - return codeLines.map(line => line.slice(leftPadding.length)).join('\n'); -} - -type CompilerTestCases = { - valid: RuleTester.ValidTestCase[]; - invalid: RuleTester.InvalidTestCase[]; -}; +import { + CompilerTestCases, + normalizeIndent, + TestRecommendedRules, +} from './shared-utils'; const tests: CompilerTestCases = { valid: [ @@ -61,8 +50,7 @@ const tests: CompilerTestCases = { `, errors: [ { - message: - "Mutating a value returned from 'useState()', which should not be mutated. Use the setter function to update instead", + message: /Modifying a value returned from 'useState\(\)'/, line: 7, }, ], @@ -71,6 +59,7 @@ const tests: CompilerTestCases = { }; const eslintTester = new RuleTester({ + // @ts-ignore[2353] - outdated types parser: require.resolve('@typescript-eslint/parser'), }); -eslintTester.run('react-compiler', ReactCompilerRule, tests); +eslintTester.run('react-compiler', TestRecommendedRules, tests); diff --git a/compiler/packages/eslint-plugin-react-compiler/__tests__/shared-utils.ts b/compiler/packages/eslint-plugin-react-compiler/__tests__/shared-utils.ts new file mode 100644 index 0000000000000..2ab24581d8d07 --- /dev/null +++ b/compiler/packages/eslint-plugin-react-compiler/__tests__/shared-utils.ts @@ -0,0 +1,76 @@ +import {RuleTester as ESLintTester, Rule} from 'eslint'; +import {type ErrorCategory} from 'babel-plugin-react-compiler/src/CompilerError'; +import escape from 'regexp.escape'; +import {configs} from '../src/index'; +import {allRules} from '../src/rules/ReactCompilerRule'; + +/** + * A string template tag that removes padding from the left side of multi-line strings + * @param {Array} strings array of code strings (only one expected) + */ +export function normalizeIndent(strings: TemplateStringsArray): string { + const codeLines = strings[0].split('\n'); + const leftPadding = codeLines[1].match(/\s+/)![0]; + return codeLines.map(line => line.slice(leftPadding.length)).join('\n'); +} + +export type CompilerTestCases = { + valid: ESLintTester.ValidTestCase[]; + invalid: ESLintTester.InvalidTestCase[]; +}; + +export function makeTestCaseError(reason: string): ESLintTester.TestCaseError { + return { + message: new RegExp(escape(reason)), + }; +} + +export function testRule( + name: string, + rule: Rule.RuleModule, + tests: { + valid: ESLintTester.ValidTestCase[]; + invalid: ESLintTester.InvalidTestCase[]; + }, +): void { + const eslintTester = new ESLintTester({ + // @ts-ignore[2353] - outdated types + parser: require.resolve('hermes-eslint'), + parserOptions: { + ecmaVersion: 2015, + sourceType: 'module', + enableExperimentalComponentSyntax: true, + }, + }); + + eslintTester.run(name, rule, tests); +} + +/** + * Aggregates all recommended rules from the plugin. + */ +export const TestRecommendedRules: Rule.RuleModule = { + meta: { + type: 'problem', + docs: { + description: 'Disallow capitalized function calls', + category: 'Possible Errors', + recommended: true, + }, + // validation is done at runtime with zod + schema: [{type: 'object', additionalProperties: true}], + }, + create(context) { + for (const rule of Object.values( + configs.recommended.plugins['react-compiler'].rules, + )) { + const listener = rule.create(context); + if (Object.entries(listener).length !== 0) { + throw new Error('TODO: handle rules that return listeners to eslint'); + } + } + return {}; + }, +}; + +test('no test', () => {}); diff --git a/compiler/packages/eslint-plugin-react-compiler/package.json b/compiler/packages/eslint-plugin-react-compiler/package.json index 2dd191f033dcf..e5402611e2bbb 100644 --- a/compiler/packages/eslint-plugin-react-compiler/package.json +++ b/compiler/packages/eslint-plugin-react-compiler/package.json @@ -24,11 +24,13 @@ "@babel/preset-typescript": "^7.18.6", "@babel/types": "^7.26.0", "@types/eslint": "^8.56.12", + "@types/jest": "^30.0.0", "@types/node": "^20.2.5", "babel-jest": "^29.0.3", "eslint": "8.57.0", "hermes-eslint": "^0.25.1", - "jest": "^29.5.0" + "jest": "^29.5.0", + "regexp.escape": "^2.0.1" }, "engines": { "node": "^14.17.0 || ^16.0.0 || >= 18.0.0" diff --git a/compiler/packages/eslint-plugin-react-compiler/src/index.ts b/compiler/packages/eslint-plugin-react-compiler/src/index.ts index a3577a101ef43..1a339e8331009 100644 --- a/compiler/packages/eslint-plugin-react-compiler/src/index.ts +++ b/compiler/packages/eslint-plugin-react-compiler/src/index.ts @@ -5,29 +5,26 @@ * LICENSE file in the root directory of this source tree. */ -import ReactCompilerRule from './rules/ReactCompilerRule'; +import {allRules, recommendedRules} from './rules/ReactCompilerRule'; const meta = { name: 'eslint-plugin-react-compiler', }; -const rules = { - 'react-compiler': ReactCompilerRule, -}; - const configs = { recommended: { plugins: { 'react-compiler': { - rules: { - 'react-compiler': ReactCompilerRule, - }, + rules: allRules, }, }, - rules: { - 'react-compiler/react-compiler': 'error' as const, - }, + rules: Object.fromEntries( + Object.keys(recommendedRules).map(ruleName => [ + 'react-compiler/' + ruleName, + 'error', + ]), + ) as Record, }, }; -export {configs, rules, meta}; +export {configs, allRules as rules, meta}; 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 e9eee26bdabc6..d2e8fdd42e0c3 100644 --- a/compiler/packages/eslint-plugin-react-compiler/src/rules/ReactCompilerRule.ts +++ b/compiler/packages/eslint-plugin-react-compiler/src/rules/ReactCompilerRule.ts @@ -5,52 +5,27 @@ * LICENSE file in the root directory of this source tree. */ -import {transformFromAstSync} from '@babel/core'; -// @ts-expect-error: no types available -import PluginProposalPrivateMethods from '@babel/plugin-proposal-private-methods'; import type {SourceLocation as BabelSourceLocation} from '@babel/types'; -import BabelPluginReactCompiler, { +import { + CompilerDiagnosticOptions, CompilerErrorDetailOptions, CompilerSuggestionOperation, - ErrorSeverity, - parsePluginOptions, - validateEnvironmentConfig, - OPT_OUT_DIRECTIVES, - type PluginOptions, } from 'babel-plugin-react-compiler/src'; -import {Logger} from 'babel-plugin-react-compiler/src/Entrypoint'; import type {Rule} from 'eslint'; -import {Statement} from 'estree'; -import * as HermesParser from 'hermes-parser'; - -type CompilerErrorDetailWithLoc = Omit & { - loc: BabelSourceLocation; -}; +import runReactCompiler, {RunCacheEntry} from '../shared/RunReactCompiler'; +import { + LintRules, + type LintRule, +} from 'babel-plugin-react-compiler/src/CompilerError'; function assertExhaustive(_: never, errorMsg: string): never { throw new Error(errorMsg); } -const DEFAULT_REPORTABLE_LEVELS = new Set([ - ErrorSeverity.InvalidReact, - ErrorSeverity.InvalidJS, -]); -let reportableLevels = DEFAULT_REPORTABLE_LEVELS; - -function isReportableDiagnostic( - detail: CompilerErrorDetailOptions, -): detail is CompilerErrorDetailWithLoc { - return ( - reportableLevels.has(detail.severity) && - detail.loc != null && - typeof detail.loc !== 'symbol' - ); -} - function makeSuggestions( - detail: CompilerErrorDetailOptions, + detail: CompilerErrorDetailOptions | CompilerDiagnosticOptions, ): Array { - let suggest: Array = []; + const suggest: Array = []; if (Array.isArray(detail.suggestions)) { for (const suggestion of detail.suggestions) { switch (suggestion.op) { @@ -100,252 +75,143 @@ function makeSuggestions( return suggest; } -const COMPILER_OPTIONS: Partial = { - noEmit: true, - panicThreshold: 'none', - // Don't emit errors on Flow suppressions--Flow already gave a signal - flowSuppressions: false, - environment: validateEnvironmentConfig({ - validateRefAccessDuringRender: false, - }), -}; +function getReactCompilerResult(context: Rule.RuleContext): RunCacheEntry { + // Compat with older versions of eslint + const sourceCode = context.sourceCode ?? context.getSourceCode(); + const filename = context.filename ?? context.getFilename(); + const userOpts = context.options[0] ?? {}; -const rule: Rule.RuleModule = { - meta: { - type: 'problem', - docs: { - description: 'Surfaces diagnostics from React Forget', - recommended: true, - }, - fixable: 'code', - hasSuggestions: true, - // validation is done at runtime with zod - schema: [{type: 'object', additionalProperties: true}], - }, - create(context: Rule.RuleContext) { - // Compat with older versions of eslint - const sourceCode = context.sourceCode ?? context.getSourceCode(); - const filename = context.filename ?? context.getFilename(); - const userOpts = context.options[0] ?? {}; - if ( - userOpts['reportableLevels'] != null && - userOpts['reportableLevels'] instanceof Set - ) { - reportableLevels = userOpts['reportableLevels']; - } 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; + const results = runReactCompiler({ + sourceCode, + filename, + userOpts, + }); + + return results; +} + +function hasFlowSuppression( + program: RunCacheEntry, + nodeLoc: BabelSourceLocation, + suppressions: Array, +): boolean { + for (const commentNode of program.flowSuppressions) { if ( - userOpts['__unstable_donotuse_reportAllBailouts'] != null && - typeof userOpts['__unstable_donotuse_reportAllBailouts'] === 'boolean' + suppressions.includes(commentNode.code) && + commentNode.line === nodeLoc.start.line - 1 ) { - __unstable_donotuse_reportAllBailouts = - userOpts['__unstable_donotuse_reportAllBailouts']; + return true; } + } + return false; +} - let shouldReportUnusedOptOutDirective = true; - const options: PluginOptions = parsePluginOptions({ - ...COMPILER_OPTIONS, - ...userOpts, - environment: { - ...COMPILER_OPTIONS.environment, - ...userOpts.environment, - }, - }); - const userLogger: Logger | null = options.logger; - options.logger = { - logEvent: (filename, event): void => { - userLogger?.logEvent(filename, event); - if (event.kind === 'CompileError') { - shouldReportUnusedOptOutDirective = false; - 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})` - : ''; - /** - * Report bailouts with a smaller span (just the first line). - * Compiler bailout lints only serve to flag that a react function - * has not been optimized by the compiler for codebases which depend - * on compiler memo heavily for perf. These lints are also often not - * actionable. - */ - let endLoc; - if (event.fnLoc.end.line === event.fnLoc.start.line) { - endLoc = event.fnLoc.end; - } else { - endLoc = { - line: event.fnLoc.start.line, - // Babel loc line numbers are 1-indexed - column: sourceCode.text.split( - /\r?\n|\r|\n/g, - event.fnLoc.start.line, - )[event.fnLoc.start.line - 1].length, - }; - } - const firstLineLoc = { - start: event.fnLoc.start, - end: endLoc, - }; - context.report({ - message: `[ReactCompilerBailout] ${detail.reason}${locStr}`, - loc: firstLineLoc, - suggest, - }); - } - - if (!isReportableDiagnostic(detail)) { - return; +function makeRule(rule: LintRule): Rule.RuleModule { + const create = (context: Rule.RuleContext): Rule.RuleListener => { + const result = getReactCompilerResult(context); + + for (const event of result.events) { + if (event.kind === 'CompileError') { + const detail = event.detail; + if (detail.category === rule.category) { + const loc = detail.primaryLocation(); + if (loc == null || typeof loc === 'symbol') { + continue; } if ( - hasFlowSuppression(detail.loc, 'react-rule-hook') || - hasFlowSuppression(detail.loc, 'react-rule-unsafe-ref') + hasFlowSuppression(result, loc, [ + 'react-rule-hook', + 'react-rule-unsafe-ref', + ]) ) { // 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, - }); + continue; } + /* + * TODO: if multiple rules report the same linter category, + * we should deduplicate them with a "reported" set + */ + context.report({ + message: detail.printErrorMessage(result.sourceCode, { + eslint: true, + }), + loc, + suggest: makeSuggestions(detail.options), + }); } - }, - }; - - try { - options.environment = validateEnvironmentConfig( - options.environment ?? {}, - ); - } catch (err) { - options.logger?.logEvent('', err); - } - - function hasFlowSuppression( - nodeLoc: BabelSourceLocation, - suppression: string, - ): boolean { - const comments = sourceCode.getAllComments(); - const flowSuppressionRegex = new RegExp( - '\\$FlowFixMe\\[' + suppression + '\\]', - ); - for (const commentNode of comments) { - if ( - flowSuppressionRegex.test(commentNode.value) && - commentNode.loc!.end.line === nodeLoc.start.line - 1 - ) { - return true; - } - } - return false; - } - - let babelAST; - if (filename.endsWith('.tsx') || filename.endsWith('.ts')) { - try { - const {parse: babelParse} = require('@babel/parser'); - babelAST = babelParse(sourceCode.text, { - filename, - sourceType: 'unambiguous', - plugins: ['typescript', 'jsx'], - }); - } catch { - /* empty */ - } - } else { - try { - babelAST = HermesParser.parse(sourceCode.text, { - babel: true, - enableExperimentalComponentSyntax: true, - sourceFilename: filename, - sourceType: 'module', - }); - } catch { - /* empty */ - } - } - - if (babelAST != null) { - try { - transformFromAstSync(babelAST, sourceCode.text, { - filename, - highlightCode: false, - retainLines: true, - plugins: [ - [PluginProposalPrivateMethods, {loose: true}], - [BabelPluginReactCompiler, options], - ], - sourceType: 'module', - configFile: false, - babelrc: false, - }); - } catch (err) { - /* errors handled by injected logger */ } } + return {}; + }; + + return { + meta: { + type: 'problem', + docs: { + description: rule.description, + recommended: rule.recommended, + }, + fixable: 'code', + hasSuggestions: true, + // validation is done at runtime with zod + schema: [{type: 'object', additionalProperties: true}], + }, + create, + }; +} - function reportUnusedOptOutDirective(stmt: Statement) { - if ( - stmt.type === 'ExpressionStatement' && - stmt.expression.type === 'Literal' && - typeof stmt.expression.value === 'string' && - OPT_OUT_DIRECTIVES.has(stmt.expression.value) && - stmt.loc != null - ) { - context.report({ - message: `Unused '${stmt.expression.value}' directive`, - loc: stmt.loc, - suggest: [ - { - desc: 'Remove the directive', - fix(fixer) { - return fixer.remove(stmt); - }, +export const NoUnusedDirectivesRule: Rule.RuleModule = { + meta: { + type: 'suggestion', + docs: { + recommended: true, + }, + fixable: 'code', + hasSuggestions: true, + // validation is done at runtime with zod + schema: [{type: 'object', additionalProperties: true}], + }, + create(context: Rule.RuleContext): Rule.RuleListener { + const results = getReactCompilerResult(context); + + for (const directive of results.unusedOptOutDirectives) { + context.report({ + message: `Unused '${directive.directive}' directive`, + loc: directive.loc, + suggest: [ + { + desc: 'Remove the directive', + fix(fixer): Rule.Fix { + return fixer.removeRange(directive.range); }, - ], - }); - } - } - if (shouldReportUnusedOptOutDirective) { - return { - FunctionDeclaration(fnDecl) { - for (const stmt of fnDecl.body.body) { - reportUnusedOptOutDirective(stmt); - } - }, - ArrowFunctionExpression(fnExpr) { - if (fnExpr.body.type === 'BlockStatement') { - for (const stmt of fnExpr.body.body) { - reportUnusedOptOutDirective(stmt); - } - } - }, - FunctionExpression(fnExpr) { - for (const stmt of fnExpr.body.body) { - reportUnusedOptOutDirective(stmt); - } - }, - }; - } else { - return {}; + }, + ], + }); } + return {}; }, }; -export default rule; +type RulesObject = {[name: string]: Rule.RuleModule}; + +export const allRules: RulesObject = LintRules.reduce( + (acc, rule) => { + acc[rule.name] = makeRule(rule); + return acc; + }, + { + 'no-unused-directives': NoUnusedDirectivesRule, + } as RulesObject, +); + +export const recommendedRules: RulesObject = LintRules.filter( + rule => rule.recommended, +).reduce( + (acc, rule) => { + acc[rule.name] = makeRule(rule); + return acc; + }, + { + 'no-unused-directives': NoUnusedDirectivesRule, + } as RulesObject, +); diff --git a/compiler/packages/eslint-plugin-react-compiler/src/shared/RunReactCompiler.ts b/compiler/packages/eslint-plugin-react-compiler/src/shared/RunReactCompiler.ts new file mode 100644 index 0000000000000..e8661d7f96c0b --- /dev/null +++ b/compiler/packages/eslint-plugin-react-compiler/src/shared/RunReactCompiler.ts @@ -0,0 +1,287 @@ +/** + * 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 {transformFromAstSync, traverse} from '@babel/core'; +import {parse as babelParse} from '@babel/parser'; +import {Directive, File} from '@babel/types'; +// @ts-expect-error: no types available +import PluginProposalPrivateMethods from '@babel/plugin-proposal-private-methods'; +import BabelPluginReactCompiler, { + parsePluginOptions, + validateEnvironmentConfig, + OPT_OUT_DIRECTIVES, + type PluginOptions, +} from 'babel-plugin-react-compiler/src'; +import {Logger, LoggerEvent} from 'babel-plugin-react-compiler/src/Entrypoint'; +import type {SourceCode} from 'eslint'; +import {SourceLocation} from 'estree'; +// @ts-expect-error: no types available +import * as HermesParser from 'hermes-parser'; +import {isDeepStrictEqual} from 'util'; +import type {ParseResult} from '@babel/parser'; + +const COMPILER_OPTIONS: Partial = { + noEmit: true, + panicThreshold: 'none', + // Don't emit errors on Flow suppressions--Flow already gave a signal + flowSuppressions: false, + environment: validateEnvironmentConfig({ + validateRefAccessDuringRender: true, + validateNoSetStateInRender: true, + validateNoSetStateInEffects: true, + validateNoJSXInTryStatements: true, + validateNoImpureFunctionsInRender: true, + validateStaticComponents: true, + validateNoFreezingKnownMutableFunctions: true, + validateNoVoidUseMemo: true, + // TODO: remove, this should be in the type system + validateNoCapitalizedCalls: [], + validateHooksUsage: true, + validateNoDerivedComputationsInEffects: true, + }), +}; + +export type UnusedOptOutDirective = { + loc: SourceLocation; + range: [number, number]; + directive: string; +}; +export type RunCacheEntry = { + sourceCode: string; + filename: string; + userOpts: PluginOptions; + flowSuppressions: Array<{line: number; code: string}>; + unusedOptOutDirectives: Array; + events: Array; +}; + +type RunParams = { + sourceCode: SourceCode; + filename: string; + userOpts: PluginOptions; +}; +const FLOW_SUPPRESSION_REGEX = /\$FlowFixMe\[([^\]]*)\]/g; + +function getFlowSuppressions( + sourceCode: SourceCode, +): Array<{line: number; code: string}> { + const comments = sourceCode.getAllComments(); + const results: Array<{line: number; code: string}> = []; + + for (const commentNode of comments) { + const matches = commentNode.value.matchAll(FLOW_SUPPRESSION_REGEX); + for (const match of matches) { + if (match.index != null && commentNode.loc != null) { + const code = match[1]; + results.push({ + line: commentNode.loc!.end.line, + code, + }); + } + } + } + return results; +} + +function filterUnusedOptOutDirectives( + directives: ReadonlyArray, +): Array { + const results: Array = []; + for (const directive of directives) { + if ( + OPT_OUT_DIRECTIVES.has(directive.value.value) && + directive.loc != null + ) { + results.push({ + loc: directive.loc, + directive: directive.value.value, + range: [directive.start!, directive.end!], + }); + } + } + return results; +} + +function runReactCompilerImpl({ + sourceCode, + filename, + userOpts, +}: RunParams): RunCacheEntry { + // Compat with older versions of eslint + const options: PluginOptions = parsePluginOptions({ + ...COMPILER_OPTIONS, + ...userOpts, + environment: { + ...COMPILER_OPTIONS.environment, + ...userOpts.environment, + }, + }); + const results: RunCacheEntry = { + sourceCode: sourceCode.text, + filename, + userOpts, + flowSuppressions: [], + unusedOptOutDirectives: [], + events: [], + }; + const userLogger: Logger | null = options.logger; + options.logger = { + logEvent: (eventFilename, event): void => { + userLogger?.logEvent(eventFilename, event); + results.events.push(event); + }, + }; + + try { + options.environment = validateEnvironmentConfig(options.environment ?? {}); + } catch (err: unknown) { + options.logger?.logEvent(filename, err as LoggerEvent); + } + + let babelAST: ParseResult | null = null; + if (filename.endsWith('.tsx') || filename.endsWith('.ts')) { + try { + babelAST = babelParse(sourceCode.text, { + sourceFilename: filename, + sourceType: 'unambiguous', + plugins: ['typescript', 'jsx'], + }); + } catch { + /* empty */ + } + } else { + try { + babelAST = HermesParser.parse(sourceCode.text, { + babel: true, + enableExperimentalComponentSyntax: true, + sourceFilename: filename, + sourceType: 'module', + }); + } catch { + /* empty */ + } + } + + if (babelAST != null) { + results.flowSuppressions = getFlowSuppressions(sourceCode); + try { + transformFromAstSync(babelAST, sourceCode.text, { + filename, + highlightCode: false, + retainLines: true, + plugins: [ + [PluginProposalPrivateMethods, {loose: true}], + [BabelPluginReactCompiler, options], + ], + sourceType: 'module', + configFile: false, + babelrc: false, + }); + + if (results.events.filter(e => e.kind === 'CompileError').length === 0) { + traverse(babelAST, { + FunctionDeclaration(path) { + path.node; + results.unusedOptOutDirectives.push( + ...filterUnusedOptOutDirectives(path.node.body.directives), + ); + }, + ArrowFunctionExpression(path) { + if (path.node.body.type === 'BlockStatement') { + results.unusedOptOutDirectives.push( + ...filterUnusedOptOutDirectives(path.node.body.directives), + ); + } + }, + FunctionExpression(path) { + results.unusedOptOutDirectives.push( + ...filterUnusedOptOutDirectives(path.node.body.directives), + ); + }, + }); + } + } catch (err) { + /* errors handled by injected logger */ + } + } + + return results; +} + +const SENTINEL = Symbol(); + +// Array backed LRU cache -- should be small < 10 elements +class LRUCache { + // newest at headIdx, then headIdx + 1, ..., tailIdx + #values: Array<[K, T | Error] | [typeof SENTINEL, void]>; + #headIdx: number = 0; + + constructor(size: number) { + this.#values = new Array(size).fill(SENTINEL); + } + + // gets a value and sets it as "recently used" + get(key: K): T | null { + let idx = this.#values.findIndex(entry => entry[0] === key); + // If found, move to front + if (idx === this.#headIdx) { + return this.#values[this.#headIdx][1] as T; + } else if (idx < 0) { + return null; + } + + const entry: [K, T] = this.#values[idx] as [K, T]; + + const len = this.#values.length; + for (let i = 0; i < Math.min(idx, len - 1); i++) { + this.#values[(this.#headIdx + i + 1) % len] = + this.#values[(this.#headIdx + i) % len]; + } + this.#values[this.#headIdx] = entry; + return entry[1]; + } + push(key: K, value: T): void { + this.#headIdx = + (this.#headIdx - 1 + this.#values.length) % this.#values.length; + this.#values[this.#headIdx] = [key, value]; + } +} +const cache = new LRUCache(10); + +export default function runReactCompiler({ + sourceCode, + filename, + userOpts, +}: RunParams): RunCacheEntry { + const entry = cache.get(filename); + if ( + entry != null && + entry.sourceCode === sourceCode.text && + isDeepStrictEqual(entry.userOpts, userOpts) + ) { + return entry; + } else if (entry != null) { + if (process.env['DEBUG']) { + console.log( + `Cache hit for ${filename}, but source code or options changed, recomputing`, + ); + } + } + + const runEntry = runReactCompilerImpl({ + sourceCode, + filename, + userOpts, + }); + // If we have a cache entry, we can update it + if (entry != null) { + Object.assign(entry, runEntry); + } else { + cache.push(filename, runEntry); + } + return {...runEntry}; +} diff --git a/compiler/packages/react-mcp-server/src/index.ts b/compiler/packages/react-mcp-server/src/index.ts index 2ec747eac4dfd..2871027d64ce3 100644 --- a/compiler/packages/react-mcp-server/src/index.ts +++ b/compiler/packages/react-mcp-server/src/index.ts @@ -21,6 +21,7 @@ import {queryAlgolia} from './utils/algolia'; import assertExhaustive from './utils/assertExhaustive'; import {convert} from 'html-to-text'; import {measurePerformance} from './tools/runtimePerf'; +import {parseReactComponentTree} from './tools/componentTree'; function calculateMean(values: number[]): string { return values.length > 0 @@ -366,6 +367,45 @@ ${calculateMean(results.renderTime)} }, ); +server.tool( + 'parse-react-component-tree', + ` + This tool gets the component tree of a React App. + passing in a url will attempt to connect to the browser and get the current state of the component tree. If no url is passed in, + the default url will be used (http://localhost:3000). + + + - The url should be a full url with the protocol (http:// or https://) and the domain name (e.g. localhost:3000). + - Also the user should be running a Chrome browser running on debug mode on port 9222. If you receive an error message, advise the user to run + the following comand in the terminal: + MacOS: "/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --remote-debugging-port=9222 --user-data-dir=/tmp/chrome" + Windows: "chrome.exe --remote-debugging-port=9222 --user-data-dir=C:\temp\chrome" + + `, + { + url: z.string().optional().default('http://localhost:3000'), + }, + async ({url}) => { + try { + const componentTree = await parseReactComponentTree(url); + + return { + content: [ + { + type: 'text' as const, + text: componentTree, + }, + ], + }; + } catch (err) { + return { + isError: true, + content: [{type: 'text' as const, text: `Error: ${err.stack}`}], + }; + } + }, +); + server.prompt('review-react-code', () => ({ messages: [ { diff --git a/compiler/packages/react-mcp-server/src/tools/componentTree.ts b/compiler/packages/react-mcp-server/src/tools/componentTree.ts new file mode 100644 index 0000000000000..a124066a9424e --- /dev/null +++ b/compiler/packages/react-mcp-server/src/tools/componentTree.ts @@ -0,0 +1,38 @@ +import puppeteer from 'puppeteer'; + +export async function parseReactComponentTree(url: string): Promise { + try { + const browser = await puppeteer.connect({ + browserURL: 'http://127.0.0.1:9222', + defaultViewport: null, + }); + + const pages = await browser.pages(); + + let localhostPage = null; + for (const page of pages) { + const pageUrl = await page.url(); + + if (pageUrl.startsWith(url)) { + localhostPage = page; + break; + } + } + + if (localhostPage) { + const componentTree = await localhostPage.evaluate(() => { + return (window as any).__REACT_DEVTOOLS_GLOBAL_HOOK__.rendererInterfaces + .get(1) + .__internal_only_getComponentTree(); + }); + + return componentTree; + } else { + throw new Error( + `Could not open the page at ${url}. Is your server running?`, + ); + } + } catch (error) { + throw new Error('Failed extract component tree' + error); + } +} diff --git a/compiler/packages/snap/src/SproutTodoFilter.ts b/compiler/packages/snap/src/SproutTodoFilter.ts index 62b8a7703fddb..02cb3775cb549 100644 --- a/compiler/packages/snap/src/SproutTodoFilter.ts +++ b/compiler/packages/snap/src/SproutTodoFilter.ts @@ -460,6 +460,7 @@ const skipFilter = new Set([ 'fbt/bug-fbt-plural-multiple-function-calls', 'fbt/bug-fbt-plural-multiple-mixed-call-tag', 'bug-invalid-phi-as-dependency', + 'bug-ref-prefix-postfix-operator', // 'react-compiler-runtime' not yet supported 'flag-enable-emit-hook-guards', @@ -485,6 +486,7 @@ const skipFilter = new Set([ 'todo.lower-context-access-array-destructuring', 'lower-context-selector-simple', 'lower-context-acess-multiple', + 'bug-separate-memoization-due-to-callback-capturing', ]); export default skipFilter; diff --git a/compiler/packages/snap/src/compiler.ts b/compiler/packages/snap/src/compiler.ts index 0cadf30bf0eb1..a6041bd5cc2a7 100644 --- a/compiler/packages/snap/src/compiler.ts +++ b/compiler/packages/snap/src/compiler.ts @@ -18,7 +18,11 @@ import type { CompilerReactTarget, CompilerPipelineValue, } from 'babel-plugin-react-compiler/src/Entrypoint'; -import type {Effect, ValueKind} from 'babel-plugin-react-compiler/src/HIR'; +import type { + Effect, + ValueKind, + ValueReason, +} from 'babel-plugin-react-compiler/src/HIR'; import type {parseConfigPragmaForTests as ParseConfigPragma} from 'babel-plugin-react-compiler/src/Utils/TestUtils'; import * as HermesParser from 'hermes-parser'; import invariant from 'invariant'; @@ -42,6 +46,7 @@ function makePluginOptions( debugIRLogger: (value: CompilerPipelineValue) => void, EffectEnum: typeof Effect, ValueKindEnum: typeof ValueKind, + ValueReasonEnum: typeof ValueReason, ): [PluginOptions, Array<{filename: string | null; event: LoggerEvent}>] { // TODO(@mofeiZ) rewrite snap fixtures to @validatePreserveExistingMemo:false let validatePreserveExistingMemoizationGuarantees = false; @@ -77,6 +82,7 @@ function makePluginOptions( moduleTypeProvider: makeSharedRuntimeTypeProvider({ EffectEnum, ValueKindEnum, + ValueReasonEnum, }), assertValidMutableRanges: true, validatePreserveExistingMemoizationGuarantees, @@ -209,6 +215,7 @@ export async function transformFixtureInput( debugIRLogger: (value: CompilerPipelineValue) => void, EffectEnum: typeof Effect, ValueKindEnum: typeof ValueKind, + ValueReasonEnum: typeof ValueReason, ): Promise<{kind: 'ok'; value: TransformResult} | {kind: 'err'; msg: string}> { // Extract the first line to quickly check for custom test directives const firstLine = input.substring(0, input.indexOf('\n')); @@ -237,11 +244,13 @@ export async function transformFixtureInput( debugIRLogger, EffectEnum, ValueKindEnum, + ValueReasonEnum, ); const forgetResult = transformFromAstSync(inputAst, input, { filename: virtualFilepath, highlightCode: false, retainLines: true, + compact: true, plugins: [ [plugin, options], 'babel-plugin-fbt', @@ -329,7 +338,16 @@ export async function transformFixtureInput( if (logs.length !== 0) { formattedLogs = logs .map(({event}) => { - return JSON.stringify(event); + return JSON.stringify(event, (key, value) => { + if ( + key === 'detail' && + value != null && + typeof value.serialize === 'function' + ) { + return value.serialize(); + } + return value; + }); }) .join('\n'); } diff --git a/compiler/packages/snap/src/runner-worker.ts b/compiler/packages/snap/src/runner-worker.ts index 2478e6a545b72..554348534e305 100644 --- a/compiler/packages/snap/src/runner-worker.ts +++ b/compiler/packages/snap/src/runner-worker.ts @@ -24,6 +24,7 @@ import type { CompilerPipelineValue, Effect, ValueKind, + ValueReason, } from 'babel-plugin-react-compiler/src'; import chalk from 'chalk'; @@ -78,6 +79,9 @@ async function compile( const ValueKindEnum = importedCompilerPlugin[ 'ValueKind' ] as typeof ValueKind; + const ValueReasonEnum = importedCompilerPlugin[ + 'ValueReason' + ] as typeof ValueReason; const printFunctionWithOutlined = importedCompilerPlugin[ PRINT_HIR_IMPORT ] as typeof PrintFunctionWithOutlined; @@ -128,6 +132,7 @@ async function compile( debugIRLogger, EffectEnum, ValueKindEnum, + ValueReasonEnum, ); if (result.kind === 'err') { @@ -140,29 +145,6 @@ async function compile( console.error(e.stack); } error = e.message.replace(/\u001b[^m]*m/g, ''); - const loc = e.details?.[0]?.loc; - if (loc != null) { - try { - error = codeFrameColumns( - input, - { - start: { - line: loc.start.line, - column: loc.start.column + 1, - }, - end: { - line: loc.end.line, - column: loc.end.column + 1, - }, - }, - { - message: e.message, - }, - ); - } catch { - // In case the location data isn't valid, skip printing a code frame. - } - } } // Promote console errors so they can be recorded in fixture output diff --git a/compiler/packages/snap/src/sprout/shared-runtime-type-provider.ts b/compiler/packages/snap/src/sprout/shared-runtime-type-provider.ts index 4c1d77f2f8986..b01a204e78b35 100644 --- a/compiler/packages/snap/src/sprout/shared-runtime-type-provider.ts +++ b/compiler/packages/snap/src/sprout/shared-runtime-type-provider.ts @@ -5,15 +5,21 @@ * LICENSE file in the root directory of this source tree. */ -import type {Effect, ValueKind} from 'babel-plugin-react-compiler/src'; +import type { + Effect, + ValueKind, + ValueReason, +} from 'babel-plugin-react-compiler/src'; import type {TypeConfig} from 'babel-plugin-react-compiler/src/HIR/TypeSchema'; export function makeSharedRuntimeTypeProvider({ EffectEnum, ValueKindEnum, + ValueReasonEnum, }: { EffectEnum: typeof Effect; ValueKindEnum: typeof ValueKind; + ValueReasonEnum: typeof ValueReason; }) { return function sharedRuntimeTypeProvider( moduleName: string, @@ -69,6 +75,172 @@ export function makeSharedRuntimeTypeProvider({ returnValueKind: ValueKindEnum.Mutable, noAlias: true, }, + typedIdentity: { + kind: 'function', + positionalParams: [EffectEnum.Read], + restParam: null, + calleeEffect: EffectEnum.Read, + returnType: {kind: 'type', name: 'Any'}, + returnValueKind: ValueKindEnum.Mutable, + aliasing: { + receiver: '@receiver', + params: ['@value'], + rest: null, + returns: '@return', + temporaries: [], + effects: [{kind: 'Assign', from: '@value', into: '@return'}], + }, + }, + typedAssign: { + kind: 'function', + positionalParams: [EffectEnum.Read], + restParam: null, + calleeEffect: EffectEnum.Read, + returnType: {kind: 'type', name: 'Any'}, + returnValueKind: ValueKindEnum.Mutable, + aliasing: { + receiver: '@receiver', + params: ['@value'], + rest: null, + returns: '@return', + temporaries: [], + effects: [{kind: 'Assign', from: '@value', into: '@return'}], + }, + }, + typedAlias: { + kind: 'function', + positionalParams: [EffectEnum.Read], + restParam: null, + calleeEffect: EffectEnum.Read, + returnType: {kind: 'type', name: 'Any'}, + returnValueKind: ValueKindEnum.Mutable, + aliasing: { + receiver: '@receiver', + params: ['@value'], + rest: null, + returns: '@return', + temporaries: [], + effects: [ + { + kind: 'Create', + into: '@return', + value: ValueKindEnum.Mutable, + reason: ValueReasonEnum.KnownReturnSignature, + }, + {kind: 'Alias', from: '@value', into: '@return'}, + ], + }, + }, + typedCapture: { + kind: 'function', + positionalParams: [EffectEnum.Read], + restParam: null, + calleeEffect: EffectEnum.Read, + returnType: {kind: 'type', name: 'Array'}, + returnValueKind: ValueKindEnum.Mutable, + aliasing: { + receiver: '@receiver', + params: ['@value'], + rest: null, + returns: '@return', + temporaries: [], + effects: [ + { + kind: 'Create', + into: '@return', + value: ValueKindEnum.Mutable, + reason: ValueReasonEnum.KnownReturnSignature, + }, + {kind: 'Capture', from: '@value', into: '@return'}, + ], + }, + }, + typedCreateFrom: { + kind: 'function', + positionalParams: [EffectEnum.Read], + restParam: null, + calleeEffect: EffectEnum.Read, + returnType: {kind: 'type', name: 'Any'}, + returnValueKind: ValueKindEnum.Mutable, + aliasing: { + receiver: '@receiver', + params: ['@value'], + rest: null, + returns: '@return', + temporaries: [], + effects: [{kind: 'CreateFrom', from: '@value', into: '@return'}], + }, + }, + typedMutate: { + kind: 'function', + positionalParams: [EffectEnum.Read, EffectEnum.Capture], + restParam: null, + calleeEffect: EffectEnum.Store, + returnType: {kind: 'type', name: 'Primitive'}, + returnValueKind: ValueKindEnum.Primitive, + aliasing: { + receiver: '@receiver', + params: ['@object', '@value'], + rest: null, + returns: '@return', + temporaries: [], + effects: [ + { + kind: 'Create', + into: '@return', + value: ValueKindEnum.Primitive, + reason: ValueReasonEnum.KnownReturnSignature, + }, + {kind: 'Mutate', value: '@object'}, + {kind: 'Capture', from: '@value', into: '@object'}, + ], + }, + }, + }, + }; + } else if (moduleName === 'ReactCompilerKnownIncompatibleTest') { + /** + * Fake module used for testing validation of known incompatible + * API validation + */ + return { + kind: 'object', + properties: { + useKnownIncompatible: { + kind: 'hook', + positionalParams: [], + restParam: EffectEnum.Read, + returnType: {kind: 'type', name: 'Any'}, + knownIncompatible: `useKnownIncompatible is known to be incompatible`, + }, + useKnownIncompatibleIndirect: { + kind: 'hook', + positionalParams: [], + restParam: EffectEnum.Read, + returnType: { + kind: 'object', + properties: { + incompatible: { + kind: 'function', + positionalParams: [], + restParam: EffectEnum.Read, + calleeEffect: EffectEnum.Read, + returnType: {kind: 'type', name: 'Any'}, + returnValueKind: ValueKindEnum.Mutable, + knownIncompatible: `useKnownIncompatibleIndirect returns an incompatible() function that is known incompatible`, + }, + }, + }, + }, + knownIncompatible: { + kind: 'function', + positionalParams: [], + restParam: EffectEnum.Read, + calleeEffect: EffectEnum.Read, + returnType: {kind: 'type', name: 'Any'}, + returnValueKind: ValueKindEnum.Mutable, + knownIncompatible: `useKnownIncompatible is known to be incompatible`, + }, }, }; } else if (moduleName === 'ReactCompilerTest') { diff --git a/compiler/packages/snap/src/sprout/shared-runtime.ts b/compiler/packages/snap/src/sprout/shared-runtime.ts index 1b8648f4ff031..f37ca82709022 100644 --- a/compiler/packages/snap/src/sprout/shared-runtime.ts +++ b/compiler/packages/snap/src/sprout/shared-runtime.ts @@ -128,6 +128,14 @@ export function getNull(): null { return null; } +export function getTrue(): true { + return true; +} + +export function getFalse(): false { + return false; +} + export function calculateExpensiveNumber(x: number): number { return x; } @@ -264,7 +272,7 @@ export function ValidateMemoization({ }: { inputs: Array; output: any; - onlyCheckCompiled: boolean; + onlyCheckCompiled?: boolean; }): React.ReactElement { 'use no forget'; // Wrap rawOutput as it might be a function, which useState would invoke. @@ -272,8 +280,9 @@ export function ValidateMemoization({ const [previousInputs, setPreviousInputs] = React.useState(inputs); const [previousOutput, setPreviousOutput] = React.useState(output); if ( - onlyCheckCompiled && - (globalThis as any).__SNAP_EVALUATOR_MODE === 'forget' + !onlyCheckCompiled || + (onlyCheckCompiled && + (globalThis as any).__SNAP_EVALUATOR_MODE === 'forget') ) { if ( inputs.length !== previousInputs.length || @@ -388,4 +397,28 @@ export function typedLog(...values: Array): void { console.log(...values); } +export function typedIdentity(value: T): T { + return value; +} + +export function typedAssign(x: T): T { + return x; +} + +export function typedAlias(x: T): T { + return x; +} + +export function typedCapture(x: T): Array { + return [x]; +} + +export function typedCreateFrom(array: Array): T { + return array[0]; +} + +export function typedMutate(x: any, v: any = null): void { + x.property = v; +} + export default typedLog; diff --git a/compiler/scripts/build-eslint-docs.js b/compiler/scripts/build-eslint-docs.js new file mode 100644 index 0000000000000..d8d59096d9ea6 --- /dev/null +++ b/compiler/scripts/build-eslint-docs.js @@ -0,0 +1,32 @@ +const ReactCompiler = require('../packages/babel-plugin-react-compiler/dist'); + +const combinedRules = [ + { + name: 'rules-of-hooks', + recommended: true, + description: + 'Validates that components and hooks follow the [Rules of Hooks](https://react.dev/reference/rules/rules-of-hooks)', + }, + { + name: 'exhaustive-deps', + recommended: true, + description: + 'Validates that hooks which accept dependency arrays (`useMemo()`, `useCallback()`, `useEffect()`, etc) ' + + 'list all referenced variables in their dependency array. Referencing a value without including it in the ' + + 'dependency array can lead to stale UI or callbacks.', + }, + ...ReactCompiler.LintRules, +]; + +const printed = combinedRules + .filter(rule => rule.recommended) + .map(rule => { + return ` +## \`react-hooks/${rule.name}\` + +${rule.description} + `.trim(); + }) + .join('\n\n'); + +console.log(printed); diff --git a/compiler/scripts/release/shared/packages.js b/compiler/scripts/release/shared/packages.js index 39970bdde6c39..235ba0f1ddb54 100644 --- a/compiler/scripts/release/shared/packages.js +++ b/compiler/scripts/release/shared/packages.js @@ -7,7 +7,6 @@ const PUBLISHABLE_PACKAGES = [ 'babel-plugin-react-compiler', - 'eslint-plugin-react-compiler', 'react-compiler-healthcheck', 'react-compiler-runtime', ]; diff --git a/compiler/scripts/rustfmt.sh b/compiler/scripts/rustfmt.sh deleted file mode 100755 index 2fb09ae324668..0000000000000 --- a/compiler/scripts/rustfmt.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/usr/bin/env bash -# 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. - -set -eo pipefail - -# Executes rustfmt using a nightly build of the compiler -# NOTE: this command must exactly match the Rust Lint command in .github/workflows/rust.yml -rustup toolchain list | grep -q nightly-2023-08-01 || (echo "Expected Rust version missing, try running: 'rustup toolchain install nightly-2023-08-01'" && exit 1) -grep -r --include "*.rs" --files-without-match "@generated" crates | xargs rustup run nightly-2023-08-01 rustfmt --config="skip_children=true" "$@" diff --git a/compiler/yarn.lock b/compiler/yarn.lock index 6e1bc7feeb32f..696261cbf53af 100644 --- a/compiler/yarn.lock +++ b/compiler/yarn.lock @@ -542,11 +542,6 @@ resolved "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz" integrity sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA== -"@babel/helper-string-parser@^7.27.1": - version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz#54da796097ab19ce67ed9f88b47bb2ec49367687" - integrity sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA== - "@babel/helper-validator-identifier@^7.19.1", "@babel/helper-validator-identifier@^7.25.9": version "7.25.9" resolved "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz" @@ -1605,7 +1600,7 @@ debug "^4.3.1" globals "^11.1.0" -"@babel/types@^7.0.0", "@babel/types@^7.19.0", "@babel/types@^7.2.0", "@babel/types@^7.2.2", "@babel/types@^7.20.2", "@babel/types@^7.20.7", "@babel/types@^7.21.2", "@babel/types@^7.24.7", "@babel/types@^7.25.9", "@babel/types@^7.26.0", "@babel/types@^7.26.3", "@babel/types@^7.3.0", "@babel/types@^7.3.3", "@babel/types@^7.4.4", "@babel/types@^7.7.4": +"@babel/types@7.26.3", "@babel/types@^7.0.0", "@babel/types@^7.19.0", "@babel/types@^7.2.0", "@babel/types@^7.2.2", "@babel/types@^7.20.2", "@babel/types@^7.20.7", "@babel/types@^7.21.2", "@babel/types@^7.24.7", "@babel/types@^7.25.9", "@babel/types@^7.26.0", "@babel/types@^7.26.10", "@babel/types@^7.26.3", "@babel/types@^7.27.0", "@babel/types@^7.27.1", "@babel/types@^7.3.0", "@babel/types@^7.3.3", "@babel/types@^7.4.4", "@babel/types@^7.7.4": version "7.26.3" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.26.3.tgz#37e79830f04c2b5687acc77db97fbc75fb81f3c0" integrity sha512-vN5p+1kl59GVKMvTHt55NzzmYVxprfJD+ql7U9NFIfKCBkYE55LYtS+WtPlaYOyzydrKI8Nezd+aZextrd+FMA== @@ -1613,14 +1608,6 @@ "@babel/helper-string-parser" "^7.25.9" "@babel/helper-validator-identifier" "^7.25.9" -"@babel/types@^7.26.10", "@babel/types@^7.27.0", "@babel/types@^7.27.1": - version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.27.1.tgz#9defc53c16fc899e46941fc6901a9eea1c9d8560" - integrity sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q== - dependencies: - "@babel/helper-string-parser" "^7.27.1" - "@babel/helper-validator-identifier" "^7.27.1" - "@bcoe/v8-coverage@^0.2.3": version "0.2.3" resolved "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz" @@ -2148,6 +2135,11 @@ slash "^3.0.0" strip-ansi "^6.0.0" +"@jest/diff-sequences@30.0.1": + version "30.0.1" + resolved "https://registry.yarnpkg.com/@jest/diff-sequences/-/diff-sequences-30.0.1.tgz#0ededeae4d071f5c8ffe3678d15f3a1be09156be" + integrity sha512-n5H8QLDJ47QqbCNn5SuFjCRDrOLEZ0h8vAHCK5RL9Ls7Xa8AQLa/YxAc9UjFqoEDM48muwtBGjtMY5cr0PLDCw== + "@jest/environment@^28.1.3": version "28.1.3" resolved "https://registry.npmjs.org/@jest/environment/-/environment-28.1.3.tgz" @@ -2178,6 +2170,13 @@ "@types/node" "*" jest-mock "^29.5.0" +"@jest/expect-utils@30.0.5": + version "30.0.5" + resolved "https://registry.yarnpkg.com/@jest/expect-utils/-/expect-utils-30.0.5.tgz#9d42e4b8bc80367db30abc6c42b2cb14073f66fc" + integrity sha512-F3lmTT7CXWYywoVUGTCmom0vXq3HTTkaZyTAzIy+bXSBizB7o5qzlC9VCtq0arOa8GqmNsbg/cE9C6HLn7Szew== + dependencies: + "@jest/get-type" "30.0.1" + "@jest/expect-utils@^28.1.3": version "28.1.3" resolved "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-28.1.3.tgz" @@ -2274,6 +2273,11 @@ jest-mock "^29.5.0" jest-util "^29.5.0" +"@jest/get-type@30.0.1": + version "30.0.1" + resolved "https://registry.yarnpkg.com/@jest/get-type/-/get-type-30.0.1.tgz#0d32f1bbfba511948ad247ab01b9007724fc9f52" + integrity sha512-AyYdemXCptSRFirI5EPazNxyPwAL0jXt3zceFjaj8NFiKP9pOi0bfXonf6qkf82z2t3QWPeLCWWw4stPBzctLw== + "@jest/globals@^28.1.3": version "28.1.3" resolved "https://registry.npmjs.org/@jest/globals/-/globals-28.1.3.tgz" @@ -2313,6 +2317,14 @@ "@jest/types" "^29.6.3" jest-mock "^29.7.0" +"@jest/pattern@30.0.1": + version "30.0.1" + resolved "https://registry.yarnpkg.com/@jest/pattern/-/pattern-30.0.1.tgz#d5304147f49a052900b4b853dedb111d080e199f" + integrity sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA== + dependencies: + "@types/node" "*" + jest-regex-util "30.0.1" + "@jest/reporters@^28.1.3": version "28.1.3" resolved "https://registry.npmjs.org/@jest/reporters/-/reporters-28.1.3.tgz" @@ -2435,6 +2447,13 @@ strip-ansi "^6.0.0" v8-to-istanbul "^9.0.1" +"@jest/schemas@30.0.5": + version "30.0.5" + resolved "https://registry.yarnpkg.com/@jest/schemas/-/schemas-30.0.5.tgz#7bdf69fc5a368a5abdb49fd91036c55225846473" + integrity sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA== + dependencies: + "@sinclair/typebox" "^0.34.0" + "@jest/schemas@^28.1.3": version "28.1.3" resolved "https://registry.npmjs.org/@jest/schemas/-/schemas-28.1.3.tgz" @@ -2663,6 +2682,19 @@ slash "^3.0.0" write-file-atomic "^4.0.2" +"@jest/types@30.0.5": + version "30.0.5" + resolved "https://registry.yarnpkg.com/@jest/types/-/types-30.0.5.tgz#29a33a4c036e3904f1cfd94f6fe77f89d2e1cc05" + integrity sha512-aREYa3aku9SSnea4aX6bhKn4bgv3AXkgijoQgbYV3yvbiGt6z+MQ85+6mIhx9DsKW2BuB/cLR/A+tcMThx+KLQ== + dependencies: + "@jest/pattern" "30.0.1" + "@jest/schemas" "30.0.5" + "@types/istanbul-lib-coverage" "^2.0.6" + "@types/istanbul-reports" "^3.0.4" + "@types/node" "*" + "@types/yargs" "^17.0.33" + chalk "^4.1.2" + "@jest/types@^24.9.0": version "24.9.0" resolved "https://registry.npmjs.org/@jest/types/-/types-24.9.0.tgz" @@ -2965,6 +2997,11 @@ resolved "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz" integrity sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA== +"@sinclair/typebox@^0.34.0": + version "0.34.38" + resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.34.38.tgz#2365df7c23406a4d79413a766567bfbca708b49d" + integrity sha512-HpkxMmc2XmZKhvaKIZZThlHmx1L0I/V1hWK1NubtlFnr6ZqdiOpV72TKudZUNQjZNsyDBay72qFEhEvb+bcwcA== + "@sinonjs/commons@^1.7.0": version "1.8.3" resolved "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.3.tgz" @@ -3154,6 +3191,11 @@ resolved "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz" integrity sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g== +"@types/istanbul-lib-coverage@^2.0.6": + version "2.0.6" + resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz#7739c232a1fee9b4d3ce8985f314c0c6d33549d7" + integrity sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w== + "@types/istanbul-lib-report@*": version "3.0.0" resolved "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz" @@ -3176,6 +3218,13 @@ dependencies: "@types/istanbul-lib-report" "*" +"@types/istanbul-reports@^3.0.4": + version "3.0.4" + resolved "https://registry.yarnpkg.com/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz#0f03e3d2f670fbdac586e34b433783070cc16f54" + integrity sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ== + dependencies: + "@types/istanbul-lib-report" "*" + "@types/jest@^28.1.6": version "28.1.8" resolved "https://registry.npmjs.org/@types/jest/-/jest-28.1.8.tgz" @@ -3200,6 +3249,14 @@ expect "^29.0.0" pretty-format "^29.0.0" +"@types/jest@^30.0.0": + version "30.0.0" + resolved "https://registry.yarnpkg.com/@types/jest/-/jest-30.0.0.tgz#5e85ae568006712e4ad66f25433e9bdac8801f1d" + integrity sha512-XTYugzhuwqWjws0CVz8QpM36+T+Dz5mTEBKhNs/esGLnCIlGdRy+Dq78NRjd7ls7r8BC8ZRMOrKlkO1hU0JOwA== + dependencies: + expect "^30.0.0" + pretty-format "^30.0.0" + "@types/jsdom@^20.0.0": version "20.0.0" resolved "https://registry.npmjs.org/@types/jsdom/-/jsdom-20.0.0.tgz" @@ -3282,6 +3339,11 @@ resolved "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz" integrity sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw== +"@types/stack-utils@^2.0.3": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.3.tgz#6209321eb2c1712a7e7466422b8cb1fc0d9dd5d8" + integrity sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw== + "@types/tough-cookie@*": version "4.0.2" resolved "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.2.tgz" @@ -3309,6 +3371,13 @@ dependencies: "@types/yargs-parser" "*" +"@types/yargs@^17.0.33": + version "17.0.33" + resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-17.0.33.tgz#8c32303da83eec050a84b3c7ae7b9f922d13e32d" + integrity sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA== + dependencies: + "@types/yargs-parser" "*" + "@types/yargs@^17.0.8": version "17.0.13" resolved "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.13.tgz" @@ -3740,7 +3809,7 @@ ansi-styles@^4.0.0, ansi-styles@^4.1.0: dependencies: color-convert "^2.0.1" -ansi-styles@^5.0.0: +ansi-styles@^5.0.0, ansi-styles@^5.2.0: version "5.2.0" resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz" integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA== @@ -3803,11 +3872,32 @@ aria-query@^5.0.0: resolved "https://registry.npmjs.org/aria-query/-/aria-query-5.0.2.tgz" integrity sha512-eigU3vhqSO+Z8BKDnVLN/ompjhf3pYzecKXz8+whRy+9gZu8n1TCGfwzQUUPnqdHl9ax1Hr9031orZ+UOEYr7Q== +array-buffer-byte-length@^1.0.1, array-buffer-byte-length@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz#384d12a37295aec3769ab022ad323a18a51ccf8b" + integrity sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw== + dependencies: + call-bound "^1.0.3" + is-array-buffer "^3.0.5" + array-union@^2.1.0: version "2.1.0" resolved "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz" integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== +arraybuffer.prototype.slice@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz#9d760d84dbdd06d0cbf92c8849615a1a7ab3183c" + integrity sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ== + dependencies: + array-buffer-byte-length "^1.0.1" + call-bind "^1.0.8" + define-properties "^1.2.1" + es-abstract "^1.23.5" + es-errors "^1.3.0" + get-intrinsic "^1.2.6" + is-array-buffer "^3.0.4" + ast-types@^0.13.4: version "0.13.4" resolved "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz" @@ -3815,6 +3905,11 @@ ast-types@^0.13.4: dependencies: tslib "^2.0.1" +async-function@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/async-function/-/async-function-1.0.0.tgz#509c9fca60eaf85034c6829838188e4e4c8ffb2b" + integrity sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA== + async@^3.2.3: version "3.2.6" resolved "https://registry.npmjs.org/async/-/async-3.2.6.tgz" @@ -3825,6 +3920,13 @@ asynckit@^0.4.0: resolved "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz" integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== +available-typed-arrays@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz#a5cc375d6a03c2efc87a553f3e0b1522def14846" + integrity sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ== + dependencies: + possible-typed-array-names "^1.0.0" + axios@^1.6.1: version "1.7.4" resolved "https://registry.npmjs.org/axios/-/axios-1.7.4.tgz" @@ -4245,7 +4347,7 @@ cac@^6.7.14: resolved "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz" integrity sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ== -call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2: +call-bind-apply-helpers@^1.0.0, call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz" integrity sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ== @@ -4253,7 +4355,17 @@ call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2: es-errors "^1.3.0" function-bind "^1.1.2" -call-bound@^1.0.2: +call-bind@^1.0.7, call-bind@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.8.tgz#0736a9660f537e3388826f440d5ec45f744eaa4c" + integrity sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww== + dependencies: + call-bind-apply-helpers "^1.0.0" + es-define-property "^1.0.0" + get-intrinsic "^1.2.4" + set-function-length "^1.2.2" + +call-bound@^1.0.2, call-bound@^1.0.3, call-bound@^1.0.4: version "1.0.4" resolved "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz" integrity sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg== @@ -4295,7 +4407,7 @@ chalk@2.4.2, chalk@^2.0.0, chalk@^2.4.2: escape-string-regexp "^1.0.5" supports-color "^5.3.0" -chalk@4, chalk@^4.0.0, chalk@^4.0.2, chalk@^4.1.0: +chalk@4, chalk@^4.0.0, chalk@^4.0.2, chalk@^4.1.0, chalk@^4.1.2: version "4.1.2" resolved "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz" integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== @@ -4377,6 +4489,11 @@ ci-info@^3.2.0: resolved "https://registry.npmjs.org/ci-info/-/ci-info-3.4.0.tgz" integrity sha512-t5QdPT5jq3o262DOQ8zA6E1tlH2upmUc4Hlvrbx1pGYJuiiHl7O7rvVNI+l8HTVhd/q3Qc9vqimkNk5yiXsAug== +ci-info@^4.2.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-4.3.0.tgz#c39b1013f8fdbd28cd78e62318357d02da160cd7" + integrity sha512-l+2bNRMiQgcfILUi33labAZYIWlH1kWDp+ecNo5iisRKrbm0xcRyCww71/YU0Fkw0mAFpz9bJayXPjey6vkmaQ== + cjs-module-lexer@^1.0.0: version "1.2.2" resolved "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.2.tgz" @@ -4722,6 +4839,33 @@ data-urls@^4.0.0: whatwg-mimetype "^3.0.0" whatwg-url "^12.0.0" +data-view-buffer@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/data-view-buffer/-/data-view-buffer-1.0.2.tgz#211a03ba95ecaf7798a8c7198d79536211f88570" + integrity sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ== + dependencies: + call-bound "^1.0.3" + es-errors "^1.3.0" + is-data-view "^1.0.2" + +data-view-byte-length@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz#9e80f7ca52453ce3e93d25a35318767ea7704735" + integrity sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ== + dependencies: + call-bound "^1.0.3" + es-errors "^1.3.0" + is-data-view "^1.0.2" + +data-view-byte-offset@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz#068307f9b71ab76dbbe10291389e020856606191" + integrity sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ== + dependencies: + call-bound "^1.0.2" + es-errors "^1.3.0" + is-data-view "^1.0.1" + date-fns@^2.29.1: version "2.30.0" resolved "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz" @@ -4788,6 +4932,24 @@ defaults@^1.0.3: dependencies: clone "^1.0.2" +define-data-property@^1.0.1, define-data-property@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.4.tgz#894dc141bb7d3060ae4366f6a0107e68fbe48c5e" + integrity sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A== + dependencies: + es-define-property "^1.0.0" + es-errors "^1.3.0" + gopd "^1.0.1" + +define-properties@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.2.1.tgz#10781cc616eb951a80a034bafcaa7377f6af2b6c" + integrity sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg== + dependencies: + define-data-property "^1.0.1" + has-property-descriptors "^1.0.0" + object-keys "^1.1.1" + degenerator@^5.0.0: version "5.0.1" resolved "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz" @@ -4922,7 +5084,7 @@ dreamopt@~0.6.0: dependencies: wordwrap ">=0.0.2" -dunder-proto@^1.0.1: +dunder-proto@^1.0.0, dunder-proto@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz" integrity sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A== @@ -5033,7 +5195,67 @@ error-ex@^1.3.1: dependencies: is-arrayish "^0.2.1" -es-define-property@^1.0.1: +es-abstract@^1.23.3, es-abstract@^1.23.5, es-abstract@^1.23.9: + version "1.24.0" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.24.0.tgz#c44732d2beb0acc1ed60df840869e3106e7af328" + integrity sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg== + dependencies: + array-buffer-byte-length "^1.0.2" + arraybuffer.prototype.slice "^1.0.4" + available-typed-arrays "^1.0.7" + call-bind "^1.0.8" + call-bound "^1.0.4" + data-view-buffer "^1.0.2" + data-view-byte-length "^1.0.2" + data-view-byte-offset "^1.0.1" + es-define-property "^1.0.1" + es-errors "^1.3.0" + es-object-atoms "^1.1.1" + es-set-tostringtag "^2.1.0" + es-to-primitive "^1.3.0" + function.prototype.name "^1.1.8" + get-intrinsic "^1.3.0" + get-proto "^1.0.1" + get-symbol-description "^1.1.0" + globalthis "^1.0.4" + gopd "^1.2.0" + has-property-descriptors "^1.0.2" + has-proto "^1.2.0" + has-symbols "^1.1.0" + hasown "^2.0.2" + internal-slot "^1.1.0" + is-array-buffer "^3.0.5" + is-callable "^1.2.7" + is-data-view "^1.0.2" + is-negative-zero "^2.0.3" + is-regex "^1.2.1" + is-set "^2.0.3" + is-shared-array-buffer "^1.0.4" + is-string "^1.1.1" + is-typed-array "^1.1.15" + is-weakref "^1.1.1" + math-intrinsics "^1.1.0" + object-inspect "^1.13.4" + object-keys "^1.1.1" + object.assign "^4.1.7" + own-keys "^1.0.1" + regexp.prototype.flags "^1.5.4" + safe-array-concat "^1.1.3" + safe-push-apply "^1.0.0" + safe-regex-test "^1.1.0" + set-proto "^1.0.0" + stop-iteration-iterator "^1.1.0" + string.prototype.trim "^1.2.10" + string.prototype.trimend "^1.0.9" + string.prototype.trimstart "^1.0.8" + typed-array-buffer "^1.0.3" + typed-array-byte-length "^1.0.3" + typed-array-byte-offset "^1.0.4" + typed-array-length "^1.0.7" + unbox-primitive "^1.1.0" + which-typed-array "^1.1.19" + +es-define-property@^1.0.0, es-define-property@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz" integrity sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g== @@ -5050,6 +5272,25 @@ es-object-atoms@^1.0.0, es-object-atoms@^1.1.1: dependencies: es-errors "^1.3.0" +es-set-tostringtag@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz#f31dbbe0c183b00a6d26eb6325c810c0fd18bd4d" + integrity sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA== + dependencies: + es-errors "^1.3.0" + get-intrinsic "^1.2.6" + has-tostringtag "^1.0.2" + hasown "^2.0.2" + +es-to-primitive@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.3.0.tgz#96c89c82cc49fd8794a24835ba3e1ff87f214e18" + integrity sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g== + dependencies: + is-callable "^1.2.7" + is-date-object "^1.0.5" + is-symbol "^1.0.4" + es5-ext@0.8.x: version "0.8.2" resolved "https://registry.npmjs.org/es5-ext/-/es5-ext-0.8.2.tgz" @@ -5428,6 +5669,18 @@ expect@^29.7.0: jest-message-util "^29.7.0" jest-util "^29.7.0" +expect@^30.0.0: + version "30.0.5" + resolved "https://registry.yarnpkg.com/expect/-/expect-30.0.5.tgz#c23bf193c5e422a742bfd2990ad990811de41a5a" + integrity sha512-P0te2pt+hHI5qLJkIR+iMvS+lYUZml8rKKsohVHAGY+uClp9XVbdyYNJOIjSRpHVp8s8YqxJCiHUkSYZGr8rtQ== + dependencies: + "@jest/expect-utils" "30.0.5" + "@jest/get-type" "30.0.1" + jest-matcher-utils "30.0.5" + jest-message-util "30.0.5" + jest-mock "30.0.5" + jest-util "30.0.5" + express-rate-limit@^7.5.0: version "7.5.0" resolved "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.0.tgz" @@ -5719,6 +5972,13 @@ follow-redirects@^1.15.6: resolved "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz" integrity sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA== +for-each@^0.3.3, for-each@^0.3.5: + version "0.3.5" + resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.5.tgz#d650688027826920feeb0af747ee7b9421a41d47" + integrity sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg== + dependencies: + is-callable "^1.2.7" + foreground-child@^3.1.0: version "3.1.1" resolved "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz" @@ -5788,6 +6048,23 @@ function-bind@^1.1.2: resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz" integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== +function.prototype.name@^1.1.6, function.prototype.name@^1.1.8: + version "1.1.8" + resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.8.tgz#e68e1df7b259a5c949eeef95cdbde53edffabb78" + integrity sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q== + dependencies: + call-bind "^1.0.8" + call-bound "^1.0.3" + define-properties "^1.2.1" + functions-have-names "^1.2.3" + hasown "^2.0.2" + is-callable "^1.2.7" + +functions-have-names@^1.2.3: + version "1.2.3" + resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834" + integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ== + gensync@^1.0.0-beta.2: version "1.0.0-beta.2" resolved "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz" @@ -5798,7 +6075,7 @@ get-caller-file@^2.0.1, get-caller-file@^2.0.5: resolved "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz" integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== -get-intrinsic@^1.2.5, get-intrinsic@^1.3.0: +get-intrinsic@^1.2.4, get-intrinsic@^1.2.5, get-intrinsic@^1.2.6, get-intrinsic@^1.2.7, get-intrinsic@^1.3.0: version "1.3.0" resolved "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz" integrity sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ== @@ -5819,7 +6096,7 @@ get-package-type@^0.1.0: resolved "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz" integrity sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q== -get-proto@^1.0.1: +get-proto@^1.0.0, get-proto@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz" integrity sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g== @@ -5839,6 +6116,15 @@ get-stream@^6.0.0: resolved "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz" integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== +get-symbol-description@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/get-symbol-description/-/get-symbol-description-1.1.0.tgz#7bdd54e0befe8ffc9f3b4e203220d9f1e881b6ee" + integrity sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg== + dependencies: + call-bound "^1.0.3" + es-errors "^1.3.0" + get-intrinsic "^1.2.6" + get-uri@^6.0.1: version "6.0.4" resolved "https://registry.npmjs.org/get-uri/-/get-uri-6.0.4.tgz" @@ -5957,6 +6243,14 @@ globals@^14.0.0: resolved "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz" integrity sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ== +globalthis@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/globalthis/-/globalthis-1.0.4.tgz#7430ed3a975d97bfb59bcce41f5cabbafa651236" + integrity sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ== + dependencies: + define-properties "^1.2.1" + gopd "^1.0.1" + globby@^11.1.0: version "11.1.0" resolved "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz" @@ -5969,12 +6263,12 @@ globby@^11.1.0: merge2 "^1.4.1" slash "^3.0.0" -gopd@^1.2.0: +gopd@^1.0.1, gopd@^1.2.0: version "1.2.0" resolved "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz" integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg== -graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.4: +graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.11, graceful-fs@^4.2.4: version "4.2.11" resolved "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz" integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== @@ -5989,6 +6283,11 @@ graphemer@^1.4.0: resolved "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz" integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag== +has-bigints@^1.0.2: + version "1.1.0" + resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.1.0.tgz#28607e965ac967e03cd2a2c70a2636a1edad49fe" + integrity sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg== + has-flag@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz" @@ -5999,11 +6298,32 @@ has-flag@^4.0.0: resolved "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz" integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== -has-symbols@^1.1.0: +has-property-descriptors@^1.0.0, has-property-descriptors@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz#963ed7d071dc7bf5f084c5bfbe0d1b6222586854" + integrity sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg== + dependencies: + es-define-property "^1.0.0" + +has-proto@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.2.0.tgz#5de5a6eabd95fdffd9818b43055e8065e39fe9d5" + integrity sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ== + dependencies: + dunder-proto "^1.0.0" + +has-symbols@^1.0.3, has-symbols@^1.1.0: version "1.1.0" resolved "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz" integrity sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ== +has-tostringtag@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz#2cdc42d40bef2e5b4eeab7c01a73c54ce7ab5abc" + integrity sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw== + dependencies: + has-symbols "^1.0.3" + has@^1.0.3: version "1.0.3" resolved "https://registry.npmjs.org/has/-/has-1.0.3.tgz" @@ -6231,6 +6551,15 @@ ini@^1.3.4: resolved "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz" integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== +internal-slot@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.1.0.tgz#1eac91762947d2f7056bc838d93e13b2e9604961" + integrity sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw== + dependencies: + es-errors "^1.3.0" + hasown "^2.0.2" + side-channel "^1.1.0" + invariant@^2.2.4: version "2.2.4" resolved "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz" @@ -6251,6 +6580,15 @@ ipaddr.js@1.9.1: resolved "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz" integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== +is-array-buffer@^3.0.4, is-array-buffer@^3.0.5: + version "3.0.5" + resolved "https://registry.yarnpkg.com/is-array-buffer/-/is-array-buffer-3.0.5.tgz#65742e1e687bd2cc666253068fd8707fe4d44280" + integrity sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A== + dependencies: + call-bind "^1.0.8" + call-bound "^1.0.3" + get-intrinsic "^1.2.6" + is-arrayish@^0.2.1: version "0.2.1" resolved "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz" @@ -6261,6 +6599,24 @@ is-arrayish@^0.3.1: resolved "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz" integrity sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ== +is-async-function@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-async-function/-/is-async-function-2.1.1.tgz#3e69018c8e04e73b738793d020bfe884b9fd3523" + integrity sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ== + dependencies: + async-function "^1.0.0" + call-bound "^1.0.3" + get-proto "^1.0.1" + has-tostringtag "^1.0.2" + safe-regex-test "^1.1.0" + +is-bigint@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.1.0.tgz#dda7a3445df57a42583db4228682eba7c4170672" + integrity sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ== + dependencies: + has-bigints "^1.0.2" + is-binary-path@~2.1.0: version "2.1.0" resolved "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz" @@ -6268,6 +6624,19 @@ is-binary-path@~2.1.0: dependencies: binary-extensions "^2.0.0" +is-boolean-object@^1.2.1: + version "1.2.2" + resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.2.2.tgz#7067f47709809a393c71ff5bb3e135d8a9215d9e" + integrity sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A== + dependencies: + call-bound "^1.0.3" + has-tostringtag "^1.0.2" + +is-callable@^1.2.7: + version "1.2.7" + resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.7.tgz#3bc2a85ea742d9e36205dcacdd72ca1fdc51b055" + integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA== + is-core-module@^2.9.0: version "2.10.0" resolved "https://registry.npmjs.org/is-core-module/-/is-core-module-2.10.0.tgz" @@ -6275,11 +6644,35 @@ is-core-module@^2.9.0: dependencies: has "^1.0.3" +is-data-view@^1.0.1, is-data-view@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-data-view/-/is-data-view-1.0.2.tgz#bae0a41b9688986c2188dda6657e56b8f9e63b8e" + integrity sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw== + dependencies: + call-bound "^1.0.2" + get-intrinsic "^1.2.6" + is-typed-array "^1.1.13" + +is-date-object@^1.0.5, is-date-object@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.1.0.tgz#ad85541996fc7aa8b2729701d27b7319f95d82f7" + integrity sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg== + dependencies: + call-bound "^1.0.2" + has-tostringtag "^1.0.2" + is-extglob@^2.1.1: version "2.1.1" resolved "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz" integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== +is-finalizationregistry@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz#eefdcdc6c94ddd0674d9c85887bf93f944a97c90" + integrity sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg== + dependencies: + call-bound "^1.0.3" + is-fullwidth-code-point@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz" @@ -6290,6 +6683,16 @@ is-generator-fn@^2.0.0: resolved "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz" integrity sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ== +is-generator-function@^1.0.10: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-generator-function/-/is-generator-function-1.1.0.tgz#bf3eeda931201394f57b5dba2800f91a238309ca" + integrity sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ== + dependencies: + call-bound "^1.0.3" + get-proto "^1.0.0" + has-tostringtag "^1.0.2" + safe-regex-test "^1.1.0" + is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: version "4.0.3" resolved "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz" @@ -6307,6 +6710,24 @@ is-interactive@^2.0.0: resolved "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz" integrity sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ== +is-map@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.3.tgz#ede96b7fe1e270b3c4465e3a465658764926d62e" + integrity sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw== + +is-negative-zero@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.3.tgz#ced903a027aca6381b777a5743069d7376a49747" + integrity sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw== + +is-number-object@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.1.1.tgz#144b21e95a1bc148205dcc2814a9134ec41b2541" + integrity sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw== + dependencies: + call-bound "^1.0.3" + has-tostringtag "^1.0.2" + is-number@^7.0.0: version "7.0.0" resolved "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz" @@ -6339,11 +6760,57 @@ is-promise@^4.0.0: resolved "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz" integrity sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ== +is-regex@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.2.1.tgz#76d70a3ed10ef9be48eb577887d74205bf0cad22" + integrity sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g== + dependencies: + call-bound "^1.0.2" + gopd "^1.2.0" + has-tostringtag "^1.0.2" + hasown "^2.0.2" + +is-set@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/is-set/-/is-set-2.0.3.tgz#8ab209ea424608141372ded6e0cb200ef1d9d01d" + integrity sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg== + +is-shared-array-buffer@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz#9b67844bd9b7f246ba0708c3a93e34269c774f6f" + integrity sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A== + dependencies: + call-bound "^1.0.3" + is-stream@^2.0.0: version "2.0.1" resolved "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz" integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== +is-string@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.1.1.tgz#92ea3f3d5c5b6e039ca8677e5ac8d07ea773cbb9" + integrity sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA== + dependencies: + call-bound "^1.0.3" + has-tostringtag "^1.0.2" + +is-symbol@^1.0.4, is-symbol@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.1.1.tgz#f47761279f532e2b05a7024a7506dbbedacd0634" + integrity sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w== + dependencies: + call-bound "^1.0.2" + has-symbols "^1.1.0" + safe-regex-test "^1.1.0" + +is-typed-array@^1.1.13, is-typed-array@^1.1.14, is-typed-array@^1.1.15: + version "1.1.15" + resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.15.tgz#4bfb4a45b61cee83a5a46fba778e4e8d59c0ce0b" + integrity sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ== + dependencies: + which-typed-array "^1.1.16" + is-unicode-supported@^0.1.0: version "0.1.0" resolved "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz" @@ -6354,11 +6821,36 @@ is-unicode-supported@^1.1.0, is-unicode-supported@^1.3.0: resolved "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz" integrity sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ== +is-weakmap@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/is-weakmap/-/is-weakmap-2.0.2.tgz#bf72615d649dfe5f699079c54b83e47d1ae19cfd" + integrity sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w== + +is-weakref@^1.0.2, is-weakref@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/is-weakref/-/is-weakref-1.1.1.tgz#eea430182be8d64174bd96bffbc46f21bf3f9293" + integrity sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew== + dependencies: + call-bound "^1.0.3" + +is-weakset@^2.0.3: + version "2.0.4" + resolved "https://registry.yarnpkg.com/is-weakset/-/is-weakset-2.0.4.tgz#c9f5deb0bc1906c6d6f1027f284ddf459249daca" + integrity sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ== + dependencies: + call-bound "^1.0.3" + get-intrinsic "^1.2.6" + is-windows@^1.0.1: version "1.0.2" resolved "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz" integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA== +isarray@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723" + integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw== + isarray@~1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz" @@ -6797,6 +7289,16 @@ jest-config@^29.7.0: slash "^3.0.0" strip-json-comments "^3.1.1" +jest-diff@30.0.5: + version "30.0.5" + resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-30.0.5.tgz#b40f81e0c0d13e5b81c4d62b0d0dfa6a524ee0fd" + integrity sha512-1UIqE9PoEKaHcIKvq2vbibrCog4Y8G0zmOxgQUVEiTqwR5hJVMCoDsN1vFvI5JvwD37hjueZ1C4l2FyGnfpE0A== + dependencies: + "@jest/diff-sequences" "30.0.1" + "@jest/get-type" "30.0.1" + chalk "^4.1.2" + pretty-format "30.0.5" + jest-diff@^28.1.3: version "28.1.3" resolved "https://registry.npmjs.org/jest-diff/-/jest-diff-28.1.3.tgz" @@ -7106,6 +7608,16 @@ jest-leak-detector@^29.7.0: jest-get-type "^29.6.3" pretty-format "^29.7.0" +jest-matcher-utils@30.0.5: + version "30.0.5" + resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-30.0.5.tgz#dff3334be58faea4a5e1becc228656fbbfc2467d" + integrity sha512-uQgGWt7GOrRLP1P7IwNWwK1WAQbq+m//ZY0yXygyfWp0rJlksMSLQAA4wYQC3b6wl3zfnchyTx+k3HZ5aPtCbQ== + dependencies: + "@jest/get-type" "30.0.1" + chalk "^4.1.2" + jest-diff "30.0.5" + pretty-format "30.0.5" + jest-matcher-utils@^28.1.3: version "28.1.3" resolved "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-28.1.3.tgz" @@ -7146,6 +7658,21 @@ jest-matcher-utils@^29.7.0: jest-get-type "^29.6.3" pretty-format "^29.7.0" +jest-message-util@30.0.5: + version "30.0.5" + resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-30.0.5.tgz#dd12ffec91dd3fa6a59cbd538a513d8e239e070c" + integrity sha512-NAiDOhsK3V7RU0Aa/HnrQo+E4JlbarbmI3q6Pi4KcxicdtjV82gcIUrejOtczChtVQR4kddu1E1EJlW6EN9IyA== + dependencies: + "@babel/code-frame" "^7.27.1" + "@jest/types" "30.0.5" + "@types/stack-utils" "^2.0.3" + chalk "^4.1.2" + graceful-fs "^4.2.11" + micromatch "^4.0.8" + pretty-format "30.0.5" + slash "^3.0.0" + stack-utils "^2.0.6" + jest-message-util@^28.1.3: version "28.1.3" resolved "https://registry.npmjs.org/jest-message-util/-/jest-message-util-28.1.3.tgz" @@ -7206,6 +7733,15 @@ jest-message-util@^29.7.0: slash "^3.0.0" stack-utils "^2.0.3" +jest-mock@30.0.5: + version "30.0.5" + resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-30.0.5.tgz#ef437e89212560dd395198115550085038570bdd" + integrity sha512-Od7TyasAAQX/6S+QCbN6vZoWOMwlTtzzGuxJku1GhGanAjz9y+QsQkpScDmETvdc9aSXyJ/Op4rhpMYBWW91wQ== + dependencies: + "@jest/types" "30.0.5" + "@types/node" "*" + jest-util "30.0.5" + jest-mock@^28.1.3: version "28.1.3" resolved "https://registry.npmjs.org/jest-mock/-/jest-mock-28.1.3.tgz" @@ -7237,6 +7773,11 @@ jest-pnp-resolver@^1.2.2: resolved "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.2.tgz" integrity sha512-olV41bKSMm8BdnuMsewT4jqlZ8+3TCARAXjZGT9jcoSnrfUnRCqnMoF9XEeoWjbzObpqF9dRhHQj0Xb9QdF6/w== +jest-regex-util@30.0.1: + version "30.0.1" + resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-30.0.1.tgz#f17c1de3958b67dfe485354f5a10093298f2a49b" + integrity sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA== + jest-regex-util@^28.0.2: version "28.0.2" resolved "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-28.0.2.tgz" @@ -7683,6 +8224,18 @@ jest-snapshot@^29.7.0: pretty-format "^29.7.0" semver "^7.5.3" +jest-util@30.0.5: + version "30.0.5" + resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-30.0.5.tgz#035d380c660ad5f1748dff71c4105338e05f8669" + integrity sha512-pvyPWssDZR0FlfMxCBoc0tvM8iUEskaRFALUtGQYzVEAqisAztmy+R8LnU14KT4XA0H/a5HMVTXat1jLne010g== + dependencies: + "@jest/types" "30.0.5" + "@types/node" "*" + chalk "^4.1.2" + ci-info "^4.2.0" + graceful-fs "^4.2.11" + picomatch "^4.0.2" + jest-util@^28.0.0, jest-util@^28.1.3: version "28.1.3" resolved "https://registry.npmjs.org/jest-util/-/jest-util-28.1.3.tgz" @@ -8327,7 +8880,7 @@ merge@^2.1.1: resolved "https://registry.npmjs.org/merge/-/merge-2.1.1.tgz" integrity sha512-jz+Cfrg9GWOZbQAnDQ4hlVnQky+341Yk5ru8bZSe6sIDTCIg8n9i/u7hSQGSVOF3C7lH6mGtqjkiT9G4wFLL0w== -micromatch@^4.0.2, micromatch@^4.0.4, micromatch@^4.0.5: +micromatch@^4.0.2, micromatch@^4.0.4, micromatch@^4.0.5, micromatch@^4.0.8: version "4.0.8" resolved "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz" integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA== @@ -8620,11 +9173,28 @@ object-assign@^4, object-assign@^4.0.1, object-assign@^4.1.1: resolved "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz" integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== -object-inspect@^1.13.3: +object-inspect@^1.13.3, object-inspect@^1.13.4: version "1.13.4" resolved "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz" integrity sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew== +object-keys@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" + integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== + +object.assign@^4.1.7: + version "4.1.7" + resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.7.tgz#8c14ca1a424c6a561b0bb2a22f66f5049a945d3d" + integrity sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw== + dependencies: + call-bind "^1.0.8" + call-bound "^1.0.3" + define-properties "^1.2.1" + es-object-atoms "^1.0.0" + has-symbols "^1.1.0" + object-keys "^1.1.1" + on-finished@^2.4.1: version "2.4.1" resolved "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz" @@ -8695,6 +9265,15 @@ ora@^7.0.1: string-width "^6.1.0" strip-ansi "^7.1.0" +own-keys@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/own-keys/-/own-keys-1.0.1.tgz#e4006910a2bf913585289676eebd6f390cf51358" + integrity sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg== + dependencies: + get-intrinsic "^1.2.6" + object-keys "^1.1.1" + safe-push-apply "^1.0.0" + p-limit@^2.0.0, p-limit@^2.2.0: version "2.3.0" resolved "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz" @@ -8949,6 +9528,11 @@ pkg-dir@^4.2.0: dependencies: find-up "^4.0.0" +possible-typed-array-names@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz#93e3582bc0e5426586d9d07b79ee40fc841de4ae" + integrity sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg== + postcss-load-config@^6.0.1: version "6.0.1" resolved "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz" @@ -8975,6 +9559,15 @@ prettier@^3.3.3: resolved "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz" integrity sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew== +pretty-format@30.0.5, pretty-format@^30.0.0: + version "30.0.5" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-30.0.5.tgz#e001649d472800396c1209684483e18a4d250360" + integrity sha512-D1tKtYvByrBkFLe2wHJl2bwMJIiT8rW+XA+TiataH79/FszLQMrpGEvzUVkzPau7OCO0Qnrhpe87PqtOAIB8Yw== + dependencies: + "@jest/schemas" "30.0.5" + ansi-styles "^5.2.0" + react-is "^18.3.1" + pretty-format@^24: version "24.9.0" resolved "https://registry.npmjs.org/pretty-format/-/pretty-format-24.9.0.tgz" @@ -9202,7 +9795,7 @@ react-is@^17.0.1: resolved "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz" integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== -react-is@^18.0.0: +react-is@^18.0.0, react-is@^18.3.1: version "18.3.1" resolved "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz" integrity sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg== @@ -9251,6 +9844,20 @@ readline@^1.3.0: resolved "https://registry.npmjs.org/readline/-/readline-1.3.0.tgz" integrity sha512-k2d6ACCkiNYz222Fs/iNze30rRJ1iIicW7JuX/7/cozvih6YCkFZH+J6mAFDVgv0dRBaAyr4jDqC95R2y4IADg== +reflect.getprototypeof@^1.0.6, reflect.getprototypeof@^1.0.9: + version "1.0.10" + resolved "https://registry.yarnpkg.com/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz#c629219e78a3316d8b604c765ef68996964e7bf9" + integrity sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw== + dependencies: + call-bind "^1.0.8" + define-properties "^1.2.1" + es-abstract "^1.23.9" + es-errors "^1.3.0" + es-object-atoms "^1.0.0" + get-intrinsic "^1.2.7" + get-proto "^1.0.1" + which-builtin-type "^1.2.1" + regenerate-unicode-properties@^10.2.0: version "10.2.0" resolved "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.0.tgz" @@ -9280,6 +9887,30 @@ regenerator-transform@^0.15.2: dependencies: "@babel/runtime" "^7.8.4" +regexp.escape@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/regexp.escape/-/regexp.escape-2.0.1.tgz#09e4beef9d202dbd739868f3818223f977cf91da" + integrity sha512-JItRb4rmyTzmERBkAf6J87LjDPy/RscIwmaJQ3gsFlAzrmZbZU8LwBw5IydFZXW9hqpgbPlGbMhtpqtuAhMgtg== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.3" + es-errors "^1.3.0" + for-each "^0.3.3" + safe-regex-test "^1.0.3" + +regexp.prototype.flags@^1.5.4: + version "1.5.4" + resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz#1ad6c62d44a259007e55b3970e00f746efbcaa19" + integrity sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA== + dependencies: + call-bind "^1.0.8" + define-properties "^1.2.1" + es-errors "^1.3.0" + get-proto "^1.0.1" + gopd "^1.2.0" + set-function-name "^2.0.2" + regexpu-core@^6.2.0: version "6.2.0" resolved "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.2.0.tgz" @@ -9457,6 +10088,17 @@ rxjs@^7.0.0, rxjs@^7.8.1: dependencies: tslib "^2.1.0" +safe-array-concat@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/safe-array-concat/-/safe-array-concat-1.1.3.tgz#c9e54ec4f603b0bbb8e7e5007a5ee7aecd1538c3" + integrity sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q== + dependencies: + call-bind "^1.0.8" + call-bound "^1.0.2" + get-intrinsic "^1.2.6" + has-symbols "^1.1.0" + isarray "^2.0.5" + safe-buffer@5.2.1, safe-buffer@^5.1.0, safe-buffer@~5.2.0: version "5.2.1" resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz" @@ -9467,6 +10109,23 @@ safe-buffer@~5.1.0, safe-buffer@~5.1.1: resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz" integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== +safe-push-apply@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/safe-push-apply/-/safe-push-apply-1.0.0.tgz#01850e981c1602d398c85081f360e4e6d03d27f5" + integrity sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA== + dependencies: + es-errors "^1.3.0" + isarray "^2.0.5" + +safe-regex-test@^1.0.3, safe-regex-test@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/safe-regex-test/-/safe-regex-test-1.1.0.tgz#7f87dfb67a3150782eaaf18583ff5d1711ac10c1" + integrity sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw== + dependencies: + call-bound "^1.0.2" + es-errors "^1.3.0" + is-regex "^1.2.1" + safe-stable-stringify@^2.3.1: version "2.5.0" resolved "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz" @@ -9576,6 +10235,37 @@ set-blocking@^2.0.0: resolved "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz" integrity sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw== +set-function-length@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449" + integrity sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg== + dependencies: + define-data-property "^1.1.4" + es-errors "^1.3.0" + function-bind "^1.1.2" + get-intrinsic "^1.2.4" + gopd "^1.0.1" + has-property-descriptors "^1.0.2" + +set-function-name@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/set-function-name/-/set-function-name-2.0.2.tgz#16a705c5a0dc2f5e638ca96d8a8cd4e1c2b90985" + integrity sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ== + dependencies: + define-data-property "^1.1.4" + es-errors "^1.3.0" + functions-have-names "^1.2.3" + has-property-descriptors "^1.0.2" + +set-proto@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/set-proto/-/set-proto-1.0.0.tgz#0760dbcff30b2d7e801fd6e19983e56da337565e" + integrity sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw== + dependencies: + dunder-proto "^1.0.1" + es-errors "^1.3.0" + es-object-atoms "^1.0.0" + setimmediate@^1.0.5: version "1.0.5" resolved "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz" @@ -9759,6 +10449,13 @@ stack-utils@^2.0.3: dependencies: escape-string-regexp "^2.0.0" +stack-utils@^2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-2.0.6.tgz#aaf0748169c02fc33c8232abccf933f54a1cc34f" + integrity sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ== + dependencies: + escape-string-regexp "^2.0.0" + statuses@2.0.1, statuses@^2.0.1: version "2.0.1" resolved "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz" @@ -9771,6 +10468,14 @@ stdin-discarder@^0.1.0: dependencies: bl "^5.0.0" +stop-iteration-iterator@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz#f481ff70a548f6124d0312c3aa14cbfa7aa542ad" + integrity sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ== + dependencies: + es-errors "^1.3.0" + internal-slot "^1.1.0" + streamx@^2.15.0, streamx@^2.21.0: version "2.22.0" resolved "https://registry.npmjs.org/streamx/-/streamx-2.22.0.tgz" @@ -9825,6 +10530,38 @@ string-width@^6.1.0: emoji-regex "^10.2.1" strip-ansi "^7.0.1" +string.prototype.trim@^1.2.10: + version "1.2.10" + resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz#40b2dd5ee94c959b4dcfb1d65ce72e90da480c81" + integrity sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA== + dependencies: + call-bind "^1.0.8" + call-bound "^1.0.2" + define-data-property "^1.1.4" + define-properties "^1.2.1" + es-abstract "^1.23.5" + es-object-atoms "^1.0.0" + has-property-descriptors "^1.0.2" + +string.prototype.trimend@^1.0.9: + version "1.0.9" + resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz#62e2731272cd285041b36596054e9f66569b6942" + integrity sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ== + dependencies: + call-bind "^1.0.8" + call-bound "^1.0.2" + define-properties "^1.2.1" + es-object-atoms "^1.0.0" + +string.prototype.trimstart@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz#7ee834dda8c7c17eff3118472bb35bfedaa34dde" + integrity sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-object-atoms "^1.0.0" + string_decoder@^1.1.1: version "1.3.0" resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz" @@ -10232,6 +10969,51 @@ type-is@^2.0.0, type-is@^2.0.1: media-typer "^1.1.0" mime-types "^3.0.0" +typed-array-buffer@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz#a72395450a4869ec033fd549371b47af3a2ee536" + integrity sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw== + dependencies: + call-bound "^1.0.3" + es-errors "^1.3.0" + is-typed-array "^1.1.14" + +typed-array-byte-length@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz#8407a04f7d78684f3d252aa1a143d2b77b4160ce" + integrity sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg== + dependencies: + call-bind "^1.0.8" + for-each "^0.3.3" + gopd "^1.2.0" + has-proto "^1.2.0" + is-typed-array "^1.1.14" + +typed-array-byte-offset@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz#ae3698b8ec91a8ab945016108aef00d5bff12355" + integrity sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ== + dependencies: + available-typed-arrays "^1.0.7" + call-bind "^1.0.8" + for-each "^0.3.3" + gopd "^1.2.0" + has-proto "^1.2.0" + is-typed-array "^1.1.15" + reflect.getprototypeof "^1.0.9" + +typed-array-length@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/typed-array-length/-/typed-array-length-1.0.7.tgz#ee4deff984b64be1e118b0de8c9c877d5ce73d3d" + integrity sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg== + dependencies: + call-bind "^1.0.7" + for-each "^0.3.3" + gopd "^1.0.1" + is-typed-array "^1.1.13" + possible-typed-array-names "^1.0.0" + reflect.getprototypeof "^1.0.6" + typed-query-selector@^2.12.0: version "2.12.0" resolved "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.0.tgz" @@ -10251,6 +11033,16 @@ typescript@^5.4.3: resolved "https://registry.npmjs.org/typescript/-/typescript-5.4.3.tgz" integrity sha512-KrPd3PKaCLr78MalgiwJnA25Nm8HAmdwN3mYUYZgG/wizIo9EainNVQI9/yDavtVFRN2h3k8uf3GLHuhDMgEHg== +unbox-primitive@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.1.0.tgz#8d9d2c9edeea8460c7f35033a88867944934d1e2" + integrity sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw== + dependencies: + call-bound "^1.0.3" + has-bigints "^1.0.2" + has-symbols "^1.1.0" + which-boxed-primitive "^1.1.1" + undici-types@~6.19.2: version "6.19.8" resolved "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz" @@ -10460,11 +11252,64 @@ whatwg-url@^7.0.0: tr46 "^1.0.1" webidl-conversions "^4.0.2" +which-boxed-primitive@^1.1.0, which-boxed-primitive@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz#d76ec27df7fa165f18d5808374a5fe23c29b176e" + integrity sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA== + dependencies: + is-bigint "^1.1.0" + is-boolean-object "^1.2.1" + is-number-object "^1.1.1" + is-string "^1.1.1" + is-symbol "^1.1.1" + +which-builtin-type@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/which-builtin-type/-/which-builtin-type-1.2.1.tgz#89183da1b4907ab089a6b02029cc5d8d6574270e" + integrity sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q== + dependencies: + call-bound "^1.0.2" + function.prototype.name "^1.1.6" + has-tostringtag "^1.0.2" + is-async-function "^2.0.0" + is-date-object "^1.1.0" + is-finalizationregistry "^1.1.0" + is-generator-function "^1.0.10" + is-regex "^1.2.1" + is-weakref "^1.0.2" + isarray "^2.0.5" + which-boxed-primitive "^1.1.0" + which-collection "^1.0.2" + which-typed-array "^1.1.16" + +which-collection@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/which-collection/-/which-collection-1.0.2.tgz#627ef76243920a107e7ce8e96191debe4b16c2a0" + integrity sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw== + dependencies: + is-map "^2.0.3" + is-set "^2.0.3" + is-weakmap "^2.0.2" + is-weakset "^2.0.3" + which-module@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz" integrity sha512-B+enWhmw6cjfVC7kS8Pj9pCrKSc5txArRyaYGe088shv/FGWH+0Rjx/xPgtsWfsUtS27FkP697E4DDhgrgoc0Q== +which-typed-array@^1.1.16, which-typed-array@^1.1.19: + version "1.1.19" + resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.19.tgz#df03842e870b6b88e117524a4b364b6fc689f956" + integrity sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw== + dependencies: + available-typed-arrays "^1.0.7" + call-bind "^1.0.8" + call-bound "^1.0.4" + for-each "^0.3.5" + get-proto "^1.0.1" + gopd "^1.2.0" + has-tostringtag "^1.0.2" + which@^1.2.10, which@^1.2.14: version "1.3.1" resolved "https://registry.npmjs.org/which/-/which-1.3.1.tgz" diff --git a/fixtures/dom/src/components/fixtures/fragment-refs/FocusCase.js b/fixtures/dom/src/components/fixtures/fragment-refs/FocusCase.js index baff30895c0e0..efd9157b4a5ce 100644 --- a/fixtures/dom/src/components/fixtures/fragment-refs/FocusCase.js +++ b/fixtures/dom/src/components/fixtures/fragment-refs/FocusCase.js @@ -3,7 +3,7 @@ import Fixture from '../../Fixture'; const React = window.React; -const {Fragment, useEffect, useRef, useState} = React; +const {Fragment, useRef} = React; export default function FocusCase() { const fragmentRef = useRef(null); diff --git a/fixtures/dom/src/components/fixtures/fragment-refs/GetClientRectsCase.js b/fixtures/dom/src/components/fixtures/fragment-refs/GetClientRectsCase.js index 7b20a0a2e0d67..563f2ad054294 100644 --- a/fixtures/dom/src/components/fixtures/fragment-refs/GetClientRectsCase.js +++ b/fixtures/dom/src/components/fixtures/fragment-refs/GetClientRectsCase.js @@ -2,7 +2,7 @@ import TestCase from '../../TestCase'; import Fixture from '../../Fixture'; const React = window.React; -const {Fragment, useEffect, useRef, useState} = React; +const {Fragment, useRef, useState} = React; export default function GetClientRectsCase() { const fragmentRef = useRef(null); diff --git a/fixtures/dom/src/components/fixtures/fragment-refs/ScrollIntoViewCase.js b/fixtures/dom/src/components/fixtures/fragment-refs/ScrollIntoViewCase.js new file mode 100644 index 0000000000000..3b1f21ef686aa --- /dev/null +++ b/fixtures/dom/src/components/fixtures/fragment-refs/ScrollIntoViewCase.js @@ -0,0 +1,184 @@ +import TestCase from '../../TestCase'; +import Fixture from '../../Fixture'; +import ScrollIntoViewCaseComplex from './ScrollIntoViewCaseComplex'; +import ScrollIntoViewCaseSimple from './ScrollIntoViewCaseSimple'; +import ScrollIntoViewTargetElement from './ScrollIntoViewTargetElement'; + +const React = window.React; +const {Fragment, useRef, useState, useEffect} = React; +const ReactDOM = window.ReactDOM; + +function Controls({ + alignToTop, + setAlignToTop, + scrollVertical, + exampleType, + setExampleType, +}) { + return ( +
+ +
+ +
+
+ +
+
+ ); +} + +export default function ScrollIntoViewCase() { + const [exampleType, setExampleType] = useState('simple'); + const [alignToTop, setAlignToTop] = useState(true); + const [caseInViewport, setCaseInViewport] = useState(false); + const fragmentRef = useRef(null); + const testCaseRef = useRef(null); + const noChildRef = useRef(null); + const scrollContainerRef = useRef(null); + + const scrollVertical = () => { + fragmentRef.current.experimental_scrollIntoView(alignToTop); + }; + + const scrollVerticalNoChildren = () => { + noChildRef.current.experimental_scrollIntoView(alignToTop); + }; + + useEffect(() => { + const observer = new IntersectionObserver(entries => { + entries.forEach(entry => { + if (entry.isIntersecting) { + setCaseInViewport(true); + } else { + setCaseInViewport(false); + } + }); + }); + testCaseRef.current.observeUsing(observer); + + const lastRef = testCaseRef.current; + return () => { + lastRef.unobserveUsing(observer); + observer.disconnect(); + }; + }); + + return ( + + + +
  • Toggle alignToTop and click the buttons to scroll
  • +
    + +

    When the Fragment has children:

    +

    + In order to handle the case where children are split between + multiple scroll containers, we call scrollIntoView on each child in + reverse order. +

    +

    When the Fragment does not have children:

    +

    + The Fragment still represents a virtual space. We can scroll to the + nearest edge by selecting the host sibling before if + alignToTop=false, or after if alignToTop=true|undefined. We'll fall + back to the other sibling or parent in the case that the preferred + sibling target doesn't exist. +

    +
    + + + + + {exampleType === 'simple' && ( + + + + )} + {exampleType === 'horizontal' && ( +
    + + + +
    + )} + {exampleType === 'multiple' && ( + +
    + + + + + )} + {exampleType === 'empty' && ( + + + + + + )} + + + + + + + ); +} diff --git a/fixtures/dom/src/components/fixtures/fragment-refs/ScrollIntoViewCaseComplex.js b/fixtures/dom/src/components/fixtures/fragment-refs/ScrollIntoViewCaseComplex.js new file mode 100644 index 0000000000000..a0ea612d09c40 --- /dev/null +++ b/fixtures/dom/src/components/fixtures/fragment-refs/ScrollIntoViewCaseComplex.js @@ -0,0 +1,50 @@ +import ScrollIntoViewTargetElement from './ScrollIntoViewTargetElement'; + +const React = window.React; +const {Fragment, useRef, useState, useEffect} = React; +const ReactDOM = window.ReactDOM; + +export default function ScrollIntoViewCaseComplex({ + caseInViewport, + scrollContainerRef, +}) { + const [didMount, setDidMount] = useState(false); + // Hack to portal child into the scroll container + // after the first render. This is to simulate a case where + // an item is portaled into another scroll container. + useEffect(() => { + if (!didMount) { + setDidMount(true); + } + }, []); + return ( + + {caseInViewport && ( + + )} + {didMount && + ReactDOM.createPortal( + , + scrollContainerRef.current + )} + + + + {caseInViewport && ( + + )} + + ); +} diff --git a/fixtures/dom/src/components/fixtures/fragment-refs/ScrollIntoViewCaseSimple.js b/fixtures/dom/src/components/fixtures/fragment-refs/ScrollIntoViewCaseSimple.js new file mode 100644 index 0000000000000..ee61cd16290f9 --- /dev/null +++ b/fixtures/dom/src/components/fixtures/fragment-refs/ScrollIntoViewCaseSimple.js @@ -0,0 +1,14 @@ +import ScrollIntoViewTargetElement from './ScrollIntoViewTargetElement'; + +const React = window.React; +const {Fragment} = React; + +export default function ScrollIntoViewCaseSimple() { + return ( + + + + + + ); +} diff --git a/fixtures/dom/src/components/fixtures/fragment-refs/ScrollIntoViewTargetElement.js b/fixtures/dom/src/components/fixtures/fragment-refs/ScrollIntoViewTargetElement.js new file mode 100644 index 0000000000000..f61668c5cf525 --- /dev/null +++ b/fixtures/dom/src/components/fixtures/fragment-refs/ScrollIntoViewTargetElement.js @@ -0,0 +1,18 @@ +const React = window.React; + +export default function ScrollIntoViewTargetElement({color, id, top}) { + return ( +
    + {id} +
    + ); +} diff --git a/fixtures/dom/src/components/fixtures/fragment-refs/index.js b/fixtures/dom/src/components/fixtures/fragment-refs/index.js index 23b440938cf7a..c560b59fbec6a 100644 --- a/fixtures/dom/src/components/fixtures/fragment-refs/index.js +++ b/fixtures/dom/src/components/fixtures/fragment-refs/index.js @@ -5,6 +5,7 @@ import IntersectionObserverCase from './IntersectionObserverCase'; import ResizeObserverCase from './ResizeObserverCase'; import FocusCase from './FocusCase'; import GetClientRectsCase from './GetClientRectsCase'; +import ScrollIntoViewCase from './ScrollIntoViewCase'; const React = window.React; @@ -17,6 +18,7 @@ export default function FragmentRefsPage() { + ); } diff --git a/fixtures/dom/src/index.js b/fixtures/dom/src/index.js index 7a23ba2acf944..a334311be0c4b 100644 --- a/fixtures/dom/src/index.js +++ b/fixtures/dom/src/index.js @@ -2,14 +2,23 @@ import './polyfills'; import loadReact, {isLocal} from './react-loader'; if (isLocal()) { - Promise.all([import('react'), import('react-dom/client')]) - .then(([React, ReactDOMClient]) => { - if (React === undefined || ReactDOMClient === undefined) { + Promise.all([ + import('react'), + import('react-dom'), + import('react-dom/client'), + ]) + .then(([React, ReactDOM, ReactDOMClient]) => { + if ( + React === undefined || + ReactDOM === undefined || + ReactDOMClient === undefined + ) { throw new Error( 'Unable to load React. Build experimental and then run `yarn dev` again' ); } window.React = React; + window.ReactDOM = ReactDOM; window.ReactDOMClient = ReactDOMClient; }) .then(() => import('./components/App')) diff --git a/fixtures/eslint-v9/eslint.config.ts b/fixtures/eslint-v9/eslint.config.ts index 62ef68671639e..66d2c087468b2 100644 --- a/fixtures/eslint-v9/eslint.config.ts +++ b/fixtures/eslint-v9/eslint.config.ts @@ -1,7 +1,9 @@ -import type {Linter} from 'eslint'; -import * as reactHooks from 'eslint-plugin-react-hooks'; +import {defineConfig} from 'eslint/config'; +import reactHooks from 'eslint-plugin-react-hooks'; -export default [ +console.log(reactHooks.configs['recommended-latest']); + +export default defineConfig([ { languageOptions: { ecmaVersion: 'latest', @@ -12,11 +14,12 @@ export default [ }, }, }, - }, - reactHooks.configs['recommended'], - { + plugins: { + 'react-hooks': reactHooks, + }, + extends: ['react-hooks/recommended-latest'], rules: { 'react-hooks/exhaustive-deps': 'error', }, }, -] satisfies Linter.Config[]; +]); diff --git a/fixtures/eslint-v9/package.json b/fixtures/eslint-v9/package.json index 80827a0d1730a..c09d7ec99bd8a 100644 --- a/fixtures/eslint-v9/package.json +++ b/fixtures/eslint-v9/package.json @@ -2,7 +2,7 @@ "private": true, "name": "eslint-v9", "dependencies": { - "eslint": "^9.18.0", + "eslint": "^9.33.0", "eslint-plugin-react-hooks": "link:../../build/oss-stable/eslint-plugin-react-hooks", "jiti": "^2.4.2" }, diff --git a/fixtures/eslint-v9/yarn.lock b/fixtures/eslint-v9/yarn.lock index 630bf074a30d0..a471aadd964cf 100644 --- a/fixtures/eslint-v9/yarn.lock +++ b/fixtures/eslint-v9/yarn.lock @@ -221,26 +221,31 @@ resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.12.1.tgz#cfc6cffe39df390a3841cde2abccf92eaa7ae0e0" integrity sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ== -"@eslint/config-array@^0.19.2": - version "0.19.2" - resolved "https://registry.yarnpkg.com/@eslint/config-array/-/config-array-0.19.2.tgz#3060b809e111abfc97adb0bb1172778b90cb46aa" - integrity sha512-GNKqxfHG2ySmJOBSHg7LxeUx4xpuCoFjacmlCoYWEbaPXLwvfIjixRI12xCQZeULksQb23uiA8F40w5TojpV7w== +"@eslint/config-array@^0.21.0": + version "0.21.0" + resolved "https://registry.yarnpkg.com/@eslint/config-array/-/config-array-0.21.0.tgz#abdbcbd16b124c638081766392a4d6b509f72636" + integrity sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ== dependencies: "@eslint/object-schema" "^2.1.6" debug "^4.3.1" minimatch "^3.1.2" -"@eslint/core@^0.12.0": - version "0.12.0" - resolved "https://registry.yarnpkg.com/@eslint/core/-/core-0.12.0.tgz#5f960c3d57728be9f6c65bd84aa6aa613078798e" - integrity sha512-cmrR6pytBuSMTaBweKoGMwu3EiHiEC+DoyupPmlZ0HxBJBtIxwe+j/E4XPIKNx+Q74c8lXKPwYawBf5glsTkHg== +"@eslint/config-helpers@^0.3.1": + version "0.3.1" + resolved "https://registry.yarnpkg.com/@eslint/config-helpers/-/config-helpers-0.3.1.tgz#d316e47905bd0a1a931fa50e669b9af4104d1617" + integrity sha512-xR93k9WhrDYpXHORXpxVL5oHj3Era7wo6k/Wd8/IsQNnZUTzkGS29lyn3nAT05v6ltUuTFVCCYDEGfy2Or/sPA== + +"@eslint/core@^0.15.2": + version "0.15.2" + resolved "https://registry.yarnpkg.com/@eslint/core/-/core-0.15.2.tgz#59386327d7862cc3603ebc7c78159d2dcc4a868f" + integrity sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg== dependencies: "@types/json-schema" "^7.0.15" -"@eslint/eslintrc@^3.3.0": - version "3.3.0" - resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-3.3.0.tgz#96a558f45842989cca7ea1ecd785ad5491193846" - integrity sha512-yaVPAiNAalnCZedKLdR21GOGILMLKPyqSLWaAjQFvYA2i/ciDi8ArYVr69Anohb6cH2Ukhqti4aFnYyPm8wdwQ== +"@eslint/eslintrc@^3.3.1": + version "3.3.1" + resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-3.3.1.tgz#e55f7f1dd400600dd066dbba349c4c0bac916964" + integrity sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ== dependencies: ajv "^6.12.4" debug "^4.3.2" @@ -252,22 +257,22 @@ minimatch "^3.1.2" strip-json-comments "^3.1.1" -"@eslint/js@9.21.0": - version "9.21.0" - resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.21.0.tgz#4303ef4e07226d87c395b8fad5278763e9c15c08" - integrity sha512-BqStZ3HX8Yz6LvsF5ByXYrtigrV5AXADWLAGc7PH/1SxOb7/FIYYMszZZWiUou/GB9P2lXWk2SV4d+Z8h0nknw== +"@eslint/js@9.33.0": + version "9.33.0" + resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.33.0.tgz#475c92fdddab59b8b8cab960e3de2564a44bf368" + integrity sha512-5K1/mKhWaMfreBGJTwval43JJmkip0RmM+3+IuqupeSKNC/Th2Kc7ucaq5ovTSra/OOKB9c58CGSz3QMVbWt0A== "@eslint/object-schema@^2.1.6": version "2.1.6" resolved "https://registry.yarnpkg.com/@eslint/object-schema/-/object-schema-2.1.6.tgz#58369ab5b5b3ca117880c0f6c0b0f32f6950f24f" integrity sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA== -"@eslint/plugin-kit@^0.2.7": - version "0.2.7" - resolved "https://registry.yarnpkg.com/@eslint/plugin-kit/-/plugin-kit-0.2.7.tgz#9901d52c136fb8f375906a73dcc382646c3b6a27" - integrity sha512-JubJ5B2pJ4k4yGxaNLdbjrnk9d/iDz6/q8wOilpIowd6PJPgaxCuHBnBszq7Ce2TyMrywm5r4PnKm6V3iiZF+g== +"@eslint/plugin-kit@^0.3.5": + version "0.3.5" + resolved "https://registry.yarnpkg.com/@eslint/plugin-kit/-/plugin-kit-0.3.5.tgz#fd8764f0ee79c8ddab4da65460c641cefee017c5" + integrity sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w== dependencies: - "@eslint/core" "^0.12.0" + "@eslint/core" "^0.15.2" levn "^0.4.1" "@humanfs/core@^0.19.1": @@ -350,6 +355,11 @@ acorn@^8.14.0: resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.14.0.tgz#063e2c70cac5fb4f6467f0b11152e04c682795b0" integrity sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA== +acorn@^8.15.0: + version "8.15.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.15.0.tgz#a360898bc415edaac46c8241f6383975b930b816" + integrity sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg== + ajv@^6.12.4: version "6.12.6" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" @@ -475,10 +485,10 @@ escape-string-regexp@^4.0.0: version "0.0.0" uid "" -eslint-scope@^8.2.0: - version "8.2.0" - resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-8.2.0.tgz#377aa6f1cb5dc7592cfd0b7f892fd0cf352ce442" - integrity sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A== +eslint-scope@^8.4.0: + version "8.4.0" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-8.4.0.tgz#88e646a207fad61436ffa39eb505147200655c82" + integrity sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg== dependencies: esrecurse "^4.3.0" estraverse "^5.2.0" @@ -493,18 +503,24 @@ eslint-visitor-keys@^4.2.0: resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz#687bacb2af884fcdda8a6e7d65c606f46a14cd45" integrity sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw== -eslint@^9.18.0: - version "9.21.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-9.21.0.tgz#b1c9c16f5153ff219791f627b94ab8f11f811591" - integrity sha512-KjeihdFqTPhOMXTt7StsDxriV4n66ueuF/jfPNC3j/lduHwr/ijDwJMsF+wyMJethgiKi5wniIE243vi07d3pg== +eslint-visitor-keys@^4.2.1: + version "4.2.1" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz#4cfea60fe7dd0ad8e816e1ed026c1d5251b512c1" + integrity sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ== + +eslint@^9.33.0: + version "9.33.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-9.33.0.tgz#cc186b3d9eb0e914539953d6a178a5b413997b73" + integrity sha512-TS9bTNIryDzStCpJN93aC5VRSW3uTx9sClUn4B87pwiCaJh220otoI0X8mJKr+VcPtniMdN8GKjlwgWGUv5ZKA== dependencies: "@eslint-community/eslint-utils" "^4.2.0" "@eslint-community/regexpp" "^4.12.1" - "@eslint/config-array" "^0.19.2" - "@eslint/core" "^0.12.0" - "@eslint/eslintrc" "^3.3.0" - "@eslint/js" "9.21.0" - "@eslint/plugin-kit" "^0.2.7" + "@eslint/config-array" "^0.21.0" + "@eslint/config-helpers" "^0.3.1" + "@eslint/core" "^0.15.2" + "@eslint/eslintrc" "^3.3.1" + "@eslint/js" "9.33.0" + "@eslint/plugin-kit" "^0.3.5" "@humanfs/node" "^0.16.6" "@humanwhocodes/module-importer" "^1.0.1" "@humanwhocodes/retry" "^0.4.2" @@ -515,9 +531,9 @@ eslint@^9.18.0: cross-spawn "^7.0.6" debug "^4.3.2" escape-string-regexp "^4.0.0" - eslint-scope "^8.2.0" - eslint-visitor-keys "^4.2.0" - espree "^10.3.0" + eslint-scope "^8.4.0" + eslint-visitor-keys "^4.2.1" + espree "^10.4.0" esquery "^1.5.0" esutils "^2.0.2" fast-deep-equal "^3.1.3" @@ -533,7 +549,7 @@ eslint@^9.18.0: natural-compare "^1.4.0" optionator "^0.9.3" -espree@^10.0.1, espree@^10.3.0: +espree@^10.0.1: version "10.3.0" resolved "https://registry.yarnpkg.com/espree/-/espree-10.3.0.tgz#29267cf5b0cb98735b65e64ba07e0ed49d1eed8a" integrity sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg== @@ -542,6 +558,15 @@ espree@^10.0.1, espree@^10.3.0: acorn-jsx "^5.3.2" eslint-visitor-keys "^4.2.0" +espree@^10.4.0: + version "10.4.0" + resolved "https://registry.yarnpkg.com/espree/-/espree-10.4.0.tgz#d54f4949d4629005a1fa168d937c3ff1f7e2a837" + integrity sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ== + dependencies: + acorn "^8.15.0" + acorn-jsx "^5.3.2" + eslint-visitor-keys "^4.2.1" + esquery@^1.5.0: version "1.6.0" resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.6.0.tgz#91419234f804d852a82dceec3e16cdc22cf9dae7" diff --git a/fixtures/fizz/package.json b/fixtures/fizz/package.json index 096a392f4b524..7156aa9e8fa0c 100644 --- a/fixtures/fizz/package.json +++ b/fixtures/fizz/package.json @@ -28,8 +28,8 @@ "prettier": "1.19.1" }, "scripts": { - "predev": "cp -r ../../build/oss-experimental/* ./node_modules/", - "prestart": "cp -r ../../build/oss-experimental/* ./node_modules/", + "predev": "cp -r ../../build/oss-experimental/* ./node_modules/ && rm -rf node_modules/.cache;", + "prestart": "cp -r ../../build/oss-experimental/* ./node_modules/ && rm -rf node_modules/.cache;", "dev": "concurrently \"npm run dev:server\" \"npm run dev:bundler\"", "start": "concurrently \"npm run start:server\" \"npm run start:bundler\"", "dev:server": "cross-env NODE_ENV=development nodemon -- --inspect server/server.js", diff --git a/fixtures/flight-esm/package.json b/fixtures/flight-esm/package.json index cb4ca1ea30b82..de711927588ca 100644 --- a/fixtures/flight-esm/package.json +++ b/fixtures/flight-esm/package.json @@ -17,8 +17,8 @@ "webpack-sources": "^3.2.0" }, "scripts": { - "predev": "cp -r ../../build/oss-experimental/* ./node_modules/", - "prestart": "cp -r ../../build/oss-experimental/* ./node_modules/", + "predev": "cp -r ../../build/oss-experimental/* ./node_modules/ && rm -rf node_modules/.cache;", + "prestart": "cp -r ../../build/oss-experimental/* ./node_modules/ && rm -rf node_modules/.cache;", "dev": "concurrently \"npm run dev:region\" \"npm run dev:global\"", "dev:global": "NODE_ENV=development BUILD_PATH=dist node server/global", "dev:region": "NODE_ENV=development BUILD_PATH=dist nodemon --watch src --watch dist -- --enable-source-maps --experimental-loader ./loader/region.js --conditions=react-server server/region", diff --git a/fixtures/flight/config/webpack.config.js b/fixtures/flight/config/webpack.config.js index 69be1859fdc6c..2aeede652b573 100644 --- a/fixtures/flight/config/webpack.config.js +++ b/fixtures/flight/config/webpack.config.js @@ -15,6 +15,7 @@ const TerserPlugin = require('terser-webpack-plugin'); const MiniCssExtractPlugin = require('mini-css-extract-plugin'); const CssMinimizerPlugin = require('css-minimizer-webpack-plugin'); const ModuleScopePlugin = require('react-dev-utils/ModuleScopePlugin'); +const DevToolsIgnorePlugin = require('devtools-ignore-webpack-plugin'); const getCSSModuleLocalIdent = require('react-dev-utils/getCSSModuleLocalIdent'); const paths = require('./paths'); const modules = require('./modules'); @@ -685,6 +686,15 @@ module.exports = function (webpackEnv) { }, }), // Fork Start + new DevToolsIgnorePlugin({ + shouldIgnorePath: function (path) { + return ( + path.includes('/node_modules/') || + path.includes('/webpack/') || + path.endsWith('/src/index.js') + ); + }, + }), new ReactFlightWebpackPlugin({ isServer: false, clientReferences: { diff --git a/fixtures/flight/package.json b/fixtures/flight/package.json index c63500727d02a..6018ffb0e4ff5 100644 --- a/fixtures/flight/package.json +++ b/fixtures/flight/package.json @@ -29,6 +29,7 @@ "concurrently": "^7.3.0", "css-loader": "^6.5.1", "css-minimizer-webpack-plugin": "^3.2.0", + "devtools-ignore-webpack-plugin": "^0.2.0", "dotenv": "^10.0.0", "dotenv-expand": "^5.1.0", "file-loader": "^6.2.0", @@ -69,12 +70,12 @@ "@playwright/test": "^1.51.1" }, "scripts": { - "predev": "cp -r ../../build/oss-experimental/* ./node_modules/", - "prebuild": "cp -r ../../build/oss-experimental/* ./node_modules/", - "dev": "concurrently \"npm run dev:region\" \"npm run dev:global\"", + "predev": "cp -r ../../build/oss-experimental/* ./node_modules/ && rm -rf node_modules/.cache;", + "prebuild": "cp -r ../../build/oss-experimental/* ./node_modules/ && rm -rf node_modules/.cache;", + "dev": "concurrently \"yarn run dev:region\" \"yarn run dev:global\"", "dev:global": "NODE_ENV=development BUILD_PATH=dist node --experimental-loader ./loader/global.js --inspect=127.0.0.1:9230 server/global", "dev:region": "NODE_ENV=development BUILD_PATH=dist nodemon --watch src --watch dist -- --enable-source-maps --experimental-loader ./loader/region.js --conditions=react-server --inspect=127.0.0.1:9229 server/region", - "start": "node scripts/build.js && concurrently \"npm run start:region\" \"npm run start:global\"", + "start": "node scripts/build.js && concurrently \"yarn run start:region\" \"yarn run start:global\"", "start:global": "NODE_ENV=production node --experimental-loader ./loader/global.js server/global", "start:region": "NODE_ENV=production node --experimental-loader ./loader/region.js --conditions=react-server server/region", "build": "node scripts/build.js", diff --git a/fixtures/flight/server/global.js b/fixtures/flight/server/global.js index a2fa737ae0f4d..f097378056a46 100644 --- a/fixtures/flight/server/global.js +++ b/fixtures/flight/server/global.js @@ -101,6 +101,12 @@ async function renderApp(req, res, next) { } else if (req.get('Content-type')) { proxiedHeaders['Content-type'] = req.get('Content-type'); } + if (req.headers['cache-control']) { + proxiedHeaders['Cache-Control'] = req.get('cache-control'); + } + if (req.get('rsc-request-id')) { + proxiedHeaders['rsc-request-id'] = req.get('rsc-request-id'); + } const requestsPrerender = req.path === '/prerender'; diff --git a/fixtures/flight/server/region.js b/fixtures/flight/server/region.js index 6896713e41cbf..7339e3a48abbb 100644 --- a/fixtures/flight/server/region.js +++ b/fixtures/flight/server/region.js @@ -50,7 +50,31 @@ const {readFile} = require('fs').promises; const React = require('react'); -async function renderApp(res, returnValue, formState) { +const activeDebugChannels = + process.env.NODE_ENV === 'development' ? new Map() : null; + +function filterStackFrame(sourceURL, functionName) { + return ( + sourceURL !== '' && + !sourceURL.startsWith('node:') && + !sourceURL.includes('node_modules') && + !sourceURL.endsWith('library.js') && + !sourceURL.includes('/server/region.js') + ); +} + +function getDebugChannel(req) { + if (process.env.NODE_ENV !== 'development') { + return undefined; + } + const requestId = req.get('rsc-request-id'); + if (!requestId) { + return undefined; + } + return activeDebugChannels.get(requestId); +} + +async function renderApp(res, returnValue, formState, noCache, debugChannel) { const {renderToPipeableStream} = await import( 'react-server-dom-webpack/server' ); @@ -97,15 +121,18 @@ async function renderApp(res, returnValue, formState) { key: filename, }) ), - React.createElement(App) + React.createElement(App, {noCache}) ); // For client-invoked server actions we refresh the tree and return a return value. const payload = {root, returnValue, formState}; - const {pipe} = renderToPipeableStream(payload, moduleMap); + const {pipe} = renderToPipeableStream(payload, moduleMap, { + debugChannel, + filterStackFrame, + }); pipe(res); } -async function prerenderApp(res, returnValue, formState) { +async function prerenderApp(res, returnValue, formState, noCache) { const {unstable_prerenderToNodeStream: prerenderToNodeStream} = await import( 'react-server-dom-webpack/static' ); @@ -152,23 +179,28 @@ async function prerenderApp(res, returnValue, formState) { key: filename, }) ), - React.createElement(App, {prerender: true}) + React.createElement(App, {prerender: true, noCache}) ); // For client-invoked server actions we refresh the tree and return a return value. const payload = {root, returnValue, formState}; - const {prelude} = await prerenderToNodeStream(payload, moduleMap); + const {prelude} = await prerenderToNodeStream(payload, moduleMap, { + filterStackFrame, + }); prelude.pipe(res); } app.get('/', async function (req, res) { + const noCache = req.get('cache-control') === 'no-cache'; + if ('prerender' in req.query) { - await prerenderApp(res, null, null); + await prerenderApp(res, null, null, noCache); } else { - await renderApp(res, null, null); + await renderApp(res, null, null, noCache, getDebugChannel(req)); } }); app.post('/', bodyParser.text(), async function (req, res) { + const noCache = req.headers['cache-control'] === 'no-cache'; const {decodeReply, decodeReplyFromBusboy, decodeAction, decodeFormState} = await import('react-server-dom-webpack/server'); const serverReference = req.get('rsc-action'); @@ -201,7 +233,7 @@ app.post('/', bodyParser.text(), async function (req, res) { // We handle the error on the client } // Refresh the client and return the value - renderApp(res, result, null); + renderApp(res, result, null, noCache, getDebugChannel(req)); } else { // This is the progressive enhancement case const UndiciRequest = require('undici').Request; @@ -217,11 +249,11 @@ app.post('/', bodyParser.text(), async function (req, res) { // Wait for any mutations const result = await action(); const formState = decodeFormState(result, formData); - renderApp(res, null, formState); + renderApp(res, null, formState, noCache, undefined); } catch (x) { const {setServerState} = await import('../src/ServerState.js'); setServerState('Error: ' + x.message); - renderApp(res, null, null); + renderApp(res, null, null, noCache, undefined); } } }); @@ -321,7 +353,7 @@ if (process.env.NODE_ENV === 'development') { }); } -app.listen(3001, () => { +const httpServer = app.listen(3001, () => { console.log('Regional Flight Server listening on port 3001...'); }); @@ -343,3 +375,24 @@ app.on('error', function (error) { throw error; } }); + +if (process.env.NODE_ENV === 'development') { + // Open a websocket server for Debug information + const WebSocket = require('ws'); + + const webSocketServer = new WebSocket.Server({ + server: httpServer, + path: '/debug-channel', + }); + + webSocketServer.on('connection', (ws, req) => { + const url = new URL(req.url, `http://${req.headers.host}`); + const requestId = url.searchParams.get('id'); + + activeDebugChannels.set(requestId, ws); + + ws.on('close', (code, reason) => { + activeDebugChannels.delete(requestId); + }); + }); +} diff --git a/fixtures/flight/src/App.js b/fixtures/flight/src/App.js index 2f2118580a93c..e6366f4bd0ea4 100644 --- a/fixtures/flight/src/App.js +++ b/fixtures/flight/src/App.js @@ -1,4 +1,7 @@ import * as React from 'react'; +import {renderToReadableStream} from 'react-server-dom-webpack/server'; +import {createFromReadableStream} from 'react-server-dom-webpack/client'; +import {PassThrough, Readable} from 'stream'; import Container from './Container.js'; @@ -12,6 +15,7 @@ import Button from './Button.js'; import Form from './Form.js'; import {Dynamic} from './Dynamic.js'; import {Client} from './Client.js'; +import {Navigate} from './Navigate.js'; import {Note} from './cjs/Note.js'; @@ -20,6 +24,7 @@ import {GenerateImage} from './GenerateImage.js'; import {like, greet, increment} from './actions.js'; import {getServerState} from './ServerState.js'; +import {sdkMethod} from './library.js'; const promisedText = new Promise(resolve => setTimeout(() => resolve('deferred text'), 50) @@ -29,20 +34,158 @@ function Foo({children}) { return
    {children}
    ; } +async function delayedError(text, ms) { + return new Promise((_, reject) => + setTimeout(() => reject(new Error(text)), ms) + ); +} + +async function delay(text, ms) { + return new Promise(resolve => setTimeout(() => resolve(text), ms)); +} + +async function delayTwice() { + try { + await delayedError('Delayed exception', 20); + } catch (x) { + // Ignored + } + await delay('', 10); +} + +async function delayTrice() { + const p = delayTwice(); + await delay('', 40); + return p; +} + async function Bar({children}) { - await new Promise(resolve => setTimeout(() => resolve('deferred text'), 10)); + await delayTrice(); return
    {children}
    ; } -async function ServerComponent() { - await new Promise(resolve => setTimeout(() => resolve('deferred text'), 50)); +async function ThirdPartyComponent() { + return await delay('hello from a 3rd party', 30); } -export default async function App({prerender}) { +let cachedThirdPartyStream; + +// We create the Component outside of AsyncLocalStorage so that it has no owner. +// That way it gets the owner from the call to createFromNodeStream. +const thirdPartyComponent = ; + +function simulateFetch(cb, latencyMs) { + return new Promise(resolve => { + // Request latency + setTimeout(() => { + const result = cb(); + // Response latency + setTimeout(() => { + resolve(result); + }, latencyMs); + }, latencyMs); + }); +} + +async function fetchThirdParty(noCache) { + // We're using the Web Streams APIs for tee'ing convenience. + let stream; + if (cachedThirdPartyStream && !noCache) { + stream = cachedThirdPartyStream; + } else { + stream = await simulateFetch( + () => + renderToReadableStream( + thirdPartyComponent, + {}, + {environmentName: 'third-party'} + ), + 25 + ); + } + + const [stream1, stream2] = stream.tee(); + cachedThirdPartyStream = stream1; + + return createFromReadableStream(stream2, { + serverConsumerManifest: { + moduleMap: {}, + serverModuleMap: null, + moduleLoading: null, + }, + }); +} + +async function ServerComponent({noCache}) { + await delay('deferred text', 50); + return await fetchThirdParty(noCache); +} + +let veryDeepObject = [ + { + bar: { + baz: { + a: {}, + }, + }, + }, + { + bar: { + baz: { + a: {}, + }, + }, + }, + { + bar: { + baz: { + a: {}, + }, + }, + }, + { + bar: { + baz: { + a: { + b: { + c: { + d: { + e: { + f: { + g: { + h: { + i: { + j: { + k: { + l: { + m: { + yay: 'You reached the end', + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, +]; + +export default async function App({prerender, noCache}) { const res = await fetch('http://localhost:3001/todos'); const todos = await res.json(); + await sdkMethod('http://localhost:3001/todos'); + + console.log('Expand me:', veryDeepObject); - const dedupedChild = ; + const dedupedChild = ; const message = getServerState(); return ( @@ -89,6 +232,7 @@ export default async function App({prerender}) { {dedupedChild} {Promise.resolve([dedupedChild])} + diff --git a/fixtures/flight/src/Navigate.js b/fixtures/flight/src/Navigate.js new file mode 100644 index 0000000000000..4436b9fdf7d2e --- /dev/null +++ b/fixtures/flight/src/Navigate.js @@ -0,0 +1,40 @@ +'use client'; + +import * as React from 'react'; +import Container from './Container.js'; + +export function Navigate() { + /** Repro for https://issues.chromium.org/u/1/issues/419746417 */ + function provokeChromeCrash() { + React.startTransition(async () => { + console.log('Default transition triggered'); + + await new Promise(resolve => { + setTimeout( + () => { + history.pushState( + {}, + '', + `?chrome-crash-419746417=${performance.now()}` + ); + }, + // This needs to happen before React's default transition indicator + // is displayed but after it's scheduled. + 100 + -50 + ); + + setTimeout(() => { + console.log('Default transition completed'); + resolve(); + }, 1000); + }); + }); + } + + return ( + +

    Navigation fixture

    + +
    + ); +} diff --git a/fixtures/flight/src/actions.js b/fixtures/flight/src/actions.js index aa19871a9dcbb..0b9b9c315d647 100644 --- a/fixtures/flight/src/actions.js +++ b/fixtures/flight/src/actions.js @@ -2,7 +2,13 @@ import {setServerState} from './ServerState.js'; +async function sleep(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); +} + export async function like() { + // Test loading state + await sleep(1000); setServerState('Liked!'); return new Promise((resolve, reject) => resolve('Liked')); } @@ -20,5 +26,7 @@ export async function greet(formData) { } export async function increment(n) { + // Test loading state + await sleep(1000); return n + 1; } diff --git a/fixtures/flight/src/index.js b/fixtures/flight/src/index.js index f08f7a110bf61..447b1957c8c09 100644 --- a/fixtures/flight/src/index.js +++ b/fixtures/flight/src/index.js @@ -14,20 +14,85 @@ function findSourceMapURL(fileName) { ); } -let updateRoot; -async function callServer(id, args) { - const response = fetch('/', { - method: 'POST', - headers: { - Accept: 'text/x-component', - 'rsc-action': id, +async function createWebSocketStream(url) { + const ws = new WebSocket(url); + ws.binaryType = 'arraybuffer'; + + await new Promise((resolve, reject) => { + ws.addEventListener('open', resolve, {once: true}); + ws.addEventListener('error', reject, {once: true}); + }); + + const writable = new WritableStream({ + write(chunk) { + ws.send(chunk); + }, + close() { + ws.close(); + }, + abort(reason) { + ws.close(1000, reason && String(reason)); }, - body: await encodeReply(args), }); - const {returnValue, root} = await createFromFetch(response, { - callServer, - findSourceMapURL, + + const readable = new ReadableStream({ + start(controller) { + ws.addEventListener('message', event => { + controller.enqueue(event.data); + }); + ws.addEventListener('close', () => { + controller.close(); + }); + ws.addEventListener('error', err => { + controller.error(err); + }); + }, }); + + return {readable, writable}; +} + +let updateRoot; +async function callServer(id, args) { + let response; + if (process.env.NODE_ENV === 'development') { + const requestId = crypto.randomUUID(); + const debugChannel = await createWebSocketStream( + `ws://localhost:3001/debug-channel?id=${requestId}` + ); + response = createFromFetch( + fetch('/', { + method: 'POST', + headers: { + Accept: 'text/x-component', + 'rsc-action': id, + 'rsc-request-id': requestId, + }, + body: await encodeReply(args), + }), + { + callServer, + debugChannel, + findSourceMapURL, + } + ); + } else { + response = createFromFetch( + fetch('/', { + method: 'POST', + headers: { + Accept: 'text/x-component', + 'rsc-action': id, + }, + body: await encodeReply(args), + }), + { + callServer, + findSourceMapURL, + } + ); + } + const {returnValue, root} = await response; // Refresh the tree with the new RSC payload. startTransition(() => { updateRoot(root); @@ -42,17 +107,39 @@ function Shell({data}) { } async function hydrateApp() { - const {root, returnValue, formState} = await createFromFetch( - fetch('/', { - headers: { - Accept: 'text/x-component', - }, - }), - { - callServer, - findSourceMapURL, - } - ); + let response; + if (process.env.NODE_ENV === 'development') { + const requestId = crypto.randomUUID(); + const debugChannel = await createWebSocketStream( + `ws://localhost:3001/debug-channel?id=${requestId}` + ); + response = createFromFetch( + fetch('/', { + headers: { + Accept: 'text/x-component', + 'rsc-request-id': requestId, + }, + }), + { + callServer, + debugChannel, + findSourceMapURL, + } + ); + } else { + response = createFromFetch( + fetch('/', { + headers: { + Accept: 'text/x-component', + }, + }), + { + callServer, + findSourceMapURL, + } + ); + } + const {root, returnValue, formState} = await response; ReactDOM.hydrateRoot( document, diff --git a/fixtures/flight/src/library.js b/fixtures/flight/src/library.js new file mode 100644 index 0000000000000..744205d1c40fe --- /dev/null +++ b/fixtures/flight/src/library.js @@ -0,0 +1,9 @@ +export async function sdkMethod(input, init) { + return fetch(input, init).then(async response => { + await new Promise(resolve => { + setTimeout(resolve, 10); + }); + + return response; + }); +} diff --git a/fixtures/flight/yarn.lock b/fixtures/flight/yarn.lock index 928e18a35eb8e..3acf46a0a0080 100644 --- a/fixtures/flight/yarn.lock +++ b/fixtures/flight/yarn.lock @@ -4614,6 +4614,11 @@ detect-port-alt@^1.1.6: address "^1.0.1" debug "^2.6.0" +devtools-ignore-webpack-plugin@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/devtools-ignore-webpack-plugin/-/devtools-ignore-webpack-plugin-0.2.0.tgz#a7b3d1bd0f593c7fee5cbb7534b07860e5e2447c" + integrity sha512-4P+1Y1VhSt1MRBRF6my8N1bs9nNMOFfIFSBHI6u18W73iCHWXNHTSWYeMoQQ72PIIHrP1q4koKpYg1Em3jb9Rw== + didyoumean@^1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/didyoumean/-/didyoumean-1.2.2.tgz#989346ffe9e839b4555ecf5666edea0d3e8ad037" @@ -8650,16 +8655,7 @@ string-length@^5.0.1: char-regex "^2.0.0" strip-ansi "^7.0.1" -"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== @@ -8730,14 +8726,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== @@ -9452,16 +9441,7 @@ wordwrap@~1.0.0: 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": - 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@^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== diff --git a/fixtures/owner-stacks/package.json b/fixtures/owner-stacks/package.json index cd288c8baeac7..2798aab568ecf 100644 --- a/fixtures/owner-stacks/package.json +++ b/fixtures/owner-stacks/package.json @@ -9,7 +9,7 @@ "web-vitals": "^2.1.0" }, "scripts": { - "prestart": "cp -a ../../build/oss-experimental/. node_modules", + "prestart": "cp -a ../../build/oss-experimental/. node_modules && rm -rf node_modules/.cache;", "start": "react-scripts start", "build": "react-scripts build", "test": "react-scripts test", diff --git a/fixtures/ssr/src/components/LargeContent.js b/fixtures/ssr/src/components/LargeContent.js index a5af3064b4917..3b1fa7ea35b25 100644 --- a/fixtures/ssr/src/components/LargeContent.js +++ b/fixtures/ssr/src/components/LargeContent.js @@ -1,8 +1,8 @@ -import React, {Fragment, Suspense} from 'react'; +import React, {Suspense, unstable_SuspenseList as SuspenseList} from 'react'; export default function LargeContent() { return ( - +

    Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris @@ -286,6 +286,6 @@ export default function LargeContent() { interdum a. Proin nec odio in nulla vestibulum.

    -
    + ); } diff --git a/fixtures/view-transition/loader/package.json b/fixtures/view-transition/loader/package.json new file mode 100644 index 0000000000000..3dbc1ca591c05 --- /dev/null +++ b/fixtures/view-transition/loader/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +} diff --git a/fixtures/view-transition/loader/server.js b/fixtures/view-transition/loader/server.js new file mode 100644 index 0000000000000..f56ac9fd039b5 --- /dev/null +++ b/fixtures/view-transition/loader/server.js @@ -0,0 +1,54 @@ +import babel from '@babel/core'; + +const babelOptions = { + babelrc: false, + ignore: [/\/(build|node_modules)\//], + plugins: [ + '@babel/plugin-syntax-import-meta', + '@babel/plugin-transform-react-jsx', + ], +}; + +export async function load(url, context, defaultLoad) { + if (url.endsWith('.css')) { + return {source: 'export default {}', format: 'module', shortCircuit: true}; + } + const {format} = context; + const result = await defaultLoad(url, context, defaultLoad); + if (result.format === 'module') { + const opt = Object.assign({filename: url}, babelOptions); + const newResult = await babel.transformAsync(result.source, opt); + if (!newResult) { + if (typeof result.source === 'string') { + return result; + } + return { + source: Buffer.from(result.source).toString('utf8'), + format: 'module', + }; + } + return {source: newResult.code, format: 'module'}; + } + return defaultLoad(url, context, defaultLoad); +} + +async function babelTransformSource(source, context, defaultTransformSource) { + const {format} = context; + if (format === 'module') { + const opt = Object.assign({filename: context.url}, babelOptions); + const newResult = await babel.transformAsync(source, opt); + if (!newResult) { + if (typeof source === 'string') { + return {source}; + } + return { + source: Buffer.from(source).toString('utf8'), + }; + } + return {source: newResult.code}; + } + return defaultTransformSource(source, context, defaultTransformSource); +} + +export const transformSource = + process.version < 'v16' ? babelTransformSource : undefined; diff --git a/fixtures/view-transition/package.json b/fixtures/view-transition/package.json index 7b9af04096027..44a8ff0bfa541 100644 --- a/fixtures/view-transition/package.json +++ b/fixtures/view-transition/package.json @@ -13,7 +13,8 @@ "express": "^4.14.0", "ignore-styles": "^5.0.1", "react": "^19.0.0", - "react-dom": "^19.0.0" + "react-dom": "^19.0.0", + "animation-timelines": "^0.0.4" }, "eslintConfig": { "extends": [ @@ -22,13 +23,13 @@ ] }, "scripts": { - "predev": "cp -r ../../build/oss-experimental/* ./node_modules/", - "prestart": "cp -r ../../build/oss-experimental/* ./node_modules/", - "prebuild": "cp -r ../../build/oss-experimental/* ./node_modules/", + "predev": "cp -r ../../build/oss-experimental/* ./node_modules/ && rm -rf node_modules/.cache;", + "prestart": "cp -r ../../build/oss-experimental/* ./node_modules/ && rm -rf node_modules/.cache;", + "prebuild": "cp -r ../../build/oss-experimental/* ./node_modules/ && rm -rf node_modules/.cache;", "dev": "concurrently \"npm run dev:server\" \"npm run dev:client\"", "dev:client": "BROWSER=none PORT=3001 react-scripts start", - "dev:server": "NODE_ENV=development node server", - "start": "react-scripts build && NODE_ENV=production node server", + "dev:server": "NODE_ENV=development node --experimental-loader ./loader/server.js server", + "start": "react-scripts build && NODE_ENV=production node --experimental-loader ./loader/server.js server", "build": "react-scripts build", "test": "react-scripts test --env=jsdom", "eject": "react-scripts eject" diff --git a/fixtures/view-transition/server/index.js b/fixtures/view-transition/server/index.js index 3f542b8f6e67d..e13d4706b9ef9 100644 --- a/fixtures/view-transition/server/index.js +++ b/fixtures/view-transition/server/index.js @@ -20,13 +20,15 @@ if (process.env.NODE_ENV === 'development') { for (var key in require.cache) { delete require.cache[key]; } - const render = require('./render').default; - render(req.url, res); + import('./render.js').then(({default: render}) => { + render(req.url, res); + }); }); } else { - const render = require('./render').default; - app.get('/', function (req, res) { - render(req.url, res); + import('./render.js').then(({default: render}) => { + app.get('/', function (req, res) { + render(req.url, res); + }); }); } diff --git a/fixtures/view-transition/server/render.js b/fixtures/view-transition/server/render.js index 11d352eabdd72..08224a57c4da2 100644 --- a/fixtures/view-transition/server/render.js +++ b/fixtures/view-transition/server/render.js @@ -1,7 +1,7 @@ import React from 'react'; import {renderToPipeableStream} from 'react-dom/server'; -import App from '../src/components/App'; +import App from '../src/components/App.js'; let assets; if (process.env.NODE_ENV === 'development') { diff --git a/fixtures/view-transition/src/components/App.js b/fixtures/view-transition/src/components/App.js index 275e594d87a1d..6b41bdf4eac2a 100644 --- a/fixtures/view-transition/src/components/App.js +++ b/fixtures/view-transition/src/components/App.js @@ -6,8 +6,8 @@ import React, { unstable_addTransitionType as addTransitionType, } from 'react'; -import Chrome from './Chrome'; -import Page from './Page'; +import Chrome from './Chrome.js'; +import Page from './Page.js'; const enableNavigationAPI = typeof navigation === 'object'; diff --git a/fixtures/view-transition/src/components/NestedReveal.js b/fixtures/view-transition/src/components/NestedReveal.js new file mode 100644 index 0000000000000..497f4430f6cfb --- /dev/null +++ b/fixtures/view-transition/src/components/NestedReveal.js @@ -0,0 +1,36 @@ +import React, {Suspense, use} from 'react'; + +async function sleep(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); +} + +function Use({useable}) { + use(useable); + return null; +} + +let delay1; +let delay2; + +export default function NestedReveal({}) { + if (!delay1) { + delay1 = sleep(100); + // Needs to happen before the throttled reveal of delay 1 + delay2 = sleep(200); + } + + return ( +
    + Shell + +
    Level 1
    + + + +
    Level 2
    + +
    +
    +
    + ); +} diff --git a/fixtures/view-transition/src/components/Page.js b/fixtures/view-transition/src/components/Page.js index 9744313c4f5ea..ef1a855320634 100644 --- a/fixtures/view-transition/src/components/Page.js +++ b/fixtures/view-transition/src/components/Page.js @@ -8,15 +8,21 @@ import React, { useId, useOptimistic, startTransition, + Suspense, } from 'react'; import {createPortal} from 'react-dom'; -import SwipeRecognizer from './SwipeRecognizer'; +import SwipeRecognizer from './SwipeRecognizer.js'; import './Page.css'; import transitions from './Transitions.module.css'; +import NestedReveal from './NestedReveal.js'; + +async function sleep(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); +} const a = (
    @@ -56,6 +62,12 @@ function Id() { return ; } +let wait; +function Suspend() { + if (!wait) wait = sleep(500); + return React.use(wait); +} + export default function Page({url, navigate}) { const [renderedUrl, optimisticNavigate] = useOptimistic( url, @@ -89,7 +101,7 @@ export default function Page({url, navigate}) { // a flushSync will. // Promise.resolve().then(() => { // flushSync(() => { - setCounter(c => c + 10); + // setCounter(c => c + 10); // }); // }); }, [show]); @@ -106,7 +118,13 @@ export default function Page({url, navigate}) { document.body ) ) : ( - ); @@ -183,22 +201,48 @@ export default function Page({url, navigate}) {
    !!
    -

    these

    -

    rows

    -

    exist

    -

    to

    -

    test

    -

    scrolling

    -

    content

    -

    out

    -

    of

    - {portal} -

    the

    -

    viewport

    + +
    + +

    █████

    +
    +

    ████

    +

    ███████

    +

    ████

    +

    ██

    +

    ██████

    +

    ███

    +

    ████

    +
    + + }> + +
    +

    these

    +

    rows

    + +

    exist

    +
    +

    to

    +

    test

    +

    scrolling

    +

    content

    +

    out

    +

    of

    + {portal} +

    the

    +

    viewport

    + +
    +
    +
    {show ? : null}
    +
    ); } diff --git a/fixtures/view-transition/src/components/SwipeRecognizer.js b/fixtures/view-transition/src/components/SwipeRecognizer.js index 7e7176d194d83..df4d743e1ba7f 100644 --- a/fixtures/view-transition/src/components/SwipeRecognizer.js +++ b/fixtures/view-transition/src/components/SwipeRecognizer.js @@ -5,6 +5,16 @@ import React, { unstable_startGestureTransition as startGestureTransition, } from 'react'; +import ScrollTimelinePolyfill from 'animation-timelines/scroll-timeline'; +import TouchPanTimeline from 'animation-timelines/touch-pan-timeline'; + +const ua = typeof navigator === 'undefined' ? '' : navigator.userAgent; +const isSafariMobile = + ua.indexOf('Safari') !== -1 && + (ua.indexOf('iPhone') !== -1 || + ua.indexOf('iPad') !== -1 || + ua.indexOf('iPod') !== -1); + // Example of a Component that can recognize swipe gestures using a ScrollTimeline // without scrolling its own content. Allowing it to be used as an inert gesture // recognizer to drive a View Transition. @@ -21,18 +31,72 @@ export default function SwipeRecognizer({ const scrollRef = useRef(null); const activeGesture = useRef(null); - function onScroll() { - if (activeGesture.current !== null) { + const touchTimeline = useRef(null); + + function onTouchStart(event) { + if (!isSafariMobile && typeof ScrollTimeline === 'function') { + // If not Safari and native ScrollTimeline is supported, then we use that. return; } - if (typeof ScrollTimeline !== 'function') { + if (touchTimeline.current) { + // We can catch the gesture before it settles. return; } - // eslint-disable-next-line no-undef - const scrollTimeline = new ScrollTimeline({ - source: scrollRef.current, + const scrollElement = scrollRef.current; + const bounds = + axis === 'x' ? scrollElement.clientWidth : scrollElement.clientHeight; + const range = + direction === 'left' || direction === 'up' ? [bounds, 0] : [0, -bounds]; + const timeline = new TouchPanTimeline({ + touch: event, + source: scrollElement, axis: axis, + range: range, + snap: range, }); + touchTimeline.current = timeline; + timeline.settled.then(() => { + if (touchTimeline.current !== timeline) { + return; + } + touchTimeline.current = null; + const changed = + direction === 'left' || direction === 'up' + ? timeline.currentTime < 50 + : timeline.currentTime > 50; + onGestureEnd(changed); + }); + } + + function onTouchEnd() { + if (activeGesture.current === null) { + // If we didn't start a gesture before we release, we can release our + // timeline. + touchTimeline.current = null; + } + } + + function onScroll() { + if (activeGesture.current !== null) { + return; + } + + let scrollTimeline; + if (touchTimeline.current) { + // We're in a polyfilled touch gesture. Let's use that timeline instead. + scrollTimeline = touchTimeline.current; + } else if (typeof ScrollTimeline === 'function') { + // eslint-disable-next-line no-undef + scrollTimeline = new ScrollTimeline({ + source: scrollRef.current, + axis: axis, + }); + } else { + scrollTimeline = new ScrollTimelinePolyfill({ + source: scrollRef.current, + axis: axis, + }); + } activeGesture.current = startGestureTransition( scrollTimeline, () => { @@ -49,7 +113,23 @@ export default function SwipeRecognizer({ } ); } + function onGestureEnd(changed) { + // Reset scroll + if (changed) { + // Trigger side-effects + startTransition(action); + } + if (activeGesture.current !== null) { + const cancelGesture = activeGesture.current; + activeGesture.current = null; + cancelGesture(); + } + } function onScrollEnd() { + if (touchTimeline.current) { + // We have a touch gesture controlling the swipe. + return; + } let changed; const scrollElement = scrollRef.current; if (axis === 'x') { @@ -67,16 +147,7 @@ export default function SwipeRecognizer({ ? scrollElement.scrollTop < halfway : scrollElement.scrollTop > halfway; } - // Reset scroll - if (changed) { - // Trigger side-effects - startTransition(action); - } - if (activeGesture.current !== null) { - const cancelGesture = activeGesture.current; - activeGesture.current = null; - cancelGesture(); - } + onGestureEnd(changed); } useEffect(() => { @@ -168,6 +239,9 @@ export default function SwipeRecognizer({ return (
    diff --git a/fixtures/view-transition/src/index.js b/fixtures/view-transition/src/index.js index 8c2fac3e67ada..29b53bf037928 100644 --- a/fixtures/view-transition/src/index.js +++ b/fixtures/view-transition/src/index.js @@ -1,7 +1,7 @@ import React from 'react'; import {hydrateRoot} from 'react-dom/client'; -import App from './components/App'; +import App from './components/App.js'; hydrateRoot( document, diff --git a/fixtures/view-transition/yarn.lock b/fixtures/view-transition/yarn.lock index 76a6af00ca2ef..3efb208f1ec1a 100644 --- a/fixtures/view-transition/yarn.lock +++ b/fixtures/view-transition/yarn.lock @@ -2427,6 +2427,11 @@ ajv@^8.0.0, ajv@^8.6.0, ajv@^8.9.0: json-schema-traverse "^1.0.0" require-from-string "^2.0.2" +animation-timelines@^0.0.4: + version "0.0.4" + resolved "https://registry.yarnpkg.com/animation-timelines/-/animation-timelines-0.0.4.tgz#7ac4614bae73c4d1ea2ff18d5d87a518793258af" + integrity sha512-HwCE3m1nM8ZdLbwDwD1j5ZNKmY+3J2CliXJNIsf3y1Si927SIaWpfxkycTg5nWLJSHgjsYxrmOy2Jbo4JR1e9A== + ansi-escapes@^4.2.1, ansi-escapes@^4.3.1: version "4.3.2" resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e" diff --git a/flow-typed.config.json b/flow-typed.config.json new file mode 100644 index 0000000000000..10b200008371b --- /dev/null +++ b/flow-typed.config.json @@ -0,0 +1,20 @@ +{ + "env": [ + "bom", + "cssom", + "dom", + "geometry", + "html", + "node", + "serviceworkers", + "streams", + "web-animations" + ], + "ignore": [ + "create-react-class", + "jest", + "regenerator-runtime", + "webpack", + "ws" + ] +} diff --git a/flow-typed/environments/bom.js b/flow-typed/environments/bom.js new file mode 100644 index 0000000000000..06412856009d4 --- /dev/null +++ b/flow-typed/environments/bom.js @@ -0,0 +1,2719 @@ +// flow-typed signature: 09630545c584c3b212588a2390c257d0 +// flow-typed version: baae4b8bcc/bom/flow_>=v0.261.x + +/* BOM */ + +declare class Screen { + +availHeight: number; + +availWidth: number; + +availLeft: number; + +availTop: number; + +top: number; + +left: number; + +colorDepth: number; + +pixelDepth: number; + +width: number; + +height: number; + +orientation?: { + lock(): Promise, + unlock(): void, + angle: number, + onchange: () => mixed, + type: + | 'portrait-primary' + | 'portrait-secondary' + | 'landscape-primary' + | 'landscape-secondary', + ... + }; + // deprecated + mozLockOrientation?: (orientation: string | Array) => boolean; + mozUnlockOrientation?: () => void; + mozOrientation?: string; + onmozorientationchange?: (...args: any[]) => mixed; +} + +declare var screen: Screen; + +declare interface Crypto { + // Not using $TypedArray as that would include Float32Array and Float64Array which are not accepted + getRandomValues: < + T: + | Int8Array + | Uint8Array + | Uint8ClampedArray + | Int16Array + | Uint16Array + | Int32Array + | Uint32Array + | BigInt64Array + | BigUint64Array, + >( + typedArray: T + ) => T; + randomUUID: () => string; +} +declare var crypto: Crypto; + +declare var window: any; + +type GamepadButton = { + pressed: boolean, + value: number, + ... +}; +type GamepadHapticActuator = { + type: 'vibration', + pulse(value: number, duration: number): Promise, + ... +}; +type GamepadPose = { + angularAcceleration: null | Float32Array, + angularVelocity: null | Float32Array, + hasOrientation: boolean, + hasPosition: boolean, + linearAcceleration: null | Float32Array, + linearVelocity: null | Float32Array, + orientation: null | Float32Array, + position: null | Float32Array, + ... +}; +type Gamepad = { + axes: number[], + buttons: GamepadButton[], + connected: boolean, + displayId?: number, + hapticActuators?: GamepadHapticActuator[], + hand?: '' | 'left' | 'right', + id: string, + index: number, + mapping: string, + pose?: null | GamepadPose, + timestamp: number, + ... +}; + +// deprecated +type BatteryManager = { + +charging: boolean, + +chargingTime: number, + +dischargingTime: number, + +level: number, + onchargingchange: ?(event: any) => mixed, + onchargingtimechange: ?(event: any) => mixed, + ondischargingtimechange: ?(event: any) => mixed, + onlevelchange: ?(event: any) => mixed, + ... +}; + +// https://wicg.github.io/web-share +type ShareData = { + title?: string, + text?: string, + url?: string, + ... +}; + +type PermissionName = + | 'geolocation' + | 'notifications' + | 'push' + | 'midi' + | 'camera' + | 'microphone' + | 'speaker' + | 'usb' + | 'device-info' + | 'background-sync' + | 'bluetooth' + | 'persistent-storage' + | 'ambient-light-sensor' + | 'accelerometer' + | 'gyroscope' + | 'magnetometer' + | 'clipboard-read' + | 'clipboard-write'; + +type PermissionState = 'granted' | 'denied' | 'prompt'; + +type PermissionDescriptor = {| + name: PermissionName, +|}; + +type DevicePermissionDescriptor = {| + deviceId?: string, + name: 'camera' | 'microphone' | 'speaker', +|}; + +type MidiPermissionDescriptor = {| + name: 'midi', + sysex?: boolean, +|}; + +type PushPermissionDescriptor = {| + name: 'push', + userVisibleOnly?: boolean, +|}; + +type ClipboardPermissionDescriptor = {| + name: 'clipboard-read' | 'clipboard-write', + allowWithoutGesture: boolean, +|}; + +type USBPermissionDescriptor = {| + name: 'usb', + filters: Array, + exclusionFilters: Array, +|}; + +type FileSystemHandlePermissionDescriptor = {| + mode: 'read' | 'readwrite', +|}; + +declare class PermissionStatus extends EventTarget { + onchange: ?(event: any) => mixed; + +state: PermissionState; +} + +declare class Permissions { + query( + permissionDesc: + | DevicePermissionDescriptor + | MidiPermissionDescriptor + | PushPermissionDescriptor + | ClipboardPermissionDescriptor + | USBPermissionDescriptor + | PermissionDescriptor + ): Promise; +} + +type MIDIPortType = 'input' | 'output'; +type MIDIPortDeviceState = 'connected' | 'disconnected'; +type MIDIPortConnectionState = 'open' | 'closed' | 'pending'; + +type MIDIOptions = {| + sysex: boolean, + software: boolean, +|}; + +type MIDIMessageEvent$Init = Event$Init & { + data: Uint8Array, + ... +}; + +declare class MIDIMessageEvent extends Event { + constructor(type: string, eventInitDict: MIDIMessageEvent$Init): void; + +data: Uint8Array; +} + +type MIDIConnectionEvent$Init = Event$Init & { + port: MIDIPort, + ... +}; + +declare class MIDIConnectionEvent extends Event { + constructor(type: string, eventInitDict: MIDIConnectionEvent$Init): void; + +port: MIDIPort; +} + +declare class MIDIPort extends EventTarget { + +id: string; + +manufacturer?: string; + +name?: string; + +type: MIDIPortType; + +version?: string; + +state: MIDIPortDeviceState; + +connection: MIDIPortConnectionState; + onstatechange: ?(ev: MIDIConnectionEvent) => mixed; + open(): Promise; + close(): Promise; +} + +declare class MIDIInput extends MIDIPort { + onmidimessage: ?(ev: MIDIMessageEvent) => mixed; +} + +declare class MIDIOutput extends MIDIPort { + send(data: Iterable, timestamp?: number): void; + clear(): void; +} + +declare class MIDIInputMap extends $ReadOnlyMap {} + +declare class MIDIOutputMap extends $ReadOnlyMap {} + +declare class MIDIAccess extends EventTarget { + +inputs: MIDIInputMap; + +outputs: MIDIOutputMap; + +sysexEnabled: boolean; + onstatechange: ?(ev: MIDIConnectionEvent) => mixed; +} + +declare class NavigatorID { + appName: 'Netscape'; + appCodeName: 'Mozilla'; + product: 'Gecko'; + appVersion: string; + platform: string; + userAgent: string; +} + +declare class NavigatorLanguage { + +language: string; + +languages: $ReadOnlyArray; +} + +declare class NavigatorContentUtils { + registerContentHandler(mimeType: string, uri: string, title: string): void; + registerProtocolHandler(protocol: string, uri: string, title: string): void; +} + +declare class NavigatorCookies { + +cookieEnabled: boolean; +} + +declare class NavigatorPlugins { + +plugins: PluginArray; + +mimeTypes: MimeTypeArray; + javaEnabled(): boolean; +} + +declare class NavigatorOnLine { + +onLine: boolean; +} + +declare class NavigatorConcurrentHardware { + +hardwareConcurrency: number; +} + +declare class NavigatorStorage { + storage?: StorageManager; +} + +declare class StorageManager { + persist: () => Promise; + persisted: () => Promise; + estimate?: () => Promise; + getDirectory: () => Promise; +} + +type StorageManagerRegisteredEndpoint = + | 'caches' + | 'indexedDB' + | 'localStorage' + | 'serviceWorkerRegistrations' + | 'sessionStorage'; + +type StorageManagerUsageDetails = {[StorageManagerRegisteredEndpoint]: number}; + +declare class StorageEstimate { + constructor( + usage: number, + quota: number, + usageDetails?: StorageManagerUsageDetails + ): void; + +usage: number; + +quota: number; + + // Not a part of the standard + +usageDetails?: StorageManagerUsageDetails; +} + +declare class Navigator + mixins + NavigatorID, + NavigatorLanguage, + NavigatorOnLine, + NavigatorContentUtils, + NavigatorCookies, + NavigatorPlugins, + NavigatorConcurrentHardware, + NavigatorStorage +{ + productSub: '20030107' | '20100101'; + vendor: '' | 'Google Inc.' | 'Apple Computer, Inc'; + vendorSub: ''; + + activeVRDisplays?: VRDisplay[]; + appCodeName: 'Mozilla'; + buildID: string; + doNotTrack: string | null; + geolocation: Geolocation; + mediaDevices?: MediaDevices; + usb?: USB; + maxTouchPoints: number; + permissions: Permissions; + serviceWorker?: ServiceWorkerContainer; + getGamepads?: () => Array; + webkitGetGamepads?: Function; + mozGetGamepads?: Function; + mozGamepads?: any; + gamepads?: any; + webkitGamepads?: any; + getVRDisplays?: () => Promise; + registerContentHandler(mimeType: string, uri: string, title: string): void; + registerProtocolHandler(protocol: string, uri: string, title: string): void; + requestMIDIAccess?: (options?: MIDIOptions) => Promise; + requestMediaKeySystemAccess?: ( + keySystem: string, + supportedConfigurations: any[] + ) => Promise; + sendBeacon?: (url: string, data?: BodyInit) => boolean; + vibrate?: (pattern: number | number[]) => boolean; + mozVibrate?: (pattern: number | number[]) => boolean; + webkitVibrate?: (pattern: number | number[]) => boolean; + canShare?: (shareData?: ShareData) => boolean; + share?: (shareData: ShareData) => Promise; + clipboard: Clipboard; + credentials?: CredMgmtCredentialsContainer; + globalPrivacyControl?: boolean; + + // deprecated + getBattery?: () => Promise; + mozGetBattery?: () => Promise; + + // deprecated + getUserMedia?: Function; + webkitGetUserMedia?: Function; + mozGetUserMedia?: Function; + msGetUserMedia?: Function; + + // Gecko + taintEnabled?: () => false; + oscpu: string; +} + +declare class Clipboard extends EventTarget { + read(): Promise; + readText(): Promise; + write(data: $ReadOnlyArray): Promise; + writeText(data: string): Promise; +} + +declare var navigator: Navigator; + +declare class MimeType { + type: string; + description: string; + suffixes: string; + enabledPlugin: Plugin; +} + +declare class MimeTypeArray { + length: number; + item(index: number): MimeType; + namedItem(name: string): MimeType; + [key: number | string]: MimeType; +} + +declare class Plugin { + description: string; + filename: string; + name: string; + version?: string; // Gecko only + length: number; + item(index: number): MimeType; + namedItem(name: string): MimeType; + [key: number | string]: MimeType; +} + +declare class PluginArray { + length: number; + item(index: number): Plugin; + namedItem(name: string): Plugin; + refresh(): void; + [key: number | string]: Plugin; +} + +// https://www.w3.org/TR/hr-time-2/#dom-domhighrestimestamp +// https://developer.mozilla.org/en-US/docs/Web/API/DOMHighResTimeStamp +declare type DOMHighResTimeStamp = number; + +// https://www.w3.org/TR/navigation-timing-2/ +declare class PerformanceTiming { + connectEnd: number; + connectStart: number; + domainLookupEnd: number; + domainLookupStart: number; + domComplete: number; + domContentLoadedEventEnd: number; + domContentLoadedEventStart: number; + domInteractive: number; + domLoading: number; + fetchStart: number; + loadEventEnd: number; + loadEventStart: number; + navigationStart: number; + redirectEnd: number; + redirectStart: number; + requestStart: number; + responseEnd: number; + responseStart: number; + secureConnectionStart: number; + unloadEventEnd: number; + unloadEventStart: number; +} + +declare class PerformanceNavigation { + TYPE_NAVIGATE: 0; + TYPE_RELOAD: 1; + TYPE_BACK_FORWARD: 2; + TYPE_RESERVED: 255; + + type: 0 | 1 | 2 | 255; + redirectCount: number; +} + +type PerformanceEntryFilterOptions = { + name: string, + entryType: string, + initiatorType: string, + ... +}; + +// https://www.w3.org/TR/performance-timeline-2/ +declare class PerformanceEntry { + name: string; + entryType: string; + startTime: DOMHighResTimeStamp; + duration: DOMHighResTimeStamp; + toJSON(): string; +} + +// https://w3c.github.io/server-timing/#the-performanceservertiming-interface +declare class PerformanceServerTiming { + description: string; + duration: DOMHighResTimeStamp; + name: string; + toJSON(): string; +} + +// https://www.w3.org/TR/resource-timing-2/#sec-performanceresourcetiming +// https://w3c.github.io/server-timing/#extension-to-the-performanceresourcetiming-interface +declare class PerformanceResourceTiming extends PerformanceEntry { + initiatorType: string; + nextHopProtocol: string; + workerStart: number; + redirectStart: number; + redirectEnd: number; + fetchStart: number; + domainLookupStart: number; + domainLookupEnd: number; + connectStart: number; + connectEnd: number; + secureConnectionStart: number; + requestStart: number; + responseStart: number; + responseEnd: number; + transferSize: string; + encodedBodySize: number; + decodedBodySize: number; + serverTiming: Array; +} + +// https://w3c.github.io/event-timing/#sec-performance-event-timing +declare class PerformanceEventTiming extends PerformanceEntry { + processingStart: number; + processingEnd: number; + cancelable: boolean; + target: ?Node; + interactionId: number; +} + +// https://w3c.github.io/longtasks/#taskattributiontiming +declare class TaskAttributionTiming extends PerformanceEntry { + containerType: string; + containerSrc: string; + containerId: string; + containerName: string; +} + +// https://w3c.github.io/longtasks/#sec-PerformanceLongTaskTiming +declare class PerformanceLongTaskTiming extends PerformanceEntry { + attribution: $ReadOnlyArray; +} + +// https://www.w3.org/TR/navigation-timing-2/ +declare class PerformanceNavigationTiming extends PerformanceResourceTiming { + unloadEventStart: number; + unloadEventEnd: number; + domInteractive: number; + domContentLoadedEventStart: number; + domContentLoadedEventEnd: number; + domComplete: number; + loadEventStart: number; + loadEventEnd: number; + type: 'navigate' | 'reload' | 'back_forward' | 'prerender'; + redirectCount: number; +} + +// https://www.w3.org/TR/user-timing/#extensions-performance-interface +declare type PerformanceMarkOptions = {| + detail?: mixed, + startTime?: number, +|}; + +declare type PerformanceMeasureOptions = {| + detail?: mixed, + start?: number | string, + end?: number | string, + duration?: number, +|}; + +type EventCountsForEachCallbackType = + | (() => void) + | ((value: number) => void) + | ((value: number, key: string) => void) + | ((value: number, key: string, map: Map) => void); + +// https://www.w3.org/TR/event-timing/#eventcounts +declare interface EventCounts { + size: number; + + entries(): Iterator<[string, number]>; + forEach(callback: EventCountsForEachCallbackType): void; + get(key: string): ?number; + has(key: string): boolean; + keys(): Iterator; + values(): Iterator; +} + +declare class Performance { + eventCounts: EventCounts; + + // deprecated + navigation: PerformanceNavigation; + timing: PerformanceTiming; + + onresourcetimingbufferfull: (ev: any) => mixed; + clearMarks(name?: string): void; + clearMeasures(name?: string): void; + clearResourceTimings(): void; + getEntries(options?: PerformanceEntryFilterOptions): Array; + getEntriesByName(name: string, type?: string): Array; + getEntriesByType(type: string): Array; + mark(name: string, options?: PerformanceMarkOptions): void; + measure( + name: string, + startMarkOrOptions?: string | PerformanceMeasureOptions, + endMark?: string + ): void; + now(): DOMHighResTimeStamp; + setResourceTimingBufferSize(maxSize: number): void; + toJSON(): string; +} + +declare var performance: Performance; + +type PerformanceEntryList = PerformanceEntry[]; + +declare interface PerformanceObserverEntryList { + getEntries(): PerformanceEntryList; + getEntriesByType(type: string): PerformanceEntryList; + getEntriesByName(name: string, type: ?string): PerformanceEntryList; +} + +type PerformanceObserverInit = { + entryTypes?: string[], + type?: string, + buffered?: boolean, + ... +}; + +declare class PerformanceObserver { + constructor( + callback: ( + entries: PerformanceObserverEntryList, + observer: PerformanceObserver + ) => mixed + ): void; + + observe(options: ?PerformanceObserverInit): void; + disconnect(): void; + takeRecords(): PerformanceEntryList; + + static supportedEntryTypes: string[]; +} + +declare class History { + length: number; + scrollRestoration: 'auto' | 'manual'; + state: any; + back(): void; + forward(): void; + go(delta?: number): void; + pushState(statedata: any, title: string, url?: string): void; + replaceState(statedata: any, title: string, url?: string): void; +} + +declare var history: History; + +declare class Location { + ancestorOrigins: string[]; + hash: string; + host: string; + hostname: string; + href: string; + origin: string; + pathname: string; + port: string; + protocol: string; + search: string; + assign(url: string): void; + reload(flag?: boolean): void; + replace(url: string): void; + toString(): string; +} + +declare var location: Location; + +/////////////////////////////////////////////////////////////////////////////// + +declare class DOMParser { + parseFromString(source: string | TrustedHTML, mimeType: string): Document; +} + +type FormDataEntryValue = string | File; + +declare class FormData { + constructor(form?: HTMLFormElement, submitter?: HTMLElement | null): void; + + has(name: string): boolean; + get(name: string): ?FormDataEntryValue; + getAll(name: string): Array; + + set(name: string, value: string): void; + set(name: string, value: Blob, filename?: string): void; + set(name: string, value: File, filename?: string): void; + + append(name: string, value: string): void; + append(name: string, value: Blob, filename?: string): void; + append(name: string, value: File, filename?: string): void; + + delete(name: string): void; + + keys(): Iterator; + values(): Iterator; + entries(): Iterator<[string, FormDataEntryValue]>; +} + +declare type IntersectionObserverEntry = { + boundingClientRect: DOMRectReadOnly, + intersectionRatio: number, + intersectionRect: DOMRectReadOnly, + isIntersecting: boolean, + rootBounds: DOMRectReadOnly, + target: Element, + time: DOMHighResTimeStamp, + ... +}; + +declare type IntersectionObserverCallback = ( + entries: Array, + observer: IntersectionObserver +) => mixed; + +declare type IntersectionObserverOptions = { + root?: Node | null, + rootMargin?: string, + threshold?: number | Array, + ... +}; + +declare class IntersectionObserver { + constructor( + callback: IntersectionObserverCallback, + options?: IntersectionObserverOptions + ): void; + root: Element | null; + rootMargin: string; + scrollMargin: string; + thresholds: number[]; + observe(target: Element): void; + unobserve(target: Element): void; + takeRecords(): Array; + disconnect(): void; +} + +declare interface ResizeObserverSize { + +inlineSize: number; + +blockSize: number; +} + +declare interface ResizeObserverEntry { + /** + * The Element whose size has changed. + */ + +target: Element; + /** + * Element's content rect when ResizeObserverCallback is invoked. + * + * Legacy, may be deprecated in the future. + */ + +contentRect: DOMRectReadOnly; + /** + * An array containing the Element's border box size when + * ResizeObserverCallback is invoked. + */ + +borderBoxSize: $ReadOnlyArray; + /** + * An array containing the Element's content rect size when + * ResizeObserverCallback is invoked. + */ + +contentBoxSize: $ReadOnlyArray; + /** + * An array containing the Element's content rect size in integral device + * pixels when ResizeObserverCallback is invoked. + * + * Not implemented in Firefox or Safari as of July 2021 + */ + +devicePixelContentBoxSize?: $ReadOnlyArray | void; +} + +/** + * ResizeObserver can observe different kinds of CSS sizes: + * - border-box : size of box border area as defined in CSS2. + * - content-box : size of content area as defined in CSS2. + * - device-pixel-content-box : size of content area as defined in CSS2, in device + * pixels, before applying any CSS transforms on the element or its ancestors. + * This size must contain integer values. + */ +type ResizeObserverBoxOptions = + | 'border-box' + | 'content-box' + | 'device-pixel-content-box'; + +declare type ResizeObserverOptions = { + box?: ResizeObserverBoxOptions, + ... +}; + +/** + * The ResizeObserver interface is used to observe changes to Element's size. + */ +declare class ResizeObserver { + constructor( + callback: ( + entries: ResizeObserverEntry[], + observer: ResizeObserver + ) => mixed + ): void; + /** + * Adds target to the list of observed elements. + */ + observe(target: Element, options?: ResizeObserverOptions): void; + /** + * Removes target from the list of observed elements. + */ + unobserve(target: Element): void; + disconnect(): void; +} + +declare class CloseEvent extends Event { + code: number; + reason: string; + wasClean: boolean; +} + +declare class WebSocket extends EventTarget { + static CONNECTING: 0; + static OPEN: 1; + static CLOSING: 2; + static CLOSED: 3; + constructor(url: string, protocols?: string | Array): void; + protocol: string; + readyState: number; + bufferedAmount: number; + extensions: string; + onopen: (ev: any) => mixed; + onmessage: (ev: MessageEvent) => mixed; + onclose: (ev: CloseEvent) => mixed; + onerror: (ev: any) => mixed; + binaryType: 'blob' | 'arraybuffer'; + url: string; + close(code?: number, reason?: string): void; + send(data: string): void; + send(data: Blob): void; + send(data: ArrayBuffer): void; + send(data: $ArrayBufferView): void; + CONNECTING: 0; + OPEN: 1; + CLOSING: 2; + CLOSED: 3; +} + +type WorkerOptions = { + type?: WorkerType, + credentials?: CredentialsType, + name?: string, + ... +}; + +declare class Worker extends EventTarget { + constructor( + stringUrl: string | TrustedScriptURL, + workerOptions?: WorkerOptions + ): void; + onerror: null | ((ev: any) => mixed); + onmessage: null | ((ev: MessageEvent) => mixed); + onmessageerror: null | ((ev: MessageEvent) => mixed); + postMessage(message: any, ports?: any): void; + terminate(): void; +} + +declare class SharedWorker extends EventTarget { + constructor(stringUrl: string | TrustedScriptURL, name?: string): void; + constructor( + stringUrl: string | TrustedScriptURL, + workerOptions?: WorkerOptions + ): void; + port: MessagePort; + onerror: (ev: any) => mixed; +} + +declare function importScripts(...urls: Array): void; + +declare class WorkerGlobalScope extends EventTarget { + self: this; + location: WorkerLocation; + navigator: WorkerNavigator; + close(): void; + importScripts(...urls: Array): void; + onerror: (ev: any) => mixed; + onlanguagechange: (ev: any) => mixed; + onoffline: (ev: any) => mixed; + ononline: (ev: any) => mixed; + onrejectionhandled: (ev: PromiseRejectionEvent) => mixed; + onunhandledrejection: (ev: PromiseRejectionEvent) => mixed; +} + +declare class DedicatedWorkerGlobalScope extends WorkerGlobalScope { + onmessage: (ev: MessageEvent) => mixed; + onmessageerror: (ev: MessageEvent) => mixed; + postMessage(message: any, transfer?: Iterable): void; +} + +declare class SharedWorkerGlobalScope extends WorkerGlobalScope { + name: string; + onconnect: (ev: MessageEvent) => mixed; +} + +declare class WorkerLocation { + origin: string; + protocol: string; + host: string; + hostname: string; + port: string; + pathname: string; + search: string; + hash: string; +} + +declare class WorkerNavigator + mixins + NavigatorID, + NavigatorLanguage, + NavigatorOnLine, + NavigatorConcurrentHardware, + NavigatorStorage +{ + permissions: Permissions; +} + +// deprecated +declare class XDomainRequest { + timeout: number; + onerror: () => mixed; + onload: () => mixed; + onprogress: () => mixed; + ontimeout: () => mixed; + +responseText: string; + +contentType: string; + open(method: 'GET' | 'POST', url: string): void; + abort(): void; + send(data?: string): void; + + statics: {create(): XDomainRequest, ...}; +} + +declare class XMLHttpRequest extends EventTarget { + static LOADING: number; + static DONE: number; + static UNSENT: number; + static OPENED: number; + static HEADERS_RECEIVED: number; + responseBody: any; + status: number; + readyState: number; + responseText: string; + responseXML: any; + responseURL: string; + ontimeout: ProgressEventHandler; + statusText: string; + onreadystatechange: (ev: any) => mixed; + timeout: number; + onload: ProgressEventHandler; + response: any; + withCredentials: boolean; + onprogress: ProgressEventHandler; + onabort: ProgressEventHandler; + responseType: string; + onloadend: ProgressEventHandler; + upload: XMLHttpRequestEventTarget; + onerror: ProgressEventHandler; + onloadstart: ProgressEventHandler; + msCaching: string; + open( + method: string, + url: string, + async?: boolean, + user?: string, + password?: string + ): void; + send(data?: any): void; + abort(): void; + getAllResponseHeaders(): string; + setRequestHeader(header: string, value: string): void; + getResponseHeader(header: string): string; + msCachingEnabled(): boolean; + overrideMimeType(mime: string): void; + LOADING: number; + DONE: number; + UNSENT: number; + OPENED: number; + HEADERS_RECEIVED: number; + + statics: {create(): XMLHttpRequest, ...}; +} + +declare class XMLHttpRequestEventTarget extends EventTarget { + onprogress: ProgressEventHandler; + onerror: ProgressEventHandler; + onload: ProgressEventHandler; + ontimeout: ProgressEventHandler; + onabort: ProgressEventHandler; + onloadstart: ProgressEventHandler; + onloadend: ProgressEventHandler; +} + +declare class XMLSerializer { + serializeToString(target: Node): string; +} + +declare class Geolocation { + getCurrentPosition( + success: (position: Position) => mixed, + error?: (error: PositionError) => mixed, + options?: PositionOptions + ): void; + watchPosition( + success: (position: Position) => mixed, + error?: (error: PositionError) => mixed, + options?: PositionOptions + ): number; + clearWatch(id: number): void; +} + +declare class Position { + coords: Coordinates; + timestamp: number; +} + +declare class Coordinates { + latitude: number; + longitude: number; + altitude?: number; + accuracy: number; + altitudeAccuracy?: number; + heading?: number; + speed?: number; +} + +declare class PositionError { + code: number; + message: string; + PERMISSION_DENIED: 1; + POSITION_UNAVAILABLE: 2; + TIMEOUT: 3; +} + +type PositionOptions = { + enableHighAccuracy?: boolean, + timeout?: number, + maximumAge?: number, + ... +}; + +type AudioContextState = 'suspended' | 'running' | 'closed'; + +// deprecated +type AudioProcessingEvent$Init = Event$Init & { + playbackTime: number, + inputBuffer: AudioBuffer, + outputBuffer: AudioBuffer, + ... +}; + +// deprecated +declare class AudioProcessingEvent extends Event { + constructor(type: string, eventInitDict: AudioProcessingEvent$Init): void; + + +playbackTime: number; + +inputBuffer: AudioBuffer; + +outputBuffer: AudioBuffer; +} + +type OfflineAudioCompletionEvent$Init = Event$Init & { + renderedBuffer: AudioBuffer, + ... +}; + +declare class OfflineAudioCompletionEvent extends Event { + constructor( + type: string, + eventInitDict: OfflineAudioCompletionEvent$Init + ): void; + + +renderedBuffer: AudioBuffer; +} + +declare class BaseAudioContext extends EventTarget { + currentTime: number; + destination: AudioDestinationNode; + listener: AudioListener; + sampleRate: number; + state: AudioContextState; + onstatechange: (ev: any) => mixed; + createBuffer( + numOfChannels: number, + length: number, + sampleRate: number + ): AudioBuffer; + createBufferSource(myMediaElement?: HTMLMediaElement): AudioBufferSourceNode; + createMediaElementSource( + myMediaElement: HTMLMediaElement + ): MediaElementAudioSourceNode; + createMediaStreamSource(stream: MediaStream): MediaStreamAudioSourceNode; + createMediaStreamDestination(): MediaStreamAudioDestinationNode; + + // deprecated + createScriptProcessor( + bufferSize: number, + numberOfInputChannels: number, + numberOfOutputChannels: number + ): ScriptProcessorNode; + + createAnalyser(): AnalyserNode; + createBiquadFilter(): BiquadFilterNode; + createChannelMerger(numberOfInputs?: number): ChannelMergerNode; + createChannelSplitter(numberOfInputs?: number): ChannelSplitterNode; + createConstantSource(): ConstantSourceNode; + createConvolver(): ConvolverNode; + createDelay(maxDelayTime?: number): DelayNode; + createDynamicsCompressor(): DynamicsCompressorNode; + createGain(): GainNode; + createIIRFilter( + feedforward: Float32Array, + feedback: Float32Array + ): IIRFilterNode; + createOscillator(): OscillatorNode; + createPanner(): PannerNode; + createStereoPanner(): StereoPannerNode; + createPeriodicWave( + real: Float32Array, + img: Float32Array, + options?: {disableNormalization: boolean, ...} + ): PeriodicWave; + createStereoPanner(): StereoPannerNode; + createWaveShaper(): WaveShaperNode; + decodeAudioData( + arrayBuffer: ArrayBuffer, + decodeSuccessCallback: (decodedData: AudioBuffer) => mixed, + decodeErrorCallback: (err: DOMError) => mixed + ): void; + decodeAudioData(arrayBuffer: ArrayBuffer): Promise; +} + +declare class AudioTimestamp { + contextTime: number; + performanceTime: number; +} + +declare class AudioContext extends BaseAudioContext { + constructor(options?: {| + latencyHint?: 'balanced' | 'interactive' | 'playback' | number, + sampleRate?: number, + |}): AudioContext; + baseLatency: number; + outputLatency: number; + getOutputTimestamp(): AudioTimestamp; + resume(): Promise; + suspend(): Promise; + close(): Promise; + createMediaElementSource( + myMediaElement: HTMLMediaElement + ): MediaElementAudioSourceNode; + createMediaStreamSource( + myMediaStream: MediaStream + ): MediaStreamAudioSourceNode; + createMediaStreamTrackSource( + myMediaStreamTrack: MediaStreamTrack + ): MediaStreamTrackAudioSourceNode; + createMediaStreamDestination(): MediaStreamAudioDestinationNode; +} + +declare class OfflineAudioContext extends BaseAudioContext { + startRendering(): Promise; + suspend(suspendTime: number): Promise; + length: number; + oncomplete: (ev: OfflineAudioCompletionEvent) => mixed; +} + +declare class AudioNode extends EventTarget { + context: AudioContext; + numberOfInputs: number; + numberOfOutputs: number; + channelCount: number; + channelCountMode: 'max' | 'clamped-max' | 'explicit'; + channelInterpretation: 'speakers' | 'discrete'; + connect(audioNode: AudioNode, output?: number, input?: number): AudioNode; + connect(destination: AudioParam, output?: number): void; + disconnect(destination?: AudioNode, output?: number, input?: number): void; +} + +declare class AudioParam extends AudioNode { + value: number; + defaultValue: number; + setValueAtTime(value: number, startTime: number): this; + linearRampToValueAtTime(value: number, endTime: number): this; + exponentialRampToValueAtTime(value: number, endTime: number): this; + setTargetAtTime( + target: number, + startTime: number, + timeConstant: number + ): this; + setValueCurveAtTime( + values: Float32Array, + startTime: number, + duration: number + ): this; + cancelScheduledValues(startTime: number): this; +} + +declare class AudioDestinationNode extends AudioNode { + maxChannelCount: number; +} + +declare class AudioListener extends AudioNode { + positionX: AudioParam; + positionY: AudioParam; + positionZ: AudioParam; + forwardX: AudioParam; + forwardY: AudioParam; + forwardZ: AudioParam; + upX: AudioParam; + upY: AudioParam; + upZ: AudioParam; + setPosition(x: number, y: number, c: number): void; + setOrientation( + x: number, + y: number, + z: number, + xUp: number, + yUp: number, + zUp: number + ): void; +} + +declare class AudioBuffer { + sampleRate: number; + length: number; + duration: number; + numberOfChannels: number; + getChannelData(channel: number): Float32Array; + copyFromChannel( + destination: Float32Array, + channelNumber: number, + startInChannel?: number + ): void; + copyToChannel( + source: Float32Array, + channelNumber: number, + startInChannel?: number + ): void; +} + +declare class AudioBufferSourceNode extends AudioNode { + buffer: AudioBuffer; + detune: AudioParam; + loop: boolean; + loopStart: number; + loopEnd: number; + playbackRate: AudioParam; + onended: (ev: any) => mixed; + start(when?: number, offset?: number, duration?: number): void; + stop(when?: number): void; +} + +declare class CanvasCaptureMediaStream extends MediaStream { + canvas: HTMLCanvasElement; + requestFrame(): void; +} + +type DoubleRange = { + max?: number, + min?: number, + ... +}; + +type LongRange = { + max?: number, + min?: number, + ... +}; + +type ConstrainBooleanParameters = { + exact?: boolean, + ideal?: boolean, + ... +}; + +type ConstrainDOMStringParameters = { + exact?: string | string[], + ideal?: string | string[], + ... +}; + +type ConstrainDoubleRange = { + ...DoubleRange, + exact?: number, + ideal?: number, + ... +}; + +type ConstrainLongRange = { + ...LongRange, + exact?: number, + ideal?: number, + ... +}; + +type MediaTrackSupportedConstraints = {| + width: boolean, + height: boolean, + aspectRatio: boolean, + frameRate: boolean, + facingMode: boolean, + resizeMode: boolean, + volume: boolean, + sampleRate: boolean, + sampleSize: boolean, + echoCancellation: boolean, + autoGainControl: boolean, + noiseSuppression: boolean, + latency: boolean, + channelCount: boolean, + deviceId: boolean, + groupId: boolean, +|}; + +type MediaTrackConstraintSet = { + width?: number | ConstrainLongRange, + height?: number | ConstrainLongRange, + aspectRatio?: number | ConstrainDoubleRange, + frameRate?: number | ConstrainDoubleRange, + facingMode?: string | string[] | ConstrainDOMStringParameters, + resizeMode?: string | string[] | ConstrainDOMStringParameters, + volume?: number | ConstrainDoubleRange, + sampleRate?: number | ConstrainLongRange, + sampleSize?: number | ConstrainLongRange, + echoCancellation?: boolean | ConstrainBooleanParameters, + autoGainControl?: boolean | ConstrainBooleanParameters, + noiseSuppression?: boolean | ConstrainBooleanParameters, + latency?: number | ConstrainDoubleRange, + channelCount?: number | ConstrainLongRange, + deviceId?: string | string[] | ConstrainDOMStringParameters, + groupId?: string | string[] | ConstrainDOMStringParameters, + ... +}; + +type MediaTrackConstraints = { + ...MediaTrackConstraintSet, + advanced?: Array, + ... +}; + +type DisplayMediaStreamConstraints = { + video?: boolean | MediaTrackConstraints, + audio?: boolean | MediaTrackConstraints, + ... +}; + +type MediaStreamConstraints = { + audio?: boolean | MediaTrackConstraints, + video?: boolean | MediaTrackConstraints, + peerIdentity?: string, + ... +}; + +type MediaTrackSettings = { + aspectRatio?: number, + deviceId?: string, + displaySurface?: 'application' | 'browser' | 'monitor' | 'window', + echoCancellation?: boolean, + facingMode?: string, + frameRate?: number, + groupId?: string, + height?: number, + logicalSurface?: boolean, + sampleRate?: number, + sampleSize?: number, + volume?: number, + width?: number, + ... +}; + +type MediaTrackCapabilities = { + aspectRatio?: number | DoubleRange, + deviceId?: string, + echoCancellation?: boolean[], + facingMode?: string, + frameRate?: number | DoubleRange, + groupId?: string, + height?: number | LongRange, + sampleRate?: number | LongRange, + sampleSize?: number | LongRange, + volume?: number | DoubleRange, + width?: number | LongRange, + ... +}; + +declare class MediaDevices extends EventTarget { + ondevicechange: (ev: any) => mixed; + enumerateDevices: () => Promise>; + getSupportedConstraints: () => MediaTrackSupportedConstraints; + getDisplayMedia: ( + constraints?: DisplayMediaStreamConstraints + ) => Promise; + getUserMedia: (constraints: MediaStreamConstraints) => Promise; +} + +declare class MediaDeviceInfo { + +deviceId: string; + +groupId: string; + +kind: 'videoinput' | 'audioinput' | 'audiooutput'; + +label: string; +} + +type MediaRecorderOptions = { + mimeType?: string, + audioBitsPerSecond?: number, + videoBitsPerSecond?: number, + bitsPerSecond?: number, + audioBitrateMode?: 'cbr' | 'vbr', + ... +}; + +declare class MediaRecorder extends EventTarget { + constructor(stream: MediaStream, options?: MediaRecorderOptions): void; + +stream: MediaStream; + +mimeType: string; + +state: 'inactive' | 'recording' | 'paused'; + + onstart: (ev: any) => mixed; + onstop: (ev: any) => mixed; + ondataavailable: (ev: any) => mixed; + onpause: (ev: any) => mixed; + onresume: (ev: any) => mixed; + onerror: (ev: any) => mixed; + + +videoBitsPerSecond: number; + +audioBitsPerSecond: number; + +audioBitrateMode: 'cbr' | 'vbr'; + + start(timeslice?: number): void; + stop(): void; + pause(): void; + resume(): void; + requestData(): void; + + static isTypeSupported(type: string): boolean; +} + +declare class MediaStream extends EventTarget { + active: boolean; + ended: boolean; + id: string; + onactive: (ev: any) => mixed; + oninactive: (ev: any) => mixed; + onended: (ev: any) => mixed; + onaddtrack: (ev: MediaStreamTrackEvent) => mixed; + onremovetrack: (ev: MediaStreamTrackEvent) => mixed; + addTrack(track: MediaStreamTrack): void; + clone(): MediaStream; + getAudioTracks(): MediaStreamTrack[]; + getTrackById(trackid?: string): ?MediaStreamTrack; + getTracks(): MediaStreamTrack[]; + getVideoTracks(): MediaStreamTrack[]; + removeTrack(track: MediaStreamTrack): void; +} + +declare class MediaStreamTrack extends EventTarget { + enabled: boolean; + id: string; + kind: string; + label: string; + muted: boolean; + readonly: boolean; + readyState: 'live' | 'ended'; + remote: boolean; + contentHint?: string; + onstarted: (ev: any) => mixed; + onmute: (ev: any) => mixed; + onunmute: (ev: any) => mixed; + onoverconstrained: (ev: any) => mixed; + onended: (ev: any) => mixed; + getConstraints(): MediaTrackConstraints; + applyConstraints(constraints?: MediaTrackConstraints): Promise; + getSettings(): MediaTrackSettings; + getCapabilities(): MediaTrackCapabilities; + clone(): MediaStreamTrack; + stop(): void; +} + +declare class MediaStreamTrackEvent extends Event { + track: MediaStreamTrack; +} + +declare class MediaElementAudioSourceNode extends AudioNode {} +declare class MediaStreamAudioSourceNode extends AudioNode {} +declare class MediaStreamTrackAudioSourceNode extends AudioNode {} + +declare class MediaStreamAudioDestinationNode extends AudioNode { + stream: MediaStream; +} + +// deprecated +declare class ScriptProcessorNode extends AudioNode { + bufferSize: number; + onaudioprocess: (ev: AudioProcessingEvent) => mixed; +} + +declare class AnalyserNode extends AudioNode { + fftSize: number; + frequencyBinCount: number; + minDecibels: number; + maxDecibels: number; + smoothingTimeConstant: number; + getFloatFrequencyData(array: Float32Array): Float32Array; + getByteFrequencyData(array: Uint8Array): Uint8Array; + getFloatTimeDomainData(array: Float32Array): Float32Array; + getByteTimeDomainData(array: Uint8Array): Uint8Array; +} + +declare class BiquadFilterNode extends AudioNode { + frequency: AudioParam; + detune: AudioParam; + Q: AudioParam; + gain: AudioParam; + type: + | 'lowpass' + | 'highpass' + | 'bandpass' + | 'lowshelf' + | 'highshelf' + | 'peaking' + | 'notch' + | 'allpass'; + getFrequencyResponse( + frequencyHz: Float32Array, + magResponse: Float32Array, + phaseResponse: Float32Array + ): void; +} + +declare class ChannelMergerNode extends AudioNode {} +declare class ChannelSplitterNode extends AudioNode {} + +type ConstantSourceOptions = {offset?: number, ...}; +declare class ConstantSourceNode extends AudioNode { + constructor(context: BaseAudioContext, options?: ConstantSourceOptions): void; + offset: AudioParam; + onended: (ev: any) => mixed; + start(when?: number): void; + stop(when?: number): void; +} + +declare class ConvolverNode extends AudioNode { + buffer: AudioBuffer; + normalize: boolean; +} + +declare class DelayNode extends AudioNode { + delayTime: number; +} + +declare class DynamicsCompressorNode extends AudioNode { + threshold: AudioParam; + knee: AudioParam; + ratio: AudioParam; + reduction: AudioParam; + attack: AudioParam; + release: AudioParam; +} + +declare class GainNode extends AudioNode { + gain: AudioParam; +} + +declare class IIRFilterNode extends AudioNode { + getFrequencyResponse( + frequencyHz: Float32Array, + magResponse: Float32Array, + phaseResponse: Float32Array + ): void; +} + +declare class OscillatorNode extends AudioNode { + frequency: AudioParam; + detune: AudioParam; + type: 'sine' | 'square' | 'sawtooth' | 'triangle' | 'custom'; + start(when?: number): void; + stop(when?: number): void; + setPeriodicWave(periodicWave: PeriodicWave): void; + onended: (ev: any) => mixed; +} + +declare class StereoPannerNode extends AudioNode { + pan: AudioParam; +} + +declare class PannerNode extends AudioNode { + panningModel: 'equalpower' | 'HRTF'; + distanceModel: 'linear' | 'inverse' | 'exponential'; + refDistance: number; + maxDistance: number; + rolloffFactor: number; + coneInnerAngle: number; + coneOuterAngle: number; + coneOuterGain: number; + setPosition(x: number, y: number, z: number): void; + setOrientation(x: number, y: number, z: number): void; +} + +declare class PeriodicWave extends AudioNode {} +declare class WaveShaperNode extends AudioNode { + curve: Float32Array; + oversample: 'none' | '2x' | '4x'; +} + +// this part of spec is not finished yet, apparently +// https://stackoverflow.com/questions/35296664/can-fetch-get-object-as-headers +type HeadersInit = + | Headers + | Array<[string, string]> + | {[key: string]: string, ...}; + +// TODO Heades and URLSearchParams are almost the same thing. +// Could it somehow be abstracted away? +declare class Headers { + @@iterator(): Iterator<[string, string]>; + constructor(init?: HeadersInit): void; + append(name: string, value: string): void; + delete(name: string): void; + entries(): Iterator<[string, string]>; + forEach( + callback: ( + this: This, + value: string, + name: string, + headers: Headers + ) => mixed, + thisArg: This + ): void; + get(name: string): null | string; + has(name: string): boolean; + keys(): Iterator; + set(name: string, value: string): void; + values(): Iterator; +} + +declare class URLSearchParams { + @@iterator(): Iterator<[string, string]>; + + size: number; + + constructor( + init?: + | string + | URLSearchParams + | Array<[string, string]> + | {[string]: string, ...} + ): void; + append(name: string, value: string): void; + delete(name: string, value?: string): void; + entries(): Iterator<[string, string]>; + forEach( + callback: ( + this: This, + value: string, + name: string, + params: URLSearchParams + ) => mixed, + thisArg: This + ): void; + get(name: string): null | string; + getAll(name: string): Array; + has(name: string, value?: string): boolean; + keys(): Iterator; + set(name: string, value: string): void; + sort(): void; + values(): Iterator; + toString(): string; +} + +type CacheType = + | 'default' + | 'no-store' + | 'reload' + | 'no-cache' + | 'force-cache' + | 'only-if-cached'; +type CredentialsType = 'omit' | 'same-origin' | 'include'; +type ModeType = 'cors' | 'no-cors' | 'same-origin' | 'navigate'; +type RedirectType = 'follow' | 'error' | 'manual'; +type ReferrerPolicyType = + | '' + | 'no-referrer' + | 'no-referrer-when-downgrade' + | 'same-origin' + | 'origin' + | 'strict-origin' + | 'origin-when-cross-origin' + | 'strict-origin-when-cross-origin' + | 'unsafe-url'; + +type ResponseType = + | 'basic' + | 'cors' + | 'default' + | 'error' + | 'opaque' + | 'opaqueredirect'; + +type BodyInit = + | string + | URLSearchParams + | FormData + | Blob + | ArrayBuffer + | $ArrayBufferView + | ReadableStream; + +type RequestInfo = Request | URL | string; + +type RequestOptions = { + body?: ?BodyInit, + cache?: CacheType, + credentials?: CredentialsType, + headers?: HeadersInit, + integrity?: string, + keepalive?: boolean, + method?: string, + mode?: ModeType, + redirect?: RedirectType, + referrer?: string, + referrerPolicy?: ReferrerPolicyType, + signal?: ?AbortSignal, + window?: any, + ... +}; + +type ResponseOptions = { + status?: number, + statusText?: string, + headers?: HeadersInit, + ... +}; + +declare class Response { + constructor(input?: ?BodyInit, init?: ResponseOptions): void; + clone(): Response; + static error(): Response; + static redirect(url: string, status?: number): Response; + + redirected: boolean; + type: ResponseType; + url: string; + ok: boolean; + status: number; + statusText: string; + headers: Headers; + trailer: Promise; + + // Body methods and attributes + bodyUsed: boolean; + body: ?ReadableStream; + + arrayBuffer(): Promise; + blob(): Promise; + formData(): Promise; + json(): Promise; + text(): Promise; +} + +declare class Request { + constructor(input: RequestInfo, init?: RequestOptions): void; + clone(): Request; + + url: string; + + cache: CacheType; + credentials: CredentialsType; + headers: Headers; + integrity: string; + method: string; + mode: ModeType; + redirect: RedirectType; + referrer: string; + referrerPolicy: ReferrerPolicyType; + +signal: AbortSignal; + + // Body methods and attributes + bodyUsed: boolean; + + arrayBuffer(): Promise; + blob(): Promise; + formData(): Promise; + json(): Promise; + text(): Promise; +} + +declare function fetch( + input: RequestInfo, + init?: RequestOptions +): Promise; + +type TextEncoder$availableEncodings = + | 'utf-8' + | 'utf8' + | 'unicode-1-1-utf-8' + | 'utf-16be' + | 'utf-16' + | 'utf-16le'; + +declare class TextEncoder { + constructor(encoding?: TextEncoder$availableEncodings): void; + encode(buffer: string, options?: {stream: boolean, ...}): Uint8Array; + encoding: TextEncoder$availableEncodings; +} + +type TextDecoder$availableEncodings = + | '866' + | 'ansi_x3.4-1968' + | 'arabic' + | 'ascii' + | 'asmo-708' + | 'big5-hkscs' + | 'big5' + | 'chinese' + | 'cn-big5' + | 'cp1250' + | 'cp1251' + | 'cp1252' + | 'cp1253' + | 'cp1254' + | 'cp1255' + | 'cp1256' + | 'cp1257' + | 'cp1258' + | 'cp819' + | 'cp866' + | 'csbig5' + | 'cseuckr' + | 'cseucpkdfmtjapanese' + | 'csgb2312' + | 'csibm866' + | 'csiso2022jp' + | 'csiso2022kr' + | 'csiso58gb231280' + | 'csiso88596e' + | 'csiso88596i' + | 'csiso88598e' + | 'csiso88598i' + | 'csisolatin1' + | 'csisolatin2' + | 'csisolatin3' + | 'csisolatin4' + | 'csisolatin5' + | 'csisolatin6' + | 'csisolatin9' + | 'csisolatinarabic' + | 'csisolatincyrillic' + | 'csisolatingreek' + | 'csisolatinhebrew' + | 'cskoi8r' + | 'csksc56011987' + | 'csmacintosh' + | 'csshiftjis' + | 'cyrillic' + | 'dos-874' + | 'ecma-114' + | 'ecma-118' + | 'elot_928' + | 'euc-jp' + | 'euc-kr' + | 'gb_2312-80' + | 'gb_2312' + | 'gb18030' + | 'gb2312' + | 'gbk' + | 'greek' + | 'greek8' + | 'hebrew' + | 'hz-gb-2312' + | 'ibm819' + | 'ibm866' + | 'iso_8859-1:1987' + | 'iso_8859-1' + | 'iso_8859-2:1987' + | 'iso_8859-2' + | 'iso_8859-3:1988' + | 'iso_8859-3' + | 'iso_8859-4:1988' + | 'iso_8859-4' + | 'iso_8859-5:1988' + | 'iso_8859-5' + | 'iso_8859-6:1987' + | 'iso_8859-6' + | 'iso_8859-7:1987' + | 'iso_8859-7' + | 'iso_8859-8:1988' + | 'iso_8859-8' + | 'iso_8859-9:1989' + | 'iso_8859-9' + | 'iso-2022-cn-ext' + | 'iso-2022-cn' + | 'iso-2022-jp' + | 'iso-2022-kr' + | 'iso-8859-1' + | 'iso-8859-10' + | 'iso-8859-11' + | 'iso-8859-13' + | 'iso-8859-14' + | 'iso-8859-15' + | 'iso-8859-16' + | 'iso-8859-2' + | 'iso-8859-3' + | 'iso-8859-4' + | 'iso-8859-5' + | 'iso-8859-6-e' + | 'iso-8859-6-i' + | 'iso-8859-6' + | 'iso-8859-7' + | 'iso-8859-8-e' + | 'iso-8859-8-i' + | 'iso-8859-8' + | 'iso-8859-9' + | 'iso-ir-100' + | 'iso-ir-101' + | 'iso-ir-109' + | 'iso-ir-110' + | 'iso-ir-126' + | 'iso-ir-127' + | 'iso-ir-138' + | 'iso-ir-144' + | 'iso-ir-148' + | 'iso-ir-149' + | 'iso-ir-157' + | 'iso-ir-58' + | 'iso8859-1' + | 'iso8859-10' + | 'iso8859-11' + | 'iso8859-13' + | 'iso8859-14' + | 'iso8859-15' + | 'iso8859-2' + | 'iso8859-3' + | 'iso8859-4' + | 'iso8859-6' + | 'iso8859-7' + | 'iso8859-8' + | 'iso8859-9' + | 'iso88591' + | 'iso885910' + | 'iso885911' + | 'iso885913' + | 'iso885914' + | 'iso885915' + | 'iso88592' + | 'iso88593' + | 'iso88594' + | 'iso88595' + | 'iso88596' + | 'iso88597' + | 'iso88598' + | 'iso88599' + | 'koi' + | 'koi8_r' + | 'koi8-r' + | 'koi8-u' + | 'koi8' + | 'korean' + | 'ks_c_5601-1987' + | 'ks_c_5601-1989' + | 'ksc_5601' + | 'ksc5601' + | 'l1' + | 'l2' + | 'l3' + | 'l4' + | 'l5' + | 'l6' + | 'l9' + | 'latin1' + | 'latin2' + | 'latin3' + | 'latin4' + | 'latin5' + | 'latin6' + | 'latin9' + | 'logical' + | 'mac' + | 'macintosh' + | 'ms_kanji' + | 'shift_jis' + | 'shift-jis' + | 'sjis' + | 'sun_eu_greek' + | 'tis-620' + | 'unicode-1-1-utf-8' + | 'us-ascii' + | 'utf-16' + | 'utf-16be' + | 'utf-16le' + | 'utf-8' + | 'utf8' + | 'visual' + | 'windows-1250' + | 'windows-1251' + | 'windows-1252' + | 'windows-1253' + | 'windows-1254' + | 'windows-1255' + | 'windows-1256' + | 'windows-1257' + | 'windows-1258' + | 'windows-31j' + | 'windows-874' + | 'windows-949' + | 'x-cp1250' + | 'x-cp1251' + | 'x-cp1252' + | 'x-cp1253' + | 'x-cp1254' + | 'x-cp1255' + | 'x-cp1256' + | 'x-cp1257' + | 'x-cp1258' + | 'x-euc-jp' + | 'x-gbk' + | 'x-mac-cyrillic' + | 'x-mac-roman' + | 'x-mac-ukrainian' + | 'x-sjis' + | 'x-user-defined' + | 'x-x-big5'; + +declare class TextDecoder { + constructor( + encoding?: TextDecoder$availableEncodings, + options?: {fatal: boolean, ...} + ): void; + encoding: TextDecoder$availableEncodings; + fatal: boolean; + ignoreBOM: boolean; + decode( + buffer?: ArrayBuffer | $ArrayBufferView, + options?: {stream: boolean, ...} + ): string; +} + +declare class TextDecoderStream { + constructor( + encoding?: TextDecoder$availableEncodings, + options?: {fatal?: boolean, ignoreBOM?: boolean, ...} + ): void; + encoding: TextDecoder$availableEncodings; + fatal: boolean; + ignoreBOM: boolean; + readable: ReadableStream; + writable: WritableStream; +} + +declare class MessagePort extends EventTarget { + postMessage(message: any, transfer?: Iterable): void; + start(): void; + close(): void; + + onmessage: null | ((ev: MessageEvent) => mixed); + onmessageerror: null | ((ev: MessageEvent) => mixed); +} + +declare class MessageChannel { + port1: MessagePort; + port2: MessagePort; +} + +declare class VRDisplay extends EventTarget { + capabilities: VRDisplayCapabilities; + depthFar: number; + depthNear: number; + displayId: number; + displayName: string; + isPresenting: boolean; + stageParameters: null | VRStageParameters; + + cancelAnimationFrame(number): void; + exitPresent(): Promise; + getEyeParameters(VREye): VREyeParameters; + getFrameData(VRFrameData): boolean; + getLayers(): VRLayerInit[]; + requestAnimationFrame(cb: (number) => mixed): number; + requestPresent(VRLayerInit[]): Promise; + submitFrame(): void; +} + +type VRSource = HTMLCanvasElement; + +type VRLayerInit = { + leftBounds?: number[], + rightBounds?: number[], + source?: null | VRSource, + ... +}; + +type VRDisplayCapabilities = { + canPresent: boolean, + hasExternalDisplay: boolean, + hasPosition: boolean, + maxLayers: number, + ... +}; + +type VREye = 'left' | 'right'; + +type VRPose = { + angularAcceleration?: Float32Array, + angularVelocity?: Float32Array, + linearAcceleration?: Float32Array, + linearVelocity?: Float32Array, + orientation?: Float32Array, + position?: Float32Array, + ... +}; + +declare class VRFrameData { + leftProjectionMatrix: Float32Array; + leftViewMatrix: Float32Array; + pose: VRPose; + rightProjectionMatrix: Float32Array; + rightViewMatrix: Float32Array; + timestamp: number; +} + +type VREyeParameters = { + offset: Float32Array, + renderWidth: number, + renderHeight: number, + ... +}; + +type VRStageParameters = { + sittingToStandingTransform: Float32Array, + sizeX: number, + sizeZ: number, + ... +}; + +type VRDisplayEventReason = + | 'mounted' + | 'navigation' + | 'requested' + | 'unmounted'; + +type VRDisplayEventInit = { + display: VRDisplay, + reason: VRDisplayEventReason, + ... +}; + +declare class VRDisplayEvent extends Event { + constructor(type: string, eventInitDict: VRDisplayEventInit): void; + display: VRDisplay; + reason?: VRDisplayEventReason; +} + +declare class MediaQueryListEvent { + matches: boolean; + media: string; +} + +declare type MediaQueryListListener = (MediaQueryListEvent) => void; + +declare class MediaQueryList extends EventTarget { + matches: boolean; + media: string; + addListener: MediaQueryListListener => void; + removeListener: MediaQueryListListener => void; + onchange: MediaQueryListListener; +} + +declare var matchMedia: string => MediaQueryList; + +// https://w3c.github.io/webappsec-credential-management/#idl-index +declare type CredMgmtCredentialRequestOptions = { + mediation?: 'silent' | 'optional' | 'required', + signal?: AbortSignal, + ... +}; + +declare type CredMgmtCredentialCreationOptions = {signal: AbortSignal, ...}; + +declare interface CredMgmtCredential { + id: string; + type: string; +} + +declare interface CredMgmtPasswordCredential extends CredMgmtCredential { + password: string; +} + +declare interface CredMgmtCredentialsContainer { + get(option?: CredMgmtCredentialRequestOptions): Promise; + store(credential: CredMgmtCredential): Promise; + create( + creationOption?: CredMgmtCredentialCreationOptions + ): Promise; + preventSilentAccess(): Promise; +} + +type SpeechSynthesisErrorCode = + | 'canceled' + | 'interrupted' + | 'audio-busy' + | 'audio-hardware' + | 'network' + | 'synthesis-unavailable' + | 'synthesis-failed' + | 'language-unavailable' + | 'voice-unavailable' + | 'text-too-long' + | 'invalid-argument' + | 'not-allowed'; + +declare class SpeechSynthesis extends EventTarget { + +pending: boolean; + +speaking: boolean; + +paused: boolean; + + onvoiceschanged: ?(ev: Event) => mixed; + + speak(utterance: SpeechSynthesisUtterance): void; + cancel(): void; + pause(): void; + resume(): void; + getVoices(): Array; +} + +declare var speechSynthesis: SpeechSynthesis; + +declare class SpeechSynthesisUtterance extends EventTarget { + constructor(text?: string): void; + + text: string; + lang: string; + voice: SpeechSynthesisVoice | null; + volume: number; + rate: number; + pitch: number; + + onstart: ?(ev: SpeechSynthesisEvent) => mixed; + onend: ?(ev: SpeechSynthesisEvent) => mixed; + onerror: ?(ev: SpeechSynthesisErrorEvent) => mixed; + onpause: ?(ev: SpeechSynthesisEvent) => mixed; + onresume: ?(ev: SpeechSynthesisEvent) => mixed; + onmark: ?(ev: SpeechSynthesisEvent) => mixed; + onboundary: ?(ev: SpeechSynthesisEvent) => mixed; +} + +type SpeechSynthesisEvent$Init = Event$Init & { + utterance: SpeechSynthesisUtterance, + charIndex?: number, + charLength?: number, + elapsedTime?: number, + name?: string, + ... +}; + +declare class SpeechSynthesisEvent extends Event { + constructor(type: string, eventInitDict?: SpeechSynthesisEvent$Init): void; + + +utterance: SpeechSynthesisUtterance; + charIndex: number; + charLength: number; + elapsedTime: number; + name: string; +} + +type SpeechSynthesisErrorEvent$Init = SpeechSynthesisEvent$Init & { + error: SpeechSynthesisErrorCode, + ... +}; + +declare class SpeechSynthesisErrorEvent extends SpeechSynthesisEvent { + constructor( + type: string, + eventInitDict?: SpeechSynthesisErrorEvent$Init + ): void; + +error: SpeechSynthesisErrorCode; +} + +declare class SpeechSynthesisVoice { + +voiceURI: string; + +name: string; + +lang: string; + +localService: boolean; + +default: boolean; +} + +type SpeechRecognitionErrorCode = + | 'no-speech' + | 'aborted' + | 'audio-capture' + | 'not-allowed' + | 'service-not-allowed' + | 'bad-grammar' + | 'language-not-supported'; + +declare class SpeechGrammar { + constructor(): void; + + src: string; + weight?: number; +} + +declare class SpeechGrammarList { + +length: number; + + item(index: number): SpeechGrammar; + addFromURI(src: string, weight?: number): void; + addFromString(string: string, weight?: number): void; +} + +declare class SpeechRecognitionAlternative { + +transcript: string; + +confidence: number; +} + +declare class SpeechRecognitionResult { + +isFinal: boolean; + +length: number; + + item(index: number): SpeechRecognitionAlternative; +} + +declare class SpeechRecognitionResultList { + +length: number; + + item(index: number): SpeechRecognitionResult; +} + +type SpeechRecognitionEvent$Init = Event$Init & { + emma: any, + interpretation: any, + resultIndex: number, + results: SpeechRecognitionResultList, + ... +}; + +declare class SpeechRecognitionEvent extends Event { + constructor(type: string, eventInitDict?: SpeechRecognitionEvent$Init): void; + + +emma: any; + +interpretation: any; + +resultIndex: number; + +results: SpeechRecognitionResultList; +} + +type SpeechRecognitionErrorEvent$Init = SpeechRecognitionEvent$Init & { + error: SpeechRecognitionErrorCode, + ... +}; + +declare class SpeechRecognitionErrorEvent extends SpeechRecognitionEvent { + constructor( + type: string, + eventInitDict?: SpeechRecognitionErrorEvent$Init + ): void; + +error: SpeechRecognitionErrorCode; + +message: string; +} + +declare class SpeechRecognition extends EventTarget { + constructor(): void; + + +grammars: SpeechGrammar[]; + +lang: string; + +continuous: boolean; + +interimResults: boolean; + +maxAlternatives: number; + +serviceURI: string; + + onaudiostart: ?(ev: Event) => mixed; + onaudioend: ?(ev: Event) => mixed; + onend: ?(ev: Event) => mixed; + onerror: ?(ev: Event) => mixed; + onnomatch: ?(ev: Event) => mixed; + onsoundstart: ?(ev: Event) => mixed; + onsoundend: ?(ev: Event) => mixed; + onspeechstart: ?(ev: Event) => mixed; + onspeechend: ?(ev: Event) => mixed; + onstart: ?(ev: Event) => mixed; + + abort(): void; + start(): void; + stop(): void; +} + +/* Trusted Types + * https://w3c.github.io/trusted-types/dist/spec/#trusted-types + */ +declare class TrustedHTML { + toString(): string; + toJSON(): string; +} + +declare class TrustedScript { + toString(): string; + toJSON(): string; +} + +declare class TrustedScriptURL { + toString(): string; + toJSON(): string; +} + +declare class TrustedTypePolicy { + +name: string; + createHTML(input: string, ...args: Array): TrustedHTML; + createScript(input: string, ...args: Array): TrustedScript; + createScriptURL(input: string, ...args: Array): TrustedScriptURL; +} + +declare type TrustedTypePolicyOptions = {| + createHTML?: (string, ...args: Array) => string, + createScript?: (string, ...args: Array) => string, + createScriptURL?: (string, ...args: Array) => string, +|}; + +// window.trustedTypes?: TrustedTypePolicyFactory +declare class TrustedTypePolicyFactory { + +emptyHTML: TrustedHTML; + +emptyScript: TrustedScript; + +defaultPolicy: ?TrustedTypePolicy; + +isHTML: (value: mixed) => value is TrustedHTML; + +isScript: (value: mixed) => value is TrustedScript; + +isScriptURL: (value: mixed) => value is TrustedScriptURL; + createPolicy( + policyName: string, + policyOptions?: TrustedTypePolicyOptions + ): TrustedTypePolicy; + getAttributeType( + tagName: string, + attribute?: string, + elementNS?: string, + attrNS?: string + ): null | string; + getPropertyType( + tagName: string, + property: string, + elementNS?: string + ): null | string; +} + +// https://wicg.github.io/webusb/ +// https://developer.mozilla.org/en-US/docs/Web/API/USBDevice +declare class USBDevice { + configuration: USBConfiguration; + configurations: Array; + deviceClass: number; + deviceProtocol: number; + deviceSubclass: number; + deviceVersionMajor: number; + deviceVersionMinor: number; + deviceVersionSubminor: number; + manufacturerName: ?string; + opened: boolean; + productId: number; + productName: ?string; + serialNumber: ?string; + usbVersionMajor: number; + usbVersionMinor: number; + usbVersionSubminor: number; + vendorId: number; + claimInterface(interfaceNumber: number): Promise; + clearHalt(direction: 'in' | 'out', endpointNumber: number): Promise; + close(): Promise; + controlTransferIn( + setup: SetUpOptions, + length: number + ): Promise; + controlTransferOut( + setup: SetUpOptions, + data: ArrayBuffer + ): Promise; + forget(): Promise; + isochronousTransferIn( + endpointNumber: number, + packetLengths: Array + ): Promise; + isochronousTransferOut( + endpointNumber: number, + data: ArrayBuffer, + packetLengths: Array + ): Promise; + open(): Promise; + releaseInterface(interfaceNumber: number): Promise; + reset(): Promise; + selectAlternateInterface( + interfaceNumber: number, + alternateSetting: number + ): Promise; + selectConfiguration(configurationValue: number): Promise; + transferIn( + endpointNumber: number, + length: number + ): Promise; + transferOut( + endpointNumber: number, + data: ArrayBuffer + ): Promise; +} + +declare class USB extends EventTarget { + getDevices(): Promise>; + requestDevice(options: USBDeviceRequestOptions): Promise; +} + +declare type USBDeviceFilter = {| + vendorId?: number, + productId?: number, + classCode?: number, + subclassCode?: number, + protocolCode?: number, + serialNumber?: string, +|}; + +declare type USBDeviceRequestOptions = {| + filters: Array, + exclusionFilters?: Array, +|}; + +declare class USBConfiguration { + constructor(): void; + configurationName: ?string; + configurationValue: number; + interfaces: $ReadOnlyArray; +} + +declare class USBInterface { + constructor(): void; + interfaceNumber: number; + alternate: USBAlternateInterface; + alternates: Array; + claimed: boolean; +} + +declare class USBAlternateInterface { + constructor(): void; + alternateSetting: number; + interfaceClass: number; + interfaceSubclass: number; + interfaceProtocol: number; + interfaceName: ?string; + endpoints: Array; +} + +declare class USBEndpoint { + constructor(): void; + endpointNumber: number; + direction: 'in' | 'out'; + type: 'bulk' | 'interrupt' | 'isochronous'; + packetSize: number; +} + +declare class USBOutTransferResult { + constructor(): void; + bytesWritten: number; + status: 'ok' | 'stall'; +} + +declare class USBInTransferResult { + constructor(): void; + data: DataView; + status: 'ok' | 'stall' | 'babble'; +} + +declare class USBIsochronousInTransferResult { + constructor(): void; + data: DataView; + packets: Array; +} + +declare class USBIsochronousInTransferPacket { + constructor(): void; + data: DataView; + status: 'ok' | 'stall' | 'babble'; +} + +declare class USBIsochronousOutTransferResult { + constructor(): void; + packets: Array; +} + +declare class USBIsochronousOutTransferPacket { + constructor(): void; + bytesWritten: number; + status: 'ok' | 'stall'; +} + +type SetUpOptions = { + requestType: string, + recipient: string, + request: number, + value: number, + index: number, + ... +}; + +declare type FileSystemHandleKind = 'file' | 'directory'; + +// https://wicg.github.io/file-system-access/#api-filesystemhandle +declare class FileSystemHandle { + +kind: FileSystemHandleKind; + +name: string; + + isSameEntry: (other: FileSystemHandle) => Promise; + queryPermission?: ( + descriptor: FileSystemHandlePermissionDescriptor + ) => Promise; + requestPermission?: ( + descriptor: FileSystemHandlePermissionDescriptor + ) => Promise; +} + +// https://fs.spec.whatwg.org/#api-filesystemfilehandle +declare class FileSystemFileHandle extends FileSystemHandle { + +kind: 'file'; + + constructor(name: string): void; + + getFile(): Promise; + createSyncAccessHandle(): Promise; + createWritable(options?: {| + keepExistingData?: boolean, + |}): Promise; +} + +// https://fs.spec.whatwg.org/#api-filesystemdirectoryhandle +declare class FileSystemDirectoryHandle extends FileSystemHandle { + +kind: 'directory'; + + constructor(name: string): void; + + getDirectoryHandle( + name: string, + options?: {|create?: boolean|} + ): Promise; + getFileHandle( + name: string, + options?: {|create?: boolean|} + ): Promise; + removeEntry(name: string, options?: {|recursive?: boolean|}): Promise; + resolve(possibleDescendant: FileSystemHandle): Promise | null>; + + // Async iterator functions + @@asyncIterator(): AsyncIterator<[string, FileSystemHandle]>; + entries(): AsyncIterator<[string, FileSystemHandle]>; + keys(): AsyncIterator; + values(): AsyncIterator; +} + +// https://fs.spec.whatwg.org/#api-filesystemsyncaccesshandle +declare class FileSystemSyncAccessHandle { + close(): void; + flush(): void; + getSize(): number; + read(buffer: ArrayBuffer, options?: {|at: number|}): number; + truncate(newSize: number): void; + write(buffer: ArrayBuffer, options?: {|at: number|}): number; +} + +// https://streams.spec.whatwg.org/#default-writer-class +declare class WritableStreamDefaultWriter { + +closed: Promise; + +desiredSize: number; + +ready: Promise; + + constructor(): void; + + abort(reason?: string): Promise; + close(): Promise; + releaseLock(): void; + write(chunk: any): Promise; +} + +// https://streams.spec.whatwg.org/#ws-class +declare class WriteableStream { + +locked: boolean; + + constructor(): void; + + abort(reason: string): Promise; + close(): Promise; + getWriter(): WritableStreamDefaultWriter; +} + +// https://fs.spec.whatwg.org/#dictdef-writeparams +declare type FileSystemWriteableFileStreamDataTypes = + | ArrayBuffer + | $TypedArray + | DataView + | Blob + | string; + +// https://fs.spec.whatwg.org/#dictdef-writeparams +declare type FileSystemWriteableFileStreamData = + | FileSystemWriteableFileStreamDataTypes + | {| + type: 'write', + position?: number, + data: FileSystemWriteableFileStreamDataTypes, + |} + | {| + type: 'seek', + position: number, + data: FileSystemWriteableFileStreamDataTypes, + |} + | {| + type: 'size', + size: number, + |}; + +// https://fs.spec.whatwg.org/#api-filesystemwritablefilestream +declare class FileSystemWritableFileStream extends WriteableStream { + write(data: FileSystemWriteableFileStreamData): Promise; + truncate(size: number): Promise; + seek(position: number): Promise; +} diff --git a/flow-typed/environments/cssom.js b/flow-typed/environments/cssom.js new file mode 100644 index 0000000000000..c70a31cb70fea --- /dev/null +++ b/flow-typed/environments/cssom.js @@ -0,0 +1,414 @@ +// flow-typed signature: ad7b684aa8897ecb82bcc3e009b9fc30 +// flow-typed version: 3e51657e95/cssom/flow_>=v0.261.x + +declare class StyleSheet { + disabled: boolean; + +href: string; + +media: MediaList; + +ownerNode: Node; + +parentStyleSheet: ?StyleSheet; + +title: string; + +type: string; +} + +declare class StyleSheetList { + @@iterator(): Iterator; + length: number; + [index: number]: StyleSheet; +} + +declare class MediaList { + @@iterator(): Iterator; + mediaText: string; + length: number; + item(index: number): ?string; + deleteMedium(oldMedium: string): void; + appendMedium(newMedium: string): void; + [index: number]: string; +} + +declare class CSSStyleSheet extends StyleSheet { + +cssRules: CSSRuleList; + +ownerRule: ?CSSRule; + deleteRule(index: number): void; + insertRule(rule: string, index: number): number; + replace(text: string): Promise; + replaceSync(text: string): void; +} + +declare class CSSGroupingRule extends CSSRule { + +cssRules: CSSRuleList; + deleteRule(index: number): void; + insertRule(rule: string, index: number): number; +} + +declare class CSSConditionRule extends CSSGroupingRule { + conditionText: string; +} + +declare class CSSMediaRule extends CSSConditionRule { + +media: MediaList; +} + +declare class CSSStyleRule extends CSSRule { + selectorText: string; + +style: CSSStyleDeclaration; +} + +declare class CSSSupportsRule extends CSSConditionRule {} + +declare class CSSRule { + cssText: string; + +parentRule: ?CSSRule; + +parentStyleSheet: ?CSSStyleSheet; + +type: number; + static STYLE_RULE: number; + static MEDIA_RULE: number; + static FONT_FACE_RULE: number; + static PAGE_RULE: number; + static IMPORT_RULE: number; + static CHARSET_RULE: number; + static UNKNOWN_RULE: number; + static KEYFRAMES_RULE: number; + static KEYFRAME_RULE: number; + static NAMESPACE_RULE: number; + static COUNTER_STYLE_RULE: number; + static SUPPORTS_RULE: number; + static DOCUMENT_RULE: number; + static FONT_FEATURE_VALUES_RULE: number; + static VIEWPORT_RULE: number; + static REGION_STYLE_RULE: number; +} + +declare class CSSKeyframeRule extends CSSRule { + keyText: string; + +style: CSSStyleDeclaration; +} + +declare class CSSKeyframesRule extends CSSRule { + name: string; + +cssRules: CSSRuleList; + appendRule(rule: string): void; + deleteRule(select: string): void; + findRule(select: string): CSSKeyframeRule | null; +} + +declare class CSSRuleList { + @@iterator(): Iterator; + length: number; + item(index: number): ?CSSRule; + [index: number]: CSSRule; +} + +declare class CSSStyleDeclaration { + @@iterator(): Iterator; + /* DOM CSS Properties */ + alignContent: string; + alignItems: string; + alignSelf: string; + all: string; + animation: string; + animationDelay: string; + animationDirection: string; + animationDuration: string; + animationFillMode: string; + animationIterationCount: string; + animationName: string; + animationPlayState: string; + animationTimingFunction: string; + backdropFilter: string; + webkitBackdropFilter: string; + backfaceVisibility: string; + background: string; + backgroundAttachment: string; + backgroundBlendMode: string; + backgroundClip: string; + backgroundColor: string; + backgroundImage: string; + backgroundOrigin: string; + backgroundPosition: string; + backgroundPositionX: string; + backgroundPositionY: string; + backgroundRepeat: string; + backgroundSize: string; + blockSize: string; + border: string; + borderBlockEnd: string; + borderBlockEndColor: string; + borderBlockEndStyle: string; + borderBlockEndWidth: string; + borderBlockStart: string; + borderBlockStartColor: string; + borderBlockStartStyle: string; + borderBlockStartWidth: string; + borderBottom: string; + borderBottomColor: string; + borderBottomLeftRadius: string; + borderBottomRightRadius: string; + borderBottomStyle: string; + borderBottomWidth: string; + borderCollapse: string; + borderColor: string; + borderImage: string; + borderImageOutset: string; + borderImageRepeat: string; + borderImageSlice: string; + borderImageSource: string; + borderImageWidth: string; + borderInlineEnd: string; + borderInlineEndColor: string; + borderInlineEndStyle: string; + borderInlineEndWidth: string; + borderInlineStart: string; + borderInlineStartColor: string; + borderInlineStartStyle: string; + borderInlineStartWidth: string; + borderLeft: string; + borderLeftColor: string; + borderLeftStyle: string; + borderLeftWidth: string; + borderRadius: string; + borderRight: string; + borderRightColor: string; + borderRightStyle: string; + borderRightWidth: string; + borderSpacing: string; + borderStyle: string; + borderTop: string; + borderTopColor: string; + borderTopLeftRadius: string; + borderTopRightRadius: string; + borderTopStyle: string; + borderTopWidth: string; + borderWidth: string; + bottom: string; + boxDecorationBreak: string; + boxShadow: string; + boxSizing: string; + breakAfter: string; + breakBefore: string; + breakInside: string; + captionSide: string; + clear: string; + clip: string; + clipPath: string; + color: string; + columns: string; + columnCount: string; + columnFill: string; + columnGap: string; + columnRule: string; + columnRuleColor: string; + columnRuleStyle: string; + columnRuleWidth: string; + columnSpan: string; + columnWidth: string; + contain: string; + content: string; + counterIncrement: string; + counterReset: string; + cursor: string; + direction: string; + display: string; + emptyCells: string; + filter: string; + flex: string; + flexBasis: string; + flexDirection: string; + flexFlow: string; + flexGrow: string; + flexShrink: string; + flexWrap: string; + float: string; + font: string; + fontFamily: string; + fontFeatureSettings: string; + fontKerning: string; + fontLanguageOverride: string; + fontSize: string; + fontSizeAdjust: string; + fontStretch: string; + fontStyle: string; + fontSynthesis: string; + fontVariant: string; + fontVariantAlternates: string; + fontVariantCaps: string; + fontVariantEastAsian: string; + fontVariantLigatures: string; + fontVariantNumeric: string; + fontVariantPosition: string; + fontWeight: string; + grad: string; + grid: string; + gridArea: string; + gridAutoColumns: string; + gridAutoFlow: string; + gridAutoPosition: string; + gridAutoRows: string; + gridColumn: string; + gridColumnStart: string; + gridColumnEnd: string; + gridRow: string; + gridRowStart: string; + gridRowEnd: string; + gridTemplate: string; + gridTemplateAreas: string; + gridTemplateRows: string; + gridTemplateColumns: string; + height: string; + hyphens: string; + imageRendering: string; + imageResolution: string; + imageOrientation: string; + imeMode: string; + inherit: string; + initial: string; + inlineSize: string; + isolation: string; + justifyContent: string; + left: string; + letterSpacing: string; + lineBreak: string; + lineHeight: string; + listStyle: string; + listStyleImage: string; + listStylePosition: string; + listStyleType: string; + margin: string; + marginBlockEnd: string; + marginBlockStart: string; + marginBottom: string; + marginInlineEnd: string; + marginInlineStart: string; + marginLeft: string; + marginRight: string; + marginTop: string; + marks: string; + mask: string; + maskType: string; + maxBlockSize: string; + maxHeight: string; + maxInlineSize: string; + maxWidth: string; + minBlockSize: string; + minHeight: string; + minInlineSize: string; + minWidth: string; + mixBlendMode: string; + mozTransform: string; + mozTransformOrigin: string; + mozTransitionDelay: string; + mozTransitionDuration: string; + mozTransitionProperty: string; + mozTransitionTimingFunction: string; + objectFit: string; + objectPosition: string; + offsetBlockEnd: string; + offsetBlockStart: string; + offsetInlineEnd: string; + offsetInlineStart: string; + opacity: string; + order: string; + orphans: string; + outline: string; + outlineColor: string; + outlineOffset: string; + outlineStyle: string; + outlineWidth: string; + overflow: string; + overflowWrap: string; + overflowX: string; + overflowY: string; + padding: string; + paddingBlockEnd: string; + paddingBlockStart: string; + paddingBottom: string; + paddingInlineEnd: string; + paddingInlineStart: string; + paddingLeft: string; + paddingRight: string; + paddingTop: string; + pageBreakAfter: string; + pageBreakBefore: string; + pageBreakInside: string; + perspective: string; + perspectiveOrigin: string; + pointerEvents: string; + position: string; + quotes: string; + rad: string; + resize: string; + right: string; + rubyAlign: string; + rubyMerge: string; + rubyPosition: string; + scrollBehavior: string; + scrollSnapCoordinate: string; + scrollSnapDestination: string; + scrollSnapPointsX: string; + scrollSnapPointsY: string; + scrollSnapType: string; + shapeImageThreshold: string; + shapeMargin: string; + shapeOutside: string; + tableLayout: string; + tabSize: string; + textAlign: string; + textAlignLast: string; + textCombineUpright: string; + textDecoration: string; + textDecorationColor: string; + textDecorationLine: string; + textDecorationStyle: string; + textIndent: string; + textOrientation: string; + textOverflow: string; + textRendering: string; + textShadow: string; + textTransform: string; + textUnderlinePosition: string; + top: string; + touchAction: string; + transform: string; + transformOrigin: string; + transformStyle: string; + transition: string; + transitionDelay: string; + transitionDuration: string; + transitionProperty: string; + transitionTimingFunction: string; + turn: string; + unicodeBidi: string; + unicodeRange: string; + userSelect: string; + verticalAlign: string; + visibility: string; + webkitOverflowScrolling: string; + webkitTransform: string; + webkitTransformOrigin: string; + webkitTransitionDelay: string; + webkitTransitionDuration: string; + webkitTransitionProperty: string; + webkitTransitionTimingFunction: string; + whiteSpace: string; + widows: string; + width: string; + willChange: string; + wordBreak: string; + wordSpacing: string; + wordWrap: string; + writingMode: string; + zIndex: string; + + cssFloat: string; + cssText: string; + getPropertyPriority(property: string): string; + getPropertyValue(property: string): string; + item(index: number): string; + [index: number]: string; + length: number; + parentRule: CSSRule; + removeProperty(property: string): string; + setProperty(property: string, value: ?string, priority: ?string): void; + setPropertyPriority(property: string, priority: string): void; +} diff --git a/flow-typed/environments/dom.js b/flow-typed/environments/dom.js new file mode 100644 index 0000000000000..8e3e757073148 --- /dev/null +++ b/flow-typed/environments/dom.js @@ -0,0 +1,3676 @@ +// flow-typed signature: 2872ddd56ba4b4bfefacac38ebdc6087 +// flow-typed version: 3e51657e95/dom/flow_>=v0.261.x + +/* Files */ + +declare class Blob { + constructor( + blobParts?: Array, + options?: { + type?: string, + endings?: string, + ... + } + ): void; + isClosed: boolean; + size: number; + type: string; + close(): void; + slice(start?: number, end?: number, contentType?: string): Blob; + arrayBuffer(): Promise; + text(): Promise; + stream(): ReadableStream; +} + +declare class FileReader extends EventTarget { + +EMPTY: 0; + +LOADING: 1; + +DONE: 2; + +error: null | DOMError; + +readyState: 0 | 1 | 2; + +result: null | string | ArrayBuffer; + abort(): void; + onabort: null | ((ev: ProgressEvent) => any); + onerror: null | ((ev: ProgressEvent) => any); + onload: null | ((ev: ProgressEvent) => any); + onloadend: null | ((ev: ProgressEvent) => any); + onloadstart: null | ((ev: ProgressEvent) => any); + onprogress: null | ((ev: ProgressEvent) => any); + readAsArrayBuffer(blob: Blob): void; + readAsBinaryString(blob: Blob): void; + readAsDataURL(blob: Blob): void; + readAsText(blob: Blob, encoding?: string): void; +} + +declare type FilePropertyBag = { + type?: string, + lastModified?: number, + ... +}; +declare class File extends Blob { + constructor( + fileBits: $ReadOnlyArray, + filename: string, + options?: FilePropertyBag + ): void; + lastModified: number; + name: string; +} + +declare class FileList { + @@iterator(): Iterator; + length: number; + item(index: number): File; + [index: number]: File; +} + +declare class DOMError { + name: string; +} + +declare interface ShadowRoot extends DocumentFragment { + +delegatesFocus: boolean; + +host: Element; + // flowlint unsafe-getters-setters:off + get innerHTML(): string; + set innerHTML(value: string | TrustedHTML): void; + // flowlint unsafe-getters-setters:error + +mode: ShadowRootMode; + + // From DocumentOrShadowRoot Mixin. + +styleSheets: StyleSheetList; + adoptedStyleSheets: Array; +} + +declare type ShadowRootMode = 'open' | 'closed'; + +declare type ShadowRootInit = { + delegatesFocus?: boolean, + mode: ShadowRootMode, + ... +}; + +declare type ScrollToOptions = { + top?: number, + left?: number, + behavior?: 'auto' | 'smooth', + ... +}; + +type EventHandler = (event: Event) => mixed; +type EventListener = {handleEvent: EventHandler, ...} | EventHandler; +type MouseEventHandler = (event: MouseEvent) => mixed; +type MouseEventListener = + | {handleEvent: MouseEventHandler, ...} + | MouseEventHandler; +type FocusEventHandler = (event: FocusEvent) => mixed; +type FocusEventListener = + | {handleEvent: FocusEventHandler, ...} + | FocusEventHandler; +type KeyboardEventHandler = (event: KeyboardEvent) => mixed; +type KeyboardEventListener = + | {handleEvent: KeyboardEventHandler, ...} + | KeyboardEventHandler; +type InputEventHandler = (event: InputEvent) => mixed; +type InputEventListener = + | {handleEvent: InputEventHandler, ...} + | InputEventHandler; +type TouchEventHandler = (event: TouchEvent) => mixed; +type TouchEventListener = + | {handleEvent: TouchEventHandler, ...} + | TouchEventHandler; +type WheelEventHandler = (event: WheelEvent) => mixed; +type WheelEventListener = + | {handleEvent: WheelEventHandler, ...} + | WheelEventHandler; +type AbortProgressEventHandler = (event: ProgressEvent) => mixed; +type AbortProgressEventListener = + | {handleEvent: AbortProgressEventHandler, ...} + | AbortProgressEventHandler; +type ProgressEventHandler = (event: ProgressEvent) => mixed; +type ProgressEventListener = + | {handleEvent: ProgressEventHandler, ...} + | ProgressEventHandler; +type DragEventHandler = (event: DragEvent) => mixed; +type DragEventListener = + | {handleEvent: DragEventHandler, ...} + | DragEventHandler; +type PointerEventHandler = (event: PointerEvent) => mixed; +type PointerEventListener = + | {handleEvent: PointerEventHandler, ...} + | PointerEventHandler; +type AnimationEventHandler = (event: AnimationEvent) => mixed; +type AnimationEventListener = + | {handleEvent: AnimationEventHandler, ...} + | AnimationEventHandler; +type ClipboardEventHandler = (event: ClipboardEvent) => mixed; +type ClipboardEventListener = + | {handleEvent: ClipboardEventHandler, ...} + | ClipboardEventHandler; +type TransitionEventHandler = (event: TransitionEvent) => mixed; +type TransitionEventListener = + | {handleEvent: TransitionEventHandler, ...} + | TransitionEventHandler; +type MessageEventHandler = (event: MessageEvent) => mixed; +type MessageEventListener = + | {handleEvent: MessageEventHandler, ...} + | MessageEventHandler; +type BeforeUnloadEventHandler = (event: BeforeUnloadEvent) => mixed; +type BeforeUnloadEventListener = + | {handleEvent: BeforeUnloadEventHandler, ...} + | BeforeUnloadEventHandler; +type StorageEventHandler = (event: StorageEvent) => mixed; +type StorageEventListener = + | {handleEvent: StorageEventHandler, ...} + | StorageEventHandler; +type SecurityPolicyViolationEventHandler = ( + event: SecurityPolicyViolationEvent +) => mixed; +type SecurityPolicyViolationEventListener = + | {handleEvent: SecurityPolicyViolationEventHandler, ...} + | SecurityPolicyViolationEventHandler; +type USBConnectionEventHandler = (event: USBConnectionEvent) => mixed; +type USBConnectionEventListener = + | {handleEvent: USBConnectionEventHandler, ...} + | USBConnectionEventHandler; + +type MediaKeySessionType = 'temporary' | 'persistent-license'; +type MediaKeyStatus = + | 'usable' + | 'expired' + | 'released' + | 'output-restricted' + | 'output-downscaled' + | 'status-pending' + | 'internal-error'; +type MouseEventTypes = + | 'contextmenu' + | 'mousedown' + | 'mouseenter' + | 'mouseleave' + | 'mousemove' + | 'mouseout' + | 'mouseover' + | 'mouseup' + | 'click' + | 'dblclick'; +type FocusEventTypes = 'blur' | 'focus' | 'focusin' | 'focusout'; +type KeyboardEventTypes = 'keydown' | 'keyup' | 'keypress'; +type InputEventTypes = 'input' | 'beforeinput'; +type TouchEventTypes = 'touchstart' | 'touchmove' | 'touchend' | 'touchcancel'; +type WheelEventTypes = 'wheel'; +type AbortProgressEventTypes = 'abort'; +type ProgressEventTypes = + | 'abort' + | 'error' + | 'load' + | 'loadend' + | 'loadstart' + | 'progress' + | 'timeout'; +type DragEventTypes = + | 'drag' + | 'dragend' + | 'dragenter' + | 'dragexit' + | 'dragleave' + | 'dragover' + | 'dragstart' + | 'drop'; +type PointerEventTypes = + | 'pointerover' + | 'pointerenter' + | 'pointerdown' + | 'pointermove' + | 'pointerup' + | 'pointercancel' + | 'pointerout' + | 'pointerleave' + | 'gotpointercapture' + | 'lostpointercapture'; +type AnimationEventTypes = + | 'animationstart' + | 'animationend' + | 'animationiteration'; +type ClipboardEventTypes = 'clipboardchange' | 'cut' | 'copy' | 'paste'; +type TransitionEventTypes = + | 'transitionrun' + | 'transitionstart' + | 'transitionend' + | 'transitioncancel'; +type MessageEventTypes = string; +type BeforeUnloadEventTypes = 'beforeunload'; +type StorageEventTypes = 'storage'; +type SecurityPolicyViolationEventTypes = 'securitypolicyviolation'; +type USBConnectionEventTypes = 'connect' | 'disconnect'; +type ToggleEventTypes = 'beforetoggle' | 'toggle'; +type EventListenerOptionsOrUseCapture = + | boolean + | { + capture?: boolean, + once?: boolean, + passive?: boolean, + signal?: AbortSignal, + ... + }; + +declare class EventTarget { + addEventListener( + type: MouseEventTypes, + listener: MouseEventListener, + optionsOrUseCapture?: EventListenerOptionsOrUseCapture + ): void; + addEventListener( + type: FocusEventTypes, + listener: FocusEventListener, + optionsOrUseCapture?: EventListenerOptionsOrUseCapture + ): void; + addEventListener( + type: KeyboardEventTypes, + listener: KeyboardEventListener, + optionsOrUseCapture?: EventListenerOptionsOrUseCapture + ): void; + addEventListener( + type: InputEventTypes, + listener: InputEventListener, + optionsOrUseCapture?: EventListenerOptionsOrUseCapture + ): void; + addEventListener( + type: TouchEventTypes, + listener: TouchEventListener, + optionsOrUseCapture?: EventListenerOptionsOrUseCapture + ): void; + addEventListener( + type: WheelEventTypes, + listener: WheelEventListener, + optionsOrUseCapture?: EventListenerOptionsOrUseCapture + ): void; + addEventListener( + type: AbortProgressEventTypes, + listener: AbortProgressEventListener, + optionsOrUseCapture?: EventListenerOptionsOrUseCapture + ): void; + addEventListener( + type: ProgressEventTypes, + listener: ProgressEventListener, + optionsOrUseCapture?: EventListenerOptionsOrUseCapture + ): void; + addEventListener( + type: DragEventTypes, + listener: DragEventListener, + optionsOrUseCapture?: EventListenerOptionsOrUseCapture + ): void; + addEventListener( + type: PointerEventTypes, + listener: PointerEventListener, + optionsOrUseCapture?: EventListenerOptionsOrUseCapture + ): void; + addEventListener( + type: AnimationEventTypes, + listener: AnimationEventListener, + optionsOrUseCapture?: EventListenerOptionsOrUseCapture + ): void; + addEventListener( + type: ClipboardEventTypes, + listener: ClipboardEventListener, + optionsOrUseCapture?: EventListenerOptionsOrUseCapture + ): void; + addEventListener( + type: TransitionEventTypes, + listener: TransitionEventListener, + optionsOrUseCapture?: EventListenerOptionsOrUseCapture + ): void; + addEventListener( + type: MessageEventTypes, + listener: MessageEventListener, + optionsOrUseCapture?: EventListenerOptionsOrUseCapture + ): void; + addEventListener( + type: BeforeUnloadEventTypes, + listener: BeforeUnloadEventListener, + optionsOrUseCapture?: EventListenerOptionsOrUseCapture + ): void; + addEventListener( + type: StorageEventTypes, + listener: StorageEventListener, + optionsOrUseCapture?: EventListenerOptionsOrUseCapture + ): void; + addEventListener( + type: SecurityPolicyViolationEventTypes, + listener: SecurityPolicyViolationEventListener, + optionsOrUseCapture?: EventListenerOptionsOrUseCapture + ): void; + addEventListener( + type: USBConnectionEventTypes, + listener: USBConnectionEventListener, + optionsOrUseCapture?: EventListenerOptionsOrUseCapture + ): void; + addEventListener( + type: string, + listener: EventListener, + optionsOrUseCapture?: EventListenerOptionsOrUseCapture + ): void; + + removeEventListener( + type: MouseEventTypes, + listener: MouseEventListener, + optionsOrUseCapture?: EventListenerOptionsOrUseCapture + ): void; + removeEventListener( + type: FocusEventTypes, + listener: FocusEventListener, + optionsOrUseCapture?: EventListenerOptionsOrUseCapture + ): void; + removeEventListener( + type: KeyboardEventTypes, + listener: KeyboardEventListener, + optionsOrUseCapture?: EventListenerOptionsOrUseCapture + ): void; + removeEventListener( + type: InputEventTypes, + listener: InputEventListener, + optionsOrUseCapture?: EventListenerOptionsOrUseCapture + ): void; + removeEventListener( + type: TouchEventTypes, + listener: TouchEventListener, + optionsOrUseCapture?: EventListenerOptionsOrUseCapture + ): void; + removeEventListener( + type: WheelEventTypes, + listener: WheelEventListener, + optionsOrUseCapture?: EventListenerOptionsOrUseCapture + ): void; + removeEventListener( + type: AbortProgressEventTypes, + listener: AbortProgressEventListener, + optionsOrUseCapture?: EventListenerOptionsOrUseCapture + ): void; + removeEventListener( + type: ProgressEventTypes, + listener: ProgressEventListener, + optionsOrUseCapture?: EventListenerOptionsOrUseCapture + ): void; + removeEventListener( + type: DragEventTypes, + listener: DragEventListener, + optionsOrUseCapture?: EventListenerOptionsOrUseCapture + ): void; + removeEventListener( + type: PointerEventTypes, + listener: PointerEventListener, + optionsOrUseCapture?: EventListenerOptionsOrUseCapture + ): void; + removeEventListener( + type: AnimationEventTypes, + listener: AnimationEventListener, + optionsOrUseCapture?: EventListenerOptionsOrUseCapture + ): void; + removeEventListener( + type: ClipboardEventTypes, + listener: ClipboardEventListener, + optionsOrUseCapture?: EventListenerOptionsOrUseCapture + ): void; + removeEventListener( + type: TransitionEventTypes, + listener: TransitionEventListener, + optionsOrUseCapture?: EventListenerOptionsOrUseCapture + ): void; + removeEventListener( + type: MessageEventTypes, + listener: MessageEventListener, + optionsOrUseCapture?: EventListenerOptionsOrUseCapture + ): void; + removeEventListener( + type: BeforeUnloadEventTypes, + listener: BeforeUnloadEventListener, + optionsOrUseCapture?: EventListenerOptionsOrUseCapture + ): void; + removeEventListener( + type: StorageEventTypes, + listener: StorageEventListener, + optionsOrUseCapture?: EventListenerOptionsOrUseCapture + ): void; + removeEventListener( + type: SecurityPolicyViolationEventTypes, + listener: SecurityPolicyViolationEventListener, + optionsOrUseCapture?: EventListenerOptionsOrUseCapture + ): void; + removeEventListener( + type: USBConnectionEventTypes, + listener: USBConnectionEventListener, + optionsOrUseCapture?: EventListenerOptionsOrUseCapture + ): void; + removeEventListener( + type: string, + listener: EventListener, + optionsOrUseCapture?: EventListenerOptionsOrUseCapture + ): void; + + attachEvent?: (type: MouseEventTypes, listener: MouseEventListener) => void; + attachEvent?: (type: FocusEventTypes, listener: FocusEventListener) => void; + attachEvent?: ( + type: KeyboardEventTypes, + listener: KeyboardEventListener + ) => void; + attachEvent?: (type: InputEventTypes, listener: InputEventListener) => void; + attachEvent?: (type: TouchEventTypes, listener: TouchEventListener) => void; + attachEvent?: (type: WheelEventTypes, listener: WheelEventListener) => void; + attachEvent?: ( + type: AbortProgressEventTypes, + listener: AbortProgressEventListener + ) => void; + attachEvent?: ( + type: ProgressEventTypes, + listener: ProgressEventListener + ) => void; + attachEvent?: (type: DragEventTypes, listener: DragEventListener) => void; + attachEvent?: ( + type: PointerEventTypes, + listener: PointerEventListener + ) => void; + attachEvent?: ( + type: AnimationEventTypes, + listener: AnimationEventListener + ) => void; + attachEvent?: ( + type: ClipboardEventTypes, + listener: ClipboardEventListener + ) => void; + attachEvent?: ( + type: TransitionEventTypes, + listener: TransitionEventListener + ) => void; + attachEvent?: ( + type: MessageEventTypes, + listener: MessageEventListener + ) => void; + attachEvent?: ( + type: BeforeUnloadEventTypes, + listener: BeforeUnloadEventListener + ) => void; + attachEvent?: ( + type: StorageEventTypes, + listener: StorageEventListener + ) => void; + attachEvent?: ( + type: USBConnectionEventTypes, + listener: USBConnectionEventListener + ) => void; + attachEvent?: (type: string, listener: EventListener) => void; + + detachEvent?: (type: MouseEventTypes, listener: MouseEventListener) => void; + detachEvent?: (type: FocusEventTypes, listener: FocusEventListener) => void; + detachEvent?: ( + type: KeyboardEventTypes, + listener: KeyboardEventListener + ) => void; + detachEvent?: (type: InputEventTypes, listener: InputEventListener) => void; + detachEvent?: (type: TouchEventTypes, listener: TouchEventListener) => void; + detachEvent?: (type: WheelEventTypes, listener: WheelEventListener) => void; + detachEvent?: ( + type: AbortProgressEventTypes, + listener: AbortProgressEventListener + ) => void; + detachEvent?: ( + type: ProgressEventTypes, + listener: ProgressEventListener + ) => void; + detachEvent?: (type: DragEventTypes, listener: DragEventListener) => void; + detachEvent?: ( + type: PointerEventTypes, + listener: PointerEventListener + ) => void; + detachEvent?: ( + type: AnimationEventTypes, + listener: AnimationEventListener + ) => void; + detachEvent?: ( + type: ClipboardEventTypes, + listener: ClipboardEventListener + ) => void; + detachEvent?: ( + type: TransitionEventTypes, + listener: TransitionEventListener + ) => void; + detachEvent?: ( + type: MessageEventTypes, + listener: MessageEventListener + ) => void; + detachEvent?: ( + type: BeforeUnloadEventTypes, + listener: BeforeUnloadEventListener + ) => void; + detachEvent?: ( + type: StorageEventTypes, + listener: StorageEventListener + ) => void; + detachEvent?: ( + type: USBConnectionEventTypes, + listener: USBConnectionEventListener + ) => void; + detachEvent?: (type: string, listener: EventListener) => void; + + dispatchEvent(evt: Event): boolean; + + // Deprecated + + cancelBubble: boolean; + initEvent( + eventTypeArg: string, + canBubbleArg: boolean, + cancelableArg: boolean + ): void; +} + +// https://dom.spec.whatwg.org/#dictdef-eventinit +type Event$Init = { + bubbles?: boolean, + cancelable?: boolean, + composed?: boolean, + /** Non-standard. See `composed` instead. */ + scoped?: boolean, + ... +}; + +// https://dom.spec.whatwg.org/#interface-event +declare class Event { + constructor(type: string, eventInitDict?: Event$Init): void; + /** + * Returns the type of event, e.g. "click", "hashchange", or "submit". + */ + +type: string; + /** + * Returns the object to which event is dispatched (its target). + */ + +target: EventTarget; // TODO: nullable + /** @deprecated */ + +srcElement: Element; // TODO: nullable + /** + * Returns the object whose event listener's callback is currently being invoked. + */ + +currentTarget: EventTarget; // TODO: nullable + /** + * Returns the invocation target objects of event's path (objects on which + * listeners will be invoked), except for any nodes in shadow trees of which + * the shadow root's mode is "closed" that are not reachable from event's + * currentTarget. + */ + composedPath(): Array; + + +NONE: number; + +AT_TARGET: number; + +BUBBLING_PHASE: number; + +CAPTURING_PHASE: number; + /** + * Returns the event's phase, which is one of NONE, CAPTURING_PHASE, AT_TARGET, + * and BUBBLING_PHASE. + */ + +eventPhase: number; + + /** + * When dispatched in a tree, invoking this method prevents event from reaching + * any objects other than the current object. + */ + stopPropagation(): void; + /** + * Invoking this method prevents event from reaching any registered event + * listeners after the current one finishes running and, when dispatched in a + * tree, also prevents event from reaching any other objects. + */ + stopImmediatePropagation(): void; + + /** + * Returns true or false depending on how event was initialized. True if + * event goes through its target's ancestors in reverse tree order, and + * false otherwise. + */ + +bubbles: boolean; + /** + * Returns true or false depending on how event was initialized. Its + * return value does not always carry meaning, but true can indicate + * that part of the operation during which event was dispatched, can + * be canceled by invoking the preventDefault() method. + */ + +cancelable: boolean; + // returnValue: boolean; // legacy, and some subclasses still define it as a string! + /** + * If invoked when the cancelable attribute value is true, and while + * executing a listener for the event with passive set to false, signals to + * the operation that caused event to be dispatched that it needs to be + * canceled. + */ + preventDefault(): void; + /** + * Returns true if preventDefault() was invoked successfully to indicate + * cancelation, and false otherwise. + */ + +defaultPrevented: boolean; + /** + * Returns true or false depending on how event was initialized. True if + * event invokes listeners past a ShadowRoot node that is the root of its + * target, and false otherwise. + */ + +composed: boolean; + + /** + * Returns true if event was dispatched by the user agent, and false otherwise. + */ + +isTrusted: boolean; + /** + * Returns the event's timestamp as the number of milliseconds measured relative + * to the time origin. + */ + +timeStamp: number; + + /** Non-standard. See Event.prototype.composedPath */ + +deepPath?: () => EventTarget[]; + /** Non-standard. See Event.prototype.composed */ + +scoped: boolean; + + /** + * @deprecated + */ + initEvent(type: string, bubbles: boolean, cancelable: boolean): void; +} + +type CustomEvent$Init = {...Event$Init, detail?: any, ...}; + +declare class CustomEvent extends Event { + constructor(type: string, eventInitDict?: CustomEvent$Init): void; + detail: any; + + // deprecated + initCustomEvent( + type: string, + bubbles: boolean, + cancelable: boolean, + detail: any + ): CustomEvent; +} + +type UIEvent$Init = {...Event$Init, detail?: number, view?: any, ...}; + +declare class UIEvent extends Event { + constructor(typeArg: string, uiEventInit?: UIEvent$Init): void; + detail: number; + view: any; +} + +declare class CompositionEvent extends UIEvent { + data: string | null; + locale: string; +} + +type MouseEvent$MouseEventInit = { + screenX?: number, + screenY?: number, + clientX?: number, + clientY?: number, + ctrlKey?: boolean, + shiftKey?: boolean, + altKey?: boolean, + metaKey?: boolean, + button?: number, + buttons?: number, + region?: string | null, + relatedTarget?: EventTarget | null, + ... +}; + +declare class MouseEvent extends UIEvent { + constructor( + typeArg: string, + mouseEventInit?: MouseEvent$MouseEventInit + ): void; + altKey: boolean; + button: number; + buttons: number; + clientX: number; + clientY: number; + ctrlKey: boolean; + metaKey: boolean; + movementX: number; + movementY: number; + offsetX: number; + offsetY: number; + pageX: number; + pageY: number; + region: string | null; + relatedTarget: EventTarget | null; + screenX: number; + screenY: number; + shiftKey: boolean; + x: number; + y: number; + getModifierState(keyArg: string): boolean; +} + +declare class FocusEvent extends UIEvent { + relatedTarget: ?EventTarget; +} + +type WheelEvent$Init = { + ...MouseEvent$MouseEventInit, + deltaX?: number, + deltaY?: number, + deltaZ?: number, + deltaMode?: 0x00 | 0x01 | 0x02, + ... +}; + +declare class WheelEvent extends MouseEvent { + static +DOM_DELTA_PIXEL: 0x00; + static +DOM_DELTA_LINE: 0x01; + static +DOM_DELTA_PAGE: 0x02; + + constructor(type: string, eventInitDict?: WheelEvent$Init): void; + +deltaX: number; + +deltaY: number; + +deltaZ: number; + +deltaMode: 0x00 | 0x01 | 0x02; +} + +declare class DragEvent extends MouseEvent { + dataTransfer: ?DataTransfer; // readonly +} + +type PointerEvent$PointerEventInit = MouseEvent$MouseEventInit & { + pointerId?: number, + width?: number, + height?: number, + pressure?: number, + tangentialPressure?: number, + tiltX?: number, + tiltY?: number, + twist?: number, + pointerType?: string, + isPrimary?: boolean, + ... +}; + +declare class PointerEvent extends MouseEvent { + constructor( + typeArg: string, + pointerEventInit?: PointerEvent$PointerEventInit + ): void; + pointerId: number; + width: number; + height: number; + pressure: number; + tangentialPressure: number; + tiltX: number; + tiltY: number; + twist: number; + pointerType: string; + isPrimary: boolean; +} + +declare class ProgressEvent extends Event { + lengthComputable: boolean; + loaded: number; + total: number; + + // Deprecated + initProgressEvent( + typeArg: string, + canBubbleArg: boolean, + cancelableArg: boolean, + lengthComputableArg: boolean, + loadedArg: number, + totalArg: number + ): void; +} + +declare class PromiseRejectionEvent extends Event { + promise: Promise; + reason: any; +} + +type PageTransitionEventInit = { + ...Event$Init, + persisted: boolean, + ... +}; + +// https://html.spec.whatwg.org/multipage/browsing-the-web.html#the-pagetransitionevent-interface +declare class PageTransitionEvent extends Event { + constructor(type: string, init?: PageTransitionEventInit): void; + +persisted: boolean; +} + +// used for websockets and postMessage, for example. See: +// https://www.w3.org/TR/2011/WD-websockets-20110419/ +// and +// https://www.w3.org/TR/2008/WD-html5-20080610/comms.html +// and +// https://html.spec.whatwg.org/multipage/comms.html#the-messageevent-interfaces +declare class MessageEvent extends Event { + data: mixed; + origin: string; + lastEventId: string; + source: WindowProxy; +} + +// https://w3c.github.io/uievents/#idl-keyboardeventinit +type KeyboardEvent$Init = { + ...UIEvent$Init, + /** + * Initializes the `key` attribute of the KeyboardEvent object to the unicode + * character string representing the meaning of a key after taking into + * account all keyboard modifiers (such as shift-state). This value is the + * final effective value of the key. If the key is not a printable character, + * then it should be one of the key values defined in [UIEvents-Key](https://www.w3.org/TR/uievents-key/). + * + * NOTE: not `null`, this results in `evt.key === 'null'`! + */ + key?: string | void, + /** + * Initializes the `code` attribute of the KeyboardEvent object to the unicode + * character string representing the key that was pressed, ignoring any + * keyboard modifications such as keyboard layout. This value should be one + * of the code values defined in [UIEvents-Code](https://www.w3.org/TR/uievents-code/). + * + * NOTE: not `null`, this results in `evt.code === 'null'`! + */ + code?: string | void, + /** + * Initializes the `location` attribute of the KeyboardEvent object to one of + * the following location numerical constants: + * + * DOM_KEY_LOCATION_STANDARD (numerical value 0) + * DOM_KEY_LOCATION_LEFT (numerical value 1) + * DOM_KEY_LOCATION_RIGHT (numerical value 2) + * DOM_KEY_LOCATION_NUMPAD (numerical value 3) + */ + location?: number, + /** + * Initializes the `ctrlKey` attribute of the KeyboardEvent object to true if + * the Control key modifier is to be considered active, false otherwise. + */ + ctrlKey?: boolean, + /** + * Initializes the `shiftKey` attribute of the KeyboardEvent object to true if + * the Shift key modifier is to be considered active, false otherwise. + */ + shiftKey?: boolean, + /** + * Initializes the `altKey` attribute of the KeyboardEvent object to true if + * the Alt (alternative) (or Option) key modifier is to be considered active, + * false otherwise. + */ + altKey?: boolean, + /** + * Initializes the `metaKey` attribute of the KeyboardEvent object to true if + * the Meta key modifier is to be considered active, false otherwise. + */ + metaKey?: boolean, + /** + * Initializes the `repeat` attribute of the KeyboardEvent object. This + * attribute should be set to true if the the current KeyboardEvent is + * considered part of a repeating sequence of similar events caused by the + * long depression of any single key, false otherwise. + */ + repeat?: boolean, + /** + * Initializes the `isComposing` attribute of the KeyboardEvent object. This + * attribute should be set to true if the event being constructed occurs as + * part of a composition sequence, false otherwise. + */ + isComposing?: boolean, + /** + * Initializes the `charCode` attribute of the KeyboardEvent to the Unicode + * code point for the event’s character. + */ + charCode?: number, + /** + * Initializes the `keyCode` attribute of the KeyboardEvent to the system- + * and implementation-dependent numerical code signifying the unmodified + * identifier associated with the key pressed. + */ + keyCode?: number, + /** Initializes the `which` attribute */ + which?: number, + ... +}; + +// https://w3c.github.io/uievents/#idl-keyboardevent +declare class KeyboardEvent extends UIEvent { + constructor(typeArg: string, init?: KeyboardEvent$Init): void; + + /** `true` if the Alt (alternative) (or "Option") key modifier was active. */ + +altKey: boolean; + /** + * Holds a string that identifies the physical key being pressed. The value + * is not affected by the current keyboard layout or modifier state, so a + * particular key will always return the same value. + */ + +code: string; + /** `true` if the Control (control) key modifier was active. */ + +ctrlKey: boolean; + /** + * `true` if the key event occurs as part of a composition session, i.e., + * after a `compositionstart` event and before the corresponding + * `compositionend` event. + */ + +isComposing: boolean; + /** + * Holds a [key attribute value](https://www.w3.org/TR/uievents-key/#key-attribute-value) + * corresponding to the key pressed. */ + +key: string; + /** An indication of the logical location of the key on the device. */ + +location: number; + /** `true` if the meta (Meta) key (or "Command") modifier was active. */ + +metaKey: boolean; + /** `true` if the key has been pressed in a sustained manner. */ + +repeat: boolean; + /** `true` if the shift (Shift) key modifier was active. */ + +shiftKey: boolean; + + /** + * Queries the state of a modifier using a key value. + * + * Returns `true` if it is a modifier key and the modifier is activated, + * `false` otherwise. + */ + getModifierState(keyArg?: string): boolean; + + /** + * Holds a character value, for keypress events which generate character + * input. The value is the Unicode reference number (code point) of that + * character (e.g. event.charCode = event.key.charCodeAt(0) for printable + * characters). For keydown or keyup events, the value of charCode is 0. + * + * @deprecated You should use KeyboardEvent.key instead, if available. + */ + +charCode: number; + /** + * Holds a system- and implementation-dependent numerical code signifying + * the unmodified identifier associated with the key pressed. Unlike the + * `key` attribute, the set of possible values are not normatively defined. + * Typically, these value of the keyCode SHOULD represent the decimal + * codepoint in ASCII or Windows 1252, but MAY be drawn from a different + * appropriate character set. Implementations that are unable to identify + * a key use the key value 0. + * + * @deprecated You should use KeyboardEvent.key instead, if available. + */ + +keyCode: number; + /** + * Holds a system- and implementation-dependent numerical code signifying + * the unmodified identifier associated with the key pressed. In most cases, + * the value is identical to keyCode. + * + * @deprecated You should use KeyboardEvent.key instead, if available. + */ + +which: number; +} + +type InputEvent$Init = { + ...UIEvent$Init, + inputType?: string, + data?: string, + dataTransfer?: DataTransfer, + isComposing?: boolean, + ranges?: Array, // TODO: StaticRange + ... +}; + +declare class InputEvent extends UIEvent { + constructor(typeArg: string, inputEventInit: InputEvent$Init): void; + +data: string | null; + +dataTransfer: DataTransfer | null; + +inputType: string; + +isComposing: boolean; + getTargetRanges(): Array; // TODO: StaticRange +} + +declare class AnimationEvent extends Event { + animationName: string; + elapsedTime: number; + pseudoElement: string; + + // deprecated + + initAnimationEvent: ( + type: 'animationstart' | 'animationend' | 'animationiteration', + canBubble: boolean, + cancelable: boolean, + animationName: string, + elapsedTime: number + ) => void; +} + +// https://www.w3.org/TR/touch-events/#idl-def-Touch +declare class Touch { + clientX: number; + clientY: number; + identifier: number; + pageX: number; + pageY: number; + screenX: number; + screenY: number; + target: EventTarget; +} + +// https://www.w3.org/TR/touch-events/#idl-def-TouchList +// TouchList#item(index) will return null if n > #length. Should #item's +// return type just been Touch? +declare class TouchList { + @@iterator(): Iterator; + length: number; + item(index: number): null | Touch; + [index: number]: Touch; +} + +// https://www.w3.org/TR/touch-events/#touchevent-interface +declare class TouchEvent extends UIEvent { + altKey: boolean; + changedTouches: TouchList; + ctrlKey: boolean; + metaKey: boolean; + shiftKey: boolean; + targetTouches: TouchList; + touches: TouchList; +} + +// https://www.w3.org/TR/clipboard-apis/#typedefdef-clipboarditemdata +// Raw string | Blob are allowed per https://webidl.spec.whatwg.org/#es-promise +type ClipboardItemData = string | Blob | Promise; + +type PresentationStyle = 'attachment' | 'inline' | 'unspecified'; + +type ClipboardItemOptions = { + presentationStyle?: PresentationStyle, + ... +}; + +declare class ClipboardItem { + +types: $ReadOnlyArray; + getType(type: string): Promise; + constructor( + items: {[type: string]: ClipboardItemData}, + options?: ClipboardItemOptions + ): void; +} + +// https://w3c.github.io/clipboard-apis/ as of 15 May 2018 +type ClipboardEvent$Init = { + ...Event$Init, + clipboardData: DataTransfer | null, + ... +}; + +declare class ClipboardEvent extends Event { + constructor(type: ClipboardEventTypes, eventInit?: ClipboardEvent$Init): void; + +clipboardData: ?DataTransfer; // readonly +} + +// https://www.w3.org/TR/2017/WD-css-transitions-1-20171130/#interface-transitionevent +type TransitionEvent$Init = { + ...Event$Init, + propertyName: string, + elapsedTime: number, + pseudoElement: string, + ... +}; + +declare class TransitionEvent extends Event { + constructor( + type: TransitionEventTypes, + eventInit?: TransitionEvent$Init + ): void; + + +propertyName: string; // readonly + +elapsedTime: number; // readonly + +pseudoElement: string; // readonly +} + +declare class SecurityPolicyViolationEvent extends Event { + +documentURI: string; + +referrer: string; + +blockedURI: string; + +effectiveDirective: string; + +violatedDirective: string; + +originalPolicy: string; + +sourceFile: string; + +sample: string; + +disposition: 'enforce' | 'report'; + +statusCode: number; + +lineNumber: number; + +columnNumber: number; +} + +// https://developer.mozilla.org/en-US/docs/Web/API/USBConnectionEvent +declare class USBConnectionEvent extends Event { + device: USBDevice; +} + +// TODO: *Event + +declare class AbortController { + constructor(): void; + +signal: AbortSignal; + abort(reason?: any): void; +} + +declare class AbortSignal extends EventTarget { + +aborted: boolean; + +reason: any; + abort(reason?: any): AbortSignal; + onabort: (event: Event) => mixed; + throwIfAborted(): void; + timeout(time: number): AbortSignal; +} + +declare class Node extends EventTarget { + baseURI: ?string; + childNodes: NodeList; + firstChild: ?Node; + +isConnected: boolean; + lastChild: ?Node; + nextSibling: ?Node; + nodeName: string; + nodeType: number; + nodeValue: string; + ownerDocument: Document; + parentElement: ?Element; + parentNode: ?Node; + previousSibling: ?Node; + rootNode: Node; + textContent: string; + appendChild(newChild: T): T; + cloneNode(deep?: boolean): this; + compareDocumentPosition(other: Node): number; + contains(other: ?Node): boolean; + getRootNode(options?: {composed: boolean, ...}): Node; + hasChildNodes(): boolean; + insertBefore(newChild: T, refChild?: ?Node): T; + isDefaultNamespace(namespaceURI: string): boolean; + isEqualNode(arg: Node): boolean; + isSameNode(other: Node): boolean; + lookupNamespaceURI(prefix: string): string; + lookupPrefix(namespaceURI: string): string; + normalize(): void; + removeChild(oldChild: T): T; + replaceChild(newChild: Node, oldChild: T): T; + replaceChildren(...nodes: $ReadOnlyArray): void; + static ATTRIBUTE_NODE: number; + static CDATA_SECTION_NODE: number; + static COMMENT_NODE: number; + static DOCUMENT_FRAGMENT_NODE: number; + static DOCUMENT_NODE: number; + static DOCUMENT_POSITION_CONTAINED_BY: number; + static DOCUMENT_POSITION_CONTAINS: number; + static DOCUMENT_POSITION_DISCONNECTED: number; + static DOCUMENT_POSITION_FOLLOWING: number; + static DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC: number; + static DOCUMENT_POSITION_PRECEDING: number; + static DOCUMENT_TYPE_NODE: number; + static ELEMENT_NODE: number; + static ENTITY_NODE: number; + static ENTITY_REFERENCE_NODE: number; + static NOTATION_NODE: number; + static PROCESSING_INSTRUCTION_NODE: number; + static TEXT_NODE: number; + + // Non-standard + innerText?: string; + outerText?: string; +} + +declare class NodeList { + @@iterator(): Iterator; + length: number; + item(index: number): T; + [index: number]: T; + + forEach( + callbackfn: (this: This, value: T, index: number, list: NodeList) => any, + thisArg: This + ): void; + entries(): Iterator<[number, T]>; + keys(): Iterator; + values(): Iterator; +} + +declare class NamedNodeMap { + @@iterator(): Iterator; + length: number; + removeNamedItemNS(namespaceURI: string, localName: string): Attr; + item(index: number): Attr; + [index: number | string]: Attr; + removeNamedItem(name: string): Attr; + getNamedItem(name: string): Attr; + setNamedItem(arg: Attr): Attr; + getNamedItemNS(namespaceURI: string, localName: string): Attr; + setNamedItemNS(arg: Attr): Attr; +} + +declare class Attr extends Node { + isId: boolean; + specified: boolean; + ownerElement: Element | null; + value: string; + name: string; + namespaceURI: string | null; + prefix: string | null; + localName: string; +} + +declare class HTMLCollection<+Elem: Element> { + @@iterator(): Iterator; + length: number; + item(nameOrIndex?: any, optionalIndex?: any): Elem | null; + namedItem(name: string): Elem | null; + [index: number | string]: Elem; +} + +// from https://www.w3.org/TR/custom-elements/#extensions-to-document-interface-to-register +// See also https://github.com/w3c/webcomponents/ +type ElementRegistrationOptions = { + +prototype?: { + // from https://www.w3.org/TR/custom-elements/#types-of-callbacks + // See also https://github.com/w3c/webcomponents/ + +createdCallback?: () => mixed, + +attachedCallback?: () => mixed, + +detachedCallback?: () => mixed, + +attributeChangedCallback?: (( + // attribute is set + attributeLocalName: string, + oldAttributeValue: null, + newAttributeValue: string, + attributeNamespace: string + ) => mixed) & + // attribute is changed + (( + attributeLocalName: string, + oldAttributeValue: string, + newAttributeValue: string, + attributeNamespace: string + ) => mixed) & + // attribute is removed + (( + attributeLocalName: string, + oldAttributeValue: string, + newAttributeValue: null, + attributeNamespace: string + ) => mixed), + ... + }, + +extends?: string, + ... +}; + +type ElementCreationOptions = {is: string, ...}; + +declare class MutationRecord { + type: 'attributes' | 'characterData' | 'childList'; + target: Node; + addedNodes: NodeList; + removedNodes: NodeList; + previousSibling: ?Node; + nextSibling: ?Node; + attributeName: ?string; + attributeNamespace: ?string; + oldValue: ?string; +} + +type MutationObserverInitRequired = + | {childList: true, ...} + | {attributes: true, ...} + | {characterData: true, ...}; + +declare type MutationObserverInit = MutationObserverInitRequired & { + subtree?: boolean, + attributeOldValue?: boolean, + characterDataOldValue?: boolean, + attributeFilter?: Array, + ... +}; + +declare class MutationObserver { + constructor( + callback: (arr: Array, observer: MutationObserver) => mixed + ): void; + observe(target: Node, options: MutationObserverInit): void; + takeRecords(): Array; + disconnect(): void; +} + +declare class Document extends Node { + +timeline: DocumentTimeline; + getAnimations(): Array; + +URL: string; + adoptNode(source: T): T; + anchors: HTMLCollection; + applets: HTMLCollection; + body: HTMLBodyElement | null; + +characterSet: string; + /** + * Legacy alias of `characterSet` + * @deprecated + */ + +charset: string; + close(): void; + +contentType: string; + cookie: string; + createAttribute(name: string): Attr; + createAttributeNS(namespaceURI: string | null, qualifiedName: string): Attr; + createCDATASection(data: string): Text; + createComment(data: string): Comment; + createDocumentFragment(): DocumentFragment; + createElement>( + localName: TName, + options?: string | ElementCreationOptions + ): HTMLElementTagNameMap[TName]; + createElementNS>( + namespaceURI: 'http://www.w3.org/1999/xhtml', + qualifiedName: TName, + options?: string | ElementCreationOptions + ): HTMLElementTagNameMap[TName]; + createElementNS( + namespaceURI: string | null, + qualifiedName: string, + options?: string | ElementCreationOptions + ): Element; + createTextNode(data: string): Text; + currentScript: HTMLScriptElement | null; + dir: 'rtl' | 'ltr'; + +doctype: DocumentType | null; + +documentElement: HTMLElement | null; + documentMode: number; + +documentURI: string; + domain: string | null; + embeds: HTMLCollection; + exitFullscreen(): Promise; + queryCommandSupported(cmdID: string): boolean; + execCommand(cmdID: string, showUI?: boolean, value?: any): boolean; + forms: HTMLCollection; + fullscreenElement: Element | null; + fullscreenEnabled: boolean; + getElementsByClassName(classNames: string): HTMLCollection; + getElementsByName(elementName: string): HTMLCollection; + getElementsByTagName>( + qualifiedName: TName + ): HTMLCollection; + getElementsByTagNameNS>( + namespaceURI: 'http://www.w3.org/1999/xhtml', + qualifiedName: TName + ): HTMLCollection; + getElementsByTagNameNS( + namespaceURI: string | null, + qualifiedName: string + ): HTMLCollection; + head: HTMLHeadElement | null; + images: HTMLCollection; + +implementation: DOMImplementation; + importNode(importedNode: T, deep: boolean): T; + /** + * Legacy alias of `characterSet` + * @deprecated + */ + +inputEncoding: string; + lastModified: string; + links: HTMLCollection; + media: string; + open(url?: string, name?: string, features?: string, replace?: boolean): any; + readyState: string; + referrer: string; + scripts: HTMLCollection; + scrollingElement: HTMLElement | null; + title: string; + visibilityState: 'visible' | 'hidden' | 'prerender' | 'unloaded'; + write(...content: Array): void; + writeln(...content: Array): void; + xmlEncoding: string; + xmlStandalone: boolean; + xmlVersion: string; + + registerElement(type: string, options?: ElementRegistrationOptions): any; + getSelection(): Selection | null; + + // 6.4.6 Focus management APIs + activeElement: HTMLElement | null; + hasFocus(): boolean; + + // extension + location: Location; + createEvent(eventInterface: 'CustomEvent'): CustomEvent; + createEvent(eventInterface: string): Event; + createRange(): Range; + elementFromPoint(x: number, y: number): HTMLElement | null; + elementsFromPoint(x: number, y: number): Array; + defaultView: any; + +compatMode: 'BackCompat' | 'CSS1Compat'; + hidden: boolean; + + // Pointer Lock specification + exitPointerLock(): void; + pointerLockElement: Element | null; + + // from ParentNode interface + childElementCount: number; + children: HTMLCollection; + firstElementChild: ?Element; + lastElementChild: ?Element; + append(...nodes: Array): void; + prepend(...nodes: Array): void; + + querySelector>( + selector: TSelector + ): HTMLElementTagNameMap[TSelector] | null; + querySelectorAll>( + selector: TSelector + ): NodeList; + // Interface DocumentTraversal + // http://www.w3.org/TR/2000/REC-DOM-Level-2-Traversal-Range-20001113/traversal.html#Traversal-Document + + // Not all combinations of RootNodeT and whatToShow are logically possible. + // The bitmasks NodeFilter.SHOW_CDATA_SECTION, + // NodeFilter.SHOW_ENTITY_REFERENCE, NodeFilter.SHOW_ENTITY, and + // NodeFilter.SHOW_NOTATION are deprecated and do not correspond to types + // that Flow knows about. + + // NodeFilter.SHOW_ATTRIBUTE is also deprecated, but corresponds to the + // type Attr. While there is no reason to prefer it to Node.attributes, + // it does have meaning and can be typed: When (whatToShow & + // NodeFilter.SHOW_ATTRIBUTE === 1), RootNodeT must be Attr, and when + // RootNodeT is Attr, bitmasks other than NodeFilter.SHOW_ATTRIBUTE are + // meaningless. + createNodeIterator( + root: RootNodeT, + whatToShow: 2, + filter?: NodeFilterInterface + ): NodeIterator; + createTreeWalker( + root: RootNodeT, + whatToShow: 2, + filter?: NodeFilterInterface, + entityReferenceExpansion?: boolean + ): TreeWalker; + + // NodeFilter.SHOW_PROCESSING_INSTRUCTION is not implemented because Flow + // does not currently define a ProcessingInstruction class. + + // When (whatToShow & NodeFilter.SHOW_DOCUMENT === 1 || whatToShow & + // NodeFilter.SHOW_DOCUMENT_TYPE === 1), RootNodeT must be Document. + createNodeIterator( + root: RootNodeT, + whatToShow: 256, + filter?: NodeFilterInterface + ): NodeIterator; + createNodeIterator( + root: RootNodeT, + whatToShow: 257, + filter?: NodeFilterInterface + ): NodeIterator; + createNodeIterator( + root: RootNodeT, + whatToShow: 260, + filter?: NodeFilterInterface + ): NodeIterator; + createNodeIterator( + root: RootNodeT, + whatToShow: 261, + filter?: NodeFilterInterface + ): NodeIterator; + createNodeIterator( + root: RootNodeT, + whatToShow: 384, + filter?: NodeFilterInterface + ): NodeIterator; + createNodeIterator( + root: RootNodeT, + whatToShow: 385, + filter?: NodeFilterInterface + ): NodeIterator; + createNodeIterator( + root: RootNodeT, + whatToShow: 388, + filter?: NodeFilterInterface + ): NodeIterator; + createNodeIterator( + root: RootNodeT, + whatToShow: 389, + filter?: NodeFilterInterface + ): NodeIterator; + createNodeIterator( + root: RootNodeT, + whatToShow: 512, + filter?: NodeFilterInterface + ): NodeIterator; + createNodeIterator( + root: RootNodeT, + whatToShow: 513, + filter?: NodeFilterInterface + ): NodeIterator; + createNodeIterator( + root: RootNodeT, + whatToShow: 516, + filter?: NodeFilterInterface + ): NodeIterator; + createNodeIterator( + root: RootNodeT, + whatToShow: 517, + filter?: NodeFilterInterface + ): NodeIterator; + createNodeIterator( + root: RootNodeT, + whatToShow: 640, + filter?: NodeFilterInterface + ): NodeIterator; + createNodeIterator( + root: RootNodeT, + whatToShow: 641, + filter?: NodeFilterInterface + ): NodeIterator; + createNodeIterator( + root: RootNodeT, + whatToShow: 644, + filter?: NodeFilterInterface + ): NodeIterator; + createNodeIterator( + root: RootNodeT, + whatToShow: 645, + filter?: NodeFilterInterface + ): NodeIterator; + createNodeIterator( + root: RootNodeT, + whatToShow: 768, + filter?: NodeFilterInterface + ): NodeIterator; + createNodeIterator( + root: RootNodeT, + whatToShow: 769, + filter?: NodeFilterInterface + ): NodeIterator; + createNodeIterator( + root: RootNodeT, + whatToShow: 772, + filter?: NodeFilterInterface + ): NodeIterator; + createNodeIterator( + root: RootNodeT, + whatToShow: 773, + filter?: NodeFilterInterface + ): NodeIterator; + createNodeIterator( + root: RootNodeT, + whatToShow: 896, + filter?: NodeFilterInterface + ): NodeIterator; + createNodeIterator( + root: RootNodeT, + whatToShow: 897, + filter?: NodeFilterInterface + ): NodeIterator; + createNodeIterator( + root: RootNodeT, + whatToShow: 900, + filter?: NodeFilterInterface + ): NodeIterator; + createNodeIterator( + root: RootNodeT, + whatToShow: 901, + filter?: NodeFilterInterface + ): NodeIterator< + RootNodeT, + DocumentType | Document | Element | Text | Comment, + >; + createTreeWalker( + root: RootNodeT, + whatToShow: 256, + filter?: NodeFilterInterface, + entityReferenceExpansion?: boolean + ): TreeWalker; + createTreeWalker( + root: RootNodeT, + whatToShow: 257, + filter?: NodeFilterInterface, + entityReferenceExpansion?: boolean + ): TreeWalker; + createTreeWalker( + root: RootNodeT, + whatToShow: 260, + filter?: NodeFilterInterface, + entityReferenceExpansion?: boolean + ): TreeWalker; + createTreeWalker( + root: RootNodeT, + whatToShow: 261, + filter?: NodeFilterInterface, + entityReferenceExpansion?: boolean + ): TreeWalker; + createTreeWalker( + root: RootNodeT, + whatToShow: 384, + filter?: NodeFilterInterface, + entityReferenceExpansion?: boolean + ): TreeWalker; + createTreeWalker( + root: RootNodeT, + whatToShow: 385, + filter?: NodeFilterInterface, + entityReferenceExpansion?: boolean + ): TreeWalker; + createTreeWalker( + root: RootNodeT, + whatToShow: 388, + filter?: NodeFilterInterface, + entityReferenceExpansion?: boolean + ): TreeWalker; + createTreeWalker( + root: RootNodeT, + whatToShow: 389, + filter?: NodeFilterInterface, + entityReferenceExpansion?: boolean + ): TreeWalker; + createTreeWalker( + root: RootNodeT, + whatToShow: 512, + filter?: NodeFilterInterface, + entityReferenceExpansion?: boolean + ): TreeWalker; + createTreeWalker( + root: RootNodeT, + whatToShow: 513, + filter?: NodeFilterInterface, + entityReferenceExpansion?: boolean + ): TreeWalker; + createTreeWalker( + root: RootNodeT, + whatToShow: 516, + filter?: NodeFilterInterface, + entityReferenceExpansion?: boolean + ): TreeWalker; + createTreeWalker( + root: RootNodeT, + whatToShow: 517, + filter?: NodeFilterInterface, + entityReferenceExpansion?: boolean + ): TreeWalker; + createTreeWalker( + root: RootNodeT, + whatToShow: 640, + filter?: NodeFilterInterface, + entityReferenceExpansion?: boolean + ): TreeWalker; + createTreeWalker( + root: RootNodeT, + whatToShow: 641, + filter?: NodeFilterInterface, + entityReferenceExpansion?: boolean + ): TreeWalker; + createTreeWalker( + root: RootNodeT, + whatToShow: 644, + filter?: NodeFilterInterface, + entityReferenceExpansion?: boolean + ): TreeWalker; + createTreeWalker( + root: RootNodeT, + whatToShow: 645, + filter?: NodeFilterInterface, + entityReferenceExpansion?: boolean + ): TreeWalker; + createTreeWalker( + root: RootNodeT, + whatToShow: 768, + filter?: NodeFilterInterface, + entityReferenceExpansion?: boolean + ): TreeWalker; + createTreeWalker( + root: RootNodeT, + whatToShow: 769, + filter?: NodeFilterInterface, + entityReferenceExpansion?: boolean + ): TreeWalker; + createTreeWalker( + root: RootNodeT, + whatToShow: 772, + filter?: NodeFilterInterface, + entityReferenceExpansion?: boolean + ): TreeWalker; + createTreeWalker( + root: RootNodeT, + whatToShow: 773, + filter?: NodeFilterInterface, + entityReferenceExpansion?: boolean + ): TreeWalker; + createTreeWalker( + root: RootNodeT, + whatToShow: 896, + filter?: NodeFilterInterface, + entityReferenceExpansion?: boolean + ): TreeWalker; + createTreeWalker( + root: RootNodeT, + whatToShow: 897, + filter?: NodeFilterInterface, + entityReferenceExpansion?: boolean + ): TreeWalker; + createTreeWalker( + root: RootNodeT, + whatToShow: 900, + filter?: NodeFilterInterface, + entityReferenceExpansion?: boolean + ): TreeWalker; + createTreeWalker( + root: RootNodeT, + whatToShow: 901, + filter?: NodeFilterInterface, + entityReferenceExpansion?: boolean + ): TreeWalker; + + // When (whatToShow & NodeFilter.SHOW_DOCUMENT_FRAGMENT === 1), RootNodeT + // must be a DocumentFragment. + createNodeIterator( + root: RootNodeT, + whatToShow: 1024, + filter?: NodeFilterInterface + ): NodeIterator; + createNodeIterator( + root: RootNodeT, + whatToShow: 1025, + filter?: NodeFilterInterface + ): NodeIterator; + createNodeIterator( + root: RootNodeT, + whatToShow: 1028, + filter?: NodeFilterInterface + ): NodeIterator; + createNodeIterator( + root: RootNodeT, + whatToShow: 1029, + filter?: NodeFilterInterface + ): NodeIterator; + createNodeIterator( + root: RootNodeT, + whatToShow: 1152, + filter?: NodeFilterInterface + ): NodeIterator; + createNodeIterator( + root: RootNodeT, + whatToShow: 1153, + filter?: NodeFilterInterface + ): NodeIterator; + createNodeIterator( + root: RootNodeT, + whatToShow: 1156, + filter?: NodeFilterInterface + ): NodeIterator; + createNodeIterator( + root: RootNodeT, + whatToShow: 1157, + filter?: NodeFilterInterface + ): NodeIterator; + createTreeWalker( + root: RootNodeT, + whatToShow: 1024, + filter?: NodeFilterInterface, + entityReferenceExpansion?: boolean + ): TreeWalker; + createTreeWalker( + root: RootNodeT, + whatToShow: 1025, + filter?: NodeFilterInterface, + entityReferenceExpansion?: boolean + ): TreeWalker; + createTreeWalker( + root: RootNodeT, + whatToShow: 1028, + filter?: NodeFilterInterface, + entityReferenceExpansion?: boolean + ): TreeWalker; + createTreeWalker( + root: RootNodeT, + whatToShow: 1029, + filter?: NodeFilterInterface, + entityReferenceExpansion?: boolean + ): TreeWalker; + createTreeWalker( + root: RootNodeT, + whatToShow: 1152, + filter?: NodeFilterInterface, + entityReferenceExpansion?: boolean + ): TreeWalker; + createTreeWalker( + root: RootNodeT, + whatToShow: 1153, + filter?: NodeFilterInterface, + entityReferenceExpansion?: boolean + ): TreeWalker; + createTreeWalker( + root: RootNodeT, + whatToShow: 1156, + filter?: NodeFilterInterface, + entityReferenceExpansion?: boolean + ): TreeWalker; + createTreeWalker( + root: RootNodeT, + whatToShow: 1157, + filter?: NodeFilterInterface, + entityReferenceExpansion?: boolean + ): TreeWalker; + + // In the general case, RootNodeT may be any Node and whatToShow may be + // NodeFilter.SHOW_ALL or any combination of NodeFilter.SHOW_ELEMENT, + // NodeFilter.SHOW_TEXT and/or NodeFilter.SHOW_COMMENT + createNodeIterator( + root: RootNodeT, + whatToShow: 1, + filter?: NodeFilterInterface + ): NodeIterator; + createNodeIterator( + root: RootNodeT, + whatToShow: 4, + filter?: NodeFilterInterface + ): NodeIterator; + createNodeIterator( + root: RootNodeT, + whatToShow: 5, + filter?: NodeFilterInterface + ): NodeIterator; + createNodeIterator( + root: RootNodeT, + whatToShow: 128, + filter?: NodeFilterInterface + ): NodeIterator; + createNodeIterator( + root: RootNodeT, + whatToShow: 129, + filter?: NodeFilterInterface + ): NodeIterator; + createNodeIterator( + root: RootNodeT, + whatToShow: 132, + filter?: NodeFilterInterface + ): NodeIterator; + createNodeIterator( + root: RootNodeT, + whatToShow: 133, + filter?: NodeFilterInterface + ): NodeIterator; + createTreeWalker( + root: RootNodeT, + whatToShow: 1, + filter?: NodeFilterInterface, + entityReferenceExpansion?: boolean + ): TreeWalker; + createTreeWalker( + root: RootNodeT, + whatToShow: 4, + filter?: NodeFilterInterface, + entityReferenceExpansion?: boolean + ): TreeWalker; + createTreeWalker( + root: RootNodeT, + whatToShow: 5, + filter?: NodeFilterInterface, + entityReferenceExpansion?: boolean + ): TreeWalker; + createTreeWalker( + root: RootNodeT, + whatToShow: 128, + filter?: NodeFilterInterface, + entityReferenceExpansion?: boolean + ): TreeWalker; + createTreeWalker( + root: RootNodeT, + whatToShow: 129, + filter?: NodeFilterInterface, + entityReferenceExpansion?: boolean + ): TreeWalker; + createTreeWalker( + root: RootNodeT, + whatToShow: 132, + filter?: NodeFilterInterface, + entityReferenceExpansion?: boolean + ): TreeWalker; + createTreeWalker( + root: RootNodeT, + whatToShow: 133, + filter?: NodeFilterInterface, + entityReferenceExpansion?: boolean + ): TreeWalker; + + // Catch all for when we don't know the value of `whatToShow` + // And for when whatToShow is not provided, it is assumed to be SHOW_ALL + createNodeIterator( + root: RootNodeT, + whatToShow?: number, + filter?: NodeFilterInterface + ): NodeIterator; + createTreeWalker( + root: RootNodeT, + whatToShow?: number, + filter?: NodeFilterInterface, + entityReferenceExpansion?: boolean + ): TreeWalker; + + // From NonElementParentNode Mixin. + getElementById(elementId: string): HTMLElement | null; + + // From DocumentOrShadowRoot Mixin. + +styleSheets: StyleSheetList; + adoptedStyleSheets: Array; +} + +declare class DocumentFragment extends Node { + // from ParentNode interface + childElementCount: number; + children: HTMLCollection; + firstElementChild: ?Element; + lastElementChild: ?Element; + append(...nodes: Array): void; + prepend(...nodes: Array): void; + + querySelector(selector: string): HTMLElement | null; + querySelectorAll(selector: string): NodeList; + + // From NonElementParentNode Mixin. + getElementById(elementId: string): HTMLElement | null; +} + +declare class Selection { + anchorNode: Node | null; + anchorOffset: number; + focusNode: Node | null; + focusOffset: number; + isCollapsed: boolean; + rangeCount: number; + type: string; + addRange(range: Range): void; + getRangeAt(index: number): Range; + removeRange(range: Range): void; + removeAllRanges(): void; + collapse(parentNode: Node | null, offset?: number): void; + collapseToStart(): void; + collapseToEnd(): void; + containsNode(aNode: Node, aPartlyContained?: boolean): boolean; + deleteFromDocument(): void; + extend(parentNode: Node, offset?: number): void; + empty(): void; + selectAllChildren(parentNode: Node): void; + setPosition(aNode: Node | null, offset?: number): void; + setBaseAndExtent( + anchorNode: Node, + anchorOffset: number, + focusNode: Node, + focusOffset: number + ): void; + toString(): string; +} + +declare class Range { + // extension + startOffset: number; + collapsed: boolean; + endOffset: number; + startContainer: Node; + endContainer: Node; + commonAncestorContainer: Node; + setStart(refNode: Node, offset: number): void; + setEndBefore(refNode: Node): void; + setStartBefore(refNode: Node): void; + selectNode(refNode: Node): void; + detach(): void; + getBoundingClientRect(): DOMRect; + toString(): string; + compareBoundaryPoints(how: number, sourceRange: Range): number; + insertNode(newNode: Node): void; + collapse(toStart: boolean): void; + selectNodeContents(refNode: Node): void; + cloneContents(): DocumentFragment; + setEnd(refNode: Node, offset: number): void; + cloneRange(): Range; + getClientRects(): DOMRectList; + surroundContents(newParent: Node): void; + deleteContents(): void; + setStartAfter(refNode: Node): void; + extractContents(): DocumentFragment; + setEndAfter(refNode: Node): void; + createContextualFragment(fragment: string | TrustedHTML): DocumentFragment; + intersectsNode(refNode: Node): boolean; + isPointInRange(refNode: Node, offset: number): boolean; + static END_TO_END: number; + static START_TO_START: number; + static START_TO_END: number; + static END_TO_START: number; +} + +declare var document: Document; + +declare class DOMTokenList { + @@iterator(): Iterator; + length: number; + item(index: number): string; + contains(token: string): boolean; + add(...token: Array): void; + remove(...token: Array): void; + toggle(token: string, force?: boolean): boolean; + replace(oldToken: string, newToken: string): boolean; + + forEach( + callbackfn: (value: string, index: number, list: DOMTokenList) => any, + thisArg?: any + ): void; + entries(): Iterator<[number, string]>; + keys(): Iterator; + values(): Iterator; + [index: number]: string; +} + +declare class Element extends Node mixins mixin$Animatable { + assignedSlot: ?HTMLSlotElement; + attachShadow(shadowRootInitDict: ShadowRootInit): ShadowRoot; + attributes: NamedNodeMap; + classList: DOMTokenList; + className: string; + clientHeight: number; + clientLeft: number; + clientTop: number; + clientWidth: number; + id: string; + // flowlint unsafe-getters-setters:off + get innerHTML(): string; + set innerHTML(value: string | TrustedHTML): void; + // flowlint unsafe-getters-setters:error + localName: string; + namespaceURI: ?string; + nextElementSibling: ?Element; + // flowlint unsafe-getters-setters:off + get outerHTML(): string; + set outerHTML(value: string | TrustedHTML): void; + // flowlint unsafe-getters-setters:error + prefix: string | null; + previousElementSibling: ?Element; + scrollHeight: number; + scrollLeft: number; + scrollTop: number; + scrollWidth: number; + +tagName: string; + + // TODO: a lot more ARIA properties + ariaHidden: void | 'true' | 'false'; + + closest(selectors: string): ?Element; + + getAttribute(name?: string): ?string; + getAttributeNames(): Array; + getAttributeNS(namespaceURI: string | null, localName: string): string | null; + getAttributeNode(name: string): Attr | null; + getAttributeNodeNS( + namespaceURI: string | null, + localName: string + ): Attr | null; + getBoundingClientRect(): DOMRect; + getClientRects(): DOMRectList; + getElementsByClassName(names: string): HTMLCollection; + getElementsByTagName>( + qualifiedName: TName + ): HTMLCollection; + getElementsByTagNameNS>( + namespaceURI: 'http://www.w3.org/1999/xhtml', + qualifiedName: TName + ): HTMLCollection; + getElementsByTagNameNS( + namespaceURI: string | null, + qualifiedName: string + ): HTMLCollection; + + hasAttribute(name: string): boolean; + hasAttributeNS(namespaceURI: string | null, localName: string): boolean; + hasAttributes(): boolean; + hasPointerCapture(pointerId: number): boolean; + insertAdjacentElement( + position: 'beforebegin' | 'afterbegin' | 'beforeend' | 'afterend', + element: Element + ): void; + insertAdjacentHTML( + position: 'beforebegin' | 'afterbegin' | 'beforeend' | 'afterend', + html: string | TrustedHTML + ): void; + insertAdjacentText( + position: 'beforebegin' | 'afterbegin' | 'beforeend' | 'afterend', + text: string + ): void; + matches(selector: string): boolean; + releasePointerCapture(pointerId: number): void; + removeAttribute(name?: string): void; + removeAttributeNode(attributeNode: Attr): Attr; + removeAttributeNS(namespaceURI: string | null, localName: string): void; + requestFullscreen(options?: { + navigationUI: 'auto' | 'show' | 'hide', + ... + }): Promise; + requestPointerLock(): void; + scrollIntoView( + arg?: + | boolean + | { + behavior?: 'auto' | 'instant' | 'smooth', + block?: 'start' | 'center' | 'end' | 'nearest', + inline?: 'start' | 'center' | 'end' | 'nearest', + ... + } + ): void; + scroll(x: number, y: number): void; + scroll(options: ScrollToOptions): void; + scrollTo(x: number, y: number): void; + scrollTo(options: ScrollToOptions): void; + scrollBy(x: number, y: number): void; + scrollBy(options: ScrollToOptions): void; + setAttribute(name?: string, value?: string): void; + toggleAttribute(name?: string, force?: boolean): void; + setAttributeNS( + namespaceURI: string | null, + qualifiedName: string, + value: string + ): void; + setAttributeNode(newAttr: Attr): Attr | null; + setAttributeNodeNS(newAttr: Attr): Attr | null; + setPointerCapture(pointerId: number): void; + shadowRoot?: ShadowRoot; + slot?: string; + + // from ParentNode interface + childElementCount: number; + children: HTMLCollection; + firstElementChild: ?Element; + lastElementChild: ?Element; + append(...nodes: Array): void; + prepend(...nodes: Array): void; + + querySelector>( + selector: TSelector + ): HTMLElementTagNameMap[TSelector] | null; + querySelectorAll>( + selector: TSelector + ): NodeList; + + // from ChildNode interface + after(...nodes: Array): void; + before(...nodes: Array): void; + replaceWith(...nodes: Array): void; + remove(): void; +} + +declare class HitRegionOptions { + path?: Path2D; + fillRule?: CanvasFillRule; + id?: string; + parentID?: string; + cursor?: string; + control?: Element; + label: ?string; + role: ?string; +} + +declare class SVGMatrix { + getComponent(index: number): number; + mMultiply(secondMatrix: SVGMatrix): SVGMatrix; + inverse(): SVGMatrix; + mTranslate(x: number, y: number): SVGMatrix; + mScale(scaleFactor: number): SVGMatrix; + mRotate(angle: number): SVGMatrix; +} + +// WebGL idl: https://www.khronos.org/registry/webgl/specs/latest/1.0/webgl.idl + +type WebGLContextAttributes = { + alpha: boolean, + depth: boolean, + stencil: boolean, + antialias: boolean, + premultipliedAlpha: boolean, + preserveDrawingBuffer: boolean, + preferLowPowerToHighPerformance: boolean, + failIfMajorPerformanceCaveat: boolean, + ... +}; + +interface WebGLObject {} + +interface WebGLBuffer extends WebGLObject {} + +interface WebGLFramebuffer extends WebGLObject {} + +interface WebGLProgram extends WebGLObject {} + +interface WebGLRenderbuffer extends WebGLObject {} + +interface WebGLShader extends WebGLObject {} + +interface WebGLTexture extends WebGLObject {} + +interface WebGLUniformLocation {} + +interface WebGLActiveInfo { + size: number; + type: number; + name: string; +} + +interface WebGLShaderPrecisionFormat { + rangeMin: number; + rangeMax: number; + precision: number; +} + +type BufferDataSource = ArrayBuffer | $ArrayBufferView; + +type TexImageSource = + | ImageBitmap + | ImageData + | HTMLImageElement + | HTMLCanvasElement + | HTMLVideoElement; + +type VertexAttribFVSource = Float32Array | Array; + +/* flow */ +declare class WebGLRenderingContext { + static DEPTH_BUFFER_BIT: 0x00000100; + DEPTH_BUFFER_BIT: 0x00000100; + static STENCIL_BUFFER_BIT: 0x00000400; + STENCIL_BUFFER_BIT: 0x00000400; + static COLOR_BUFFER_BIT: 0x00004000; + COLOR_BUFFER_BIT: 0x00004000; + static POINTS: 0x0000; + POINTS: 0x0000; + static LINES: 0x0001; + LINES: 0x0001; + static LINE_LOOP: 0x0002; + LINE_LOOP: 0x0002; + static LINE_STRIP: 0x0003; + LINE_STRIP: 0x0003; + static TRIANGLES: 0x0004; + TRIANGLES: 0x0004; + static TRIANGLE_STRIP: 0x0005; + TRIANGLE_STRIP: 0x0005; + static TRIANGLE_FAN: 0x0006; + TRIANGLE_FAN: 0x0006; + static ZERO: 0; + ZERO: 0; + static ONE: 1; + ONE: 1; + static SRC_COLOR: 0x0300; + SRC_COLOR: 0x0300; + static ONE_MINUS_SRC_COLOR: 0x0301; + ONE_MINUS_SRC_COLOR: 0x0301; + static SRC_ALPHA: 0x0302; + SRC_ALPHA: 0x0302; + static ONE_MINUS_SRC_ALPHA: 0x0303; + ONE_MINUS_SRC_ALPHA: 0x0303; + static DST_ALPHA: 0x0304; + DST_ALPHA: 0x0304; + static ONE_MINUS_DST_ALPHA: 0x0305; + ONE_MINUS_DST_ALPHA: 0x0305; + static DST_COLOR: 0x0306; + DST_COLOR: 0x0306; + static ONE_MINUS_DST_COLOR: 0x0307; + ONE_MINUS_DST_COLOR: 0x0307; + static SRC_ALPHA_SATURATE: 0x0308; + SRC_ALPHA_SATURATE: 0x0308; + static FUNC_ADD: 0x8006; + FUNC_ADD: 0x8006; + static BLEND_EQUATION: 0x8009; + BLEND_EQUATION: 0x8009; + static BLEND_EQUATION_RGB: 0x8009; + BLEND_EQUATION_RGB: 0x8009; + static BLEND_EQUATION_ALPHA: 0x883d; + BLEND_EQUATION_ALPHA: 0x883d; + static FUNC_SUBTRACT: 0x800a; + FUNC_SUBTRACT: 0x800a; + static FUNC_REVERSE_SUBTRACT: 0x800b; + FUNC_REVERSE_SUBTRACT: 0x800b; + static BLEND_DST_RGB: 0x80c8; + BLEND_DST_RGB: 0x80c8; + static BLEND_SRC_RGB: 0x80c9; + BLEND_SRC_RGB: 0x80c9; + static BLEND_DST_ALPHA: 0x80ca; + BLEND_DST_ALPHA: 0x80ca; + static BLEND_SRC_ALPHA: 0x80cb; + BLEND_SRC_ALPHA: 0x80cb; + static CONSTANT_COLOR: 0x8001; + CONSTANT_COLOR: 0x8001; + static ONE_MINUS_CONSTANT_COLOR: 0x8002; + ONE_MINUS_CONSTANT_COLOR: 0x8002; + static CONSTANT_ALPHA: 0x8003; + CONSTANT_ALPHA: 0x8003; + static ONE_MINUS_CONSTANT_ALPHA: 0x8004; + ONE_MINUS_CONSTANT_ALPHA: 0x8004; + static BLEND_COLOR: 0x8005; + BLEND_COLOR: 0x8005; + static ARRAY_BUFFER: 0x8892; + ARRAY_BUFFER: 0x8892; + static ELEMENT_ARRAY_BUFFER: 0x8893; + ELEMENT_ARRAY_BUFFER: 0x8893; + static ARRAY_BUFFER_BINDING: 0x8894; + ARRAY_BUFFER_BINDING: 0x8894; + static ELEMENT_ARRAY_BUFFER_BINDING: 0x8895; + ELEMENT_ARRAY_BUFFER_BINDING: 0x8895; + static STREAM_DRAW: 0x88e0; + STREAM_DRAW: 0x88e0; + static STATIC_DRAW: 0x88e4; + STATIC_DRAW: 0x88e4; + static DYNAMIC_DRAW: 0x88e8; + DYNAMIC_DRAW: 0x88e8; + static BUFFER_SIZE: 0x8764; + BUFFER_SIZE: 0x8764; + static BUFFER_USAGE: 0x8765; + BUFFER_USAGE: 0x8765; + static CURRENT_VERTEX_ATTRIB: 0x8626; + CURRENT_VERTEX_ATTRIB: 0x8626; + static FRONT: 0x0404; + FRONT: 0x0404; + static BACK: 0x0405; + BACK: 0x0405; + static FRONT_AND_BACK: 0x0408; + FRONT_AND_BACK: 0x0408; + static CULL_FACE: 0x0b44; + CULL_FACE: 0x0b44; + static BLEND: 0x0be2; + BLEND: 0x0be2; + static DITHER: 0x0bd0; + DITHER: 0x0bd0; + static STENCIL_TEST: 0x0b90; + STENCIL_TEST: 0x0b90; + static DEPTH_TEST: 0x0b71; + DEPTH_TEST: 0x0b71; + static SCISSOR_TEST: 0x0c11; + SCISSOR_TEST: 0x0c11; + static POLYGON_OFFSET_FILL: 0x8037; + POLYGON_OFFSET_FILL: 0x8037; + static SAMPLE_ALPHA_TO_COVERAGE: 0x809e; + SAMPLE_ALPHA_TO_COVERAGE: 0x809e; + static SAMPLE_COVERAGE: 0x80a0; + SAMPLE_COVERAGE: 0x80a0; + static NO_ERROR: 0; + NO_ERROR: 0; + static INVALID_ENUM: 0x0500; + INVALID_ENUM: 0x0500; + static INVALID_VALUE: 0x0501; + INVALID_VALUE: 0x0501; + static INVALID_OPERATION: 0x0502; + INVALID_OPERATION: 0x0502; + static OUT_OF_MEMORY: 0x0505; + OUT_OF_MEMORY: 0x0505; + static CW: 0x0900; + CW: 0x0900; + static CCW: 0x0901; + CCW: 0x0901; + static LINE_WIDTH: 0x0b21; + LINE_WIDTH: 0x0b21; + static ALIASED_POINT_SIZE_RANGE: 0x846d; + ALIASED_POINT_SIZE_RANGE: 0x846d; + static ALIASED_LINE_WIDTH_RANGE: 0x846e; + ALIASED_LINE_WIDTH_RANGE: 0x846e; + static CULL_FACE_MODE: 0x0b45; + CULL_FACE_MODE: 0x0b45; + static FRONT_FACE: 0x0b46; + FRONT_FACE: 0x0b46; + static DEPTH_RANGE: 0x0b70; + DEPTH_RANGE: 0x0b70; + static DEPTH_WRITEMASK: 0x0b72; + DEPTH_WRITEMASK: 0x0b72; + static DEPTH_CLEAR_VALUE: 0x0b73; + DEPTH_CLEAR_VALUE: 0x0b73; + static DEPTH_FUNC: 0x0b74; + DEPTH_FUNC: 0x0b74; + static STENCIL_CLEAR_VALUE: 0x0b91; + STENCIL_CLEAR_VALUE: 0x0b91; + static STENCIL_FUNC: 0x0b92; + STENCIL_FUNC: 0x0b92; + static STENCIL_FAIL: 0x0b94; + STENCIL_FAIL: 0x0b94; + static STENCIL_PASS_DEPTH_FAIL: 0x0b95; + STENCIL_PASS_DEPTH_FAIL: 0x0b95; + static STENCIL_PASS_DEPTH_PASS: 0x0b96; + STENCIL_PASS_DEPTH_PASS: 0x0b96; + static STENCIL_REF: 0x0b97; + STENCIL_REF: 0x0b97; + static STENCIL_VALUE_MASK: 0x0b93; + STENCIL_VALUE_MASK: 0x0b93; + static STENCIL_WRITEMASK: 0x0b98; + STENCIL_WRITEMASK: 0x0b98; + static STENCIL_BACK_FUNC: 0x8800; + STENCIL_BACK_FUNC: 0x8800; + static STENCIL_BACK_FAIL: 0x8801; + STENCIL_BACK_FAIL: 0x8801; + static STENCIL_BACK_PASS_DEPTH_FAIL: 0x8802; + STENCIL_BACK_PASS_DEPTH_FAIL: 0x8802; + static STENCIL_BACK_PASS_DEPTH_PASS: 0x8803; + STENCIL_BACK_PASS_DEPTH_PASS: 0x8803; + static STENCIL_BACK_REF: 0x8ca3; + STENCIL_BACK_REF: 0x8ca3; + static STENCIL_BACK_VALUE_MASK: 0x8ca4; + STENCIL_BACK_VALUE_MASK: 0x8ca4; + static STENCIL_BACK_WRITEMASK: 0x8ca5; + STENCIL_BACK_WRITEMASK: 0x8ca5; + static VIEWPORT: 0x0ba2; + VIEWPORT: 0x0ba2; + static SCISSOR_BOX: 0x0c10; + SCISSOR_BOX: 0x0c10; + static COLOR_CLEAR_VALUE: 0x0c22; + COLOR_CLEAR_VALUE: 0x0c22; + static COLOR_WRITEMASK: 0x0c23; + COLOR_WRITEMASK: 0x0c23; + static UNPACK_ALIGNMENT: 0x0cf5; + UNPACK_ALIGNMENT: 0x0cf5; + static PACK_ALIGNMENT: 0x0d05; + PACK_ALIGNMENT: 0x0d05; + static MAX_TEXTURE_SIZE: 0x0d33; + MAX_TEXTURE_SIZE: 0x0d33; + static MAX_VIEWPORT_DIMS: 0x0d3a; + MAX_VIEWPORT_DIMS: 0x0d3a; + static SUBPIXEL_BITS: 0x0d50; + SUBPIXEL_BITS: 0x0d50; + static RED_BITS: 0x0d52; + RED_BITS: 0x0d52; + static GREEN_BITS: 0x0d53; + GREEN_BITS: 0x0d53; + static BLUE_BITS: 0x0d54; + BLUE_BITS: 0x0d54; + static ALPHA_BITS: 0x0d55; + ALPHA_BITS: 0x0d55; + static DEPTH_BITS: 0x0d56; + DEPTH_BITS: 0x0d56; + static STENCIL_BITS: 0x0d57; + STENCIL_BITS: 0x0d57; + static POLYGON_OFFSET_UNITS: 0x2a00; + POLYGON_OFFSET_UNITS: 0x2a00; + static POLYGON_OFFSET_FACTOR: 0x8038; + POLYGON_OFFSET_FACTOR: 0x8038; + static TEXTURE_BINDING_2D: 0x8069; + TEXTURE_BINDING_2D: 0x8069; + static SAMPLE_BUFFERS: 0x80a8; + SAMPLE_BUFFERS: 0x80a8; + static SAMPLES: 0x80a9; + SAMPLES: 0x80a9; + static SAMPLE_COVERAGE_VALUE: 0x80aa; + SAMPLE_COVERAGE_VALUE: 0x80aa; + static SAMPLE_COVERAGE_INVERT: 0x80ab; + SAMPLE_COVERAGE_INVERT: 0x80ab; + static COMPRESSED_TEXTURE_FORMATS: 0x86a3; + COMPRESSED_TEXTURE_FORMATS: 0x86a3; + static DONT_CARE: 0x1100; + DONT_CARE: 0x1100; + static FASTEST: 0x1101; + FASTEST: 0x1101; + static NICEST: 0x1102; + NICEST: 0x1102; + static GENERATE_MIPMAP_HINT: 0x8192; + GENERATE_MIPMAP_HINT: 0x8192; + static BYTE: 0x1400; + BYTE: 0x1400; + static UNSIGNED_BYTE: 0x1401; + UNSIGNED_BYTE: 0x1401; + static SHORT: 0x1402; + SHORT: 0x1402; + static UNSIGNED_SHORT: 0x1403; + UNSIGNED_SHORT: 0x1403; + static INT: 0x1404; + INT: 0x1404; + static UNSIGNED_INT: 0x1405; + UNSIGNED_INT: 0x1405; + static FLOAT: 0x1406; + FLOAT: 0x1406; + static DEPTH_COMPONENT: 0x1902; + DEPTH_COMPONENT: 0x1902; + static ALPHA: 0x1906; + ALPHA: 0x1906; + static RGB: 0x1907; + RGB: 0x1907; + static RGBA: 0x1908; + RGBA: 0x1908; + static LUMINANCE: 0x1909; + LUMINANCE: 0x1909; + static LUMINANCE_ALPHA: 0x190a; + LUMINANCE_ALPHA: 0x190a; + static UNSIGNED_SHORT_4_4_4_4: 0x8033; + UNSIGNED_SHORT_4_4_4_4: 0x8033; + static UNSIGNED_SHORT_5_5_5_1: 0x8034; + UNSIGNED_SHORT_5_5_5_1: 0x8034; + static UNSIGNED_SHORT_5_6_5: 0x8363; + UNSIGNED_SHORT_5_6_5: 0x8363; + static FRAGMENT_SHADER: 0x8b30; + FRAGMENT_SHADER: 0x8b30; + static VERTEX_SHADER: 0x8b31; + VERTEX_SHADER: 0x8b31; + static MAX_VERTEX_ATTRIBS: 0x8869; + MAX_VERTEX_ATTRIBS: 0x8869; + static MAX_VERTEX_UNIFORM_VECTORS: 0x8dfb; + MAX_VERTEX_UNIFORM_VECTORS: 0x8dfb; + static MAX_VARYING_VECTORS: 0x8dfc; + MAX_VARYING_VECTORS: 0x8dfc; + static MAX_COMBINED_TEXTURE_IMAGE_UNITS: 0x8b4d; + MAX_COMBINED_TEXTURE_IMAGE_UNITS: 0x8b4d; + static MAX_VERTEX_TEXTURE_IMAGE_UNITS: 0x8b4c; + MAX_VERTEX_TEXTURE_IMAGE_UNITS: 0x8b4c; + static MAX_TEXTURE_IMAGE_UNITS: 0x8872; + MAX_TEXTURE_IMAGE_UNITS: 0x8872; + static MAX_FRAGMENT_UNIFORM_VECTORS: 0x8dfd; + MAX_FRAGMENT_UNIFORM_VECTORS: 0x8dfd; + static SHADER_TYPE: 0x8b4f; + SHADER_TYPE: 0x8b4f; + static DELETE_STATUS: 0x8b80; + DELETE_STATUS: 0x8b80; + static LINK_STATUS: 0x8b82; + LINK_STATUS: 0x8b82; + static VALIDATE_STATUS: 0x8b83; + VALIDATE_STATUS: 0x8b83; + static ATTACHED_SHADERS: 0x8b85; + ATTACHED_SHADERS: 0x8b85; + static ACTIVE_UNIFORMS: 0x8b86; + ACTIVE_UNIFORMS: 0x8b86; + static ACTIVE_ATTRIBUTES: 0x8b89; + ACTIVE_ATTRIBUTES: 0x8b89; + static SHADING_LANGUAGE_VERSION: 0x8b8c; + SHADING_LANGUAGE_VERSION: 0x8b8c; + static CURRENT_PROGRAM: 0x8b8d; + CURRENT_PROGRAM: 0x8b8d; + static NEVER: 0x0200; + NEVER: 0x0200; + static LESS: 0x0201; + LESS: 0x0201; + static EQUAL: 0x0202; + EQUAL: 0x0202; + static LEQUAL: 0x0203; + LEQUAL: 0x0203; + static GREATER: 0x0204; + GREATER: 0x0204; + static NOTEQUAL: 0x0205; + NOTEQUAL: 0x0205; + static GEQUAL: 0x0206; + GEQUAL: 0x0206; + static ALWAYS: 0x0207; + ALWAYS: 0x0207; + static KEEP: 0x1e00; + KEEP: 0x1e00; + static REPLACE: 0x1e01; + REPLACE: 0x1e01; + static INCR: 0x1e02; + INCR: 0x1e02; + static DECR: 0x1e03; + DECR: 0x1e03; + static INVERT: 0x150a; + INVERT: 0x150a; + static INCR_WRAP: 0x8507; + INCR_WRAP: 0x8507; + static DECR_WRAP: 0x8508; + DECR_WRAP: 0x8508; + static VENDOR: 0x1f00; + VENDOR: 0x1f00; + static RENDERER: 0x1f01; + RENDERER: 0x1f01; + static VERSION: 0x1f02; + VERSION: 0x1f02; + static NEAREST: 0x2600; + NEAREST: 0x2600; + static LINEAR: 0x2601; + LINEAR: 0x2601; + static NEAREST_MIPMAP_NEAREST: 0x2700; + NEAREST_MIPMAP_NEAREST: 0x2700; + static LINEAR_MIPMAP_NEAREST: 0x2701; + LINEAR_MIPMAP_NEAREST: 0x2701; + static NEAREST_MIPMAP_LINEAR: 0x2702; + NEAREST_MIPMAP_LINEAR: 0x2702; + static LINEAR_MIPMAP_LINEAR: 0x2703; + LINEAR_MIPMAP_LINEAR: 0x2703; + static TEXTURE_MAG_FILTER: 0x2800; + TEXTURE_MAG_FILTER: 0x2800; + static TEXTURE_MIN_FILTER: 0x2801; + TEXTURE_MIN_FILTER: 0x2801; + static TEXTURE_WRAP_S: 0x2802; + TEXTURE_WRAP_S: 0x2802; + static TEXTURE_WRAP_T: 0x2803; + TEXTURE_WRAP_T: 0x2803; + static TEXTURE_2D: 0x0de1; + TEXTURE_2D: 0x0de1; + static TEXTURE: 0x1702; + TEXTURE: 0x1702; + static TEXTURE_CUBE_MAP: 0x8513; + TEXTURE_CUBE_MAP: 0x8513; + static TEXTURE_BINDING_CUBE_MAP: 0x8514; + TEXTURE_BINDING_CUBE_MAP: 0x8514; + static TEXTURE_CUBE_MAP_POSITIVE_X: 0x8515; + TEXTURE_CUBE_MAP_POSITIVE_X: 0x8515; + static TEXTURE_CUBE_MAP_NEGATIVE_X: 0x8516; + TEXTURE_CUBE_MAP_NEGATIVE_X: 0x8516; + static TEXTURE_CUBE_MAP_POSITIVE_Y: 0x8517; + TEXTURE_CUBE_MAP_POSITIVE_Y: 0x8517; + static TEXTURE_CUBE_MAP_NEGATIVE_Y: 0x8518; + TEXTURE_CUBE_MAP_NEGATIVE_Y: 0x8518; + static TEXTURE_CUBE_MAP_POSITIVE_Z: 0x8519; + TEXTURE_CUBE_MAP_POSITIVE_Z: 0x8519; + static TEXTURE_CUBE_MAP_NEGATIVE_Z: 0x851a; + TEXTURE_CUBE_MAP_NEGATIVE_Z: 0x851a; + static MAX_CUBE_MAP_TEXTURE_SIZE: 0x851c; + MAX_CUBE_MAP_TEXTURE_SIZE: 0x851c; + static TEXTURE0: 0x84c0; + TEXTURE0: 0x84c0; + static TEXTURE1: 0x84c1; + TEXTURE1: 0x84c1; + static TEXTURE2: 0x84c2; + TEXTURE2: 0x84c2; + static TEXTURE3: 0x84c3; + TEXTURE3: 0x84c3; + static TEXTURE4: 0x84c4; + TEXTURE4: 0x84c4; + static TEXTURE5: 0x84c5; + TEXTURE5: 0x84c5; + static TEXTURE6: 0x84c6; + TEXTURE6: 0x84c6; + static TEXTURE7: 0x84c7; + TEXTURE7: 0x84c7; + static TEXTURE8: 0x84c8; + TEXTURE8: 0x84c8; + static TEXTURE9: 0x84c9; + TEXTURE9: 0x84c9; + static TEXTURE10: 0x84ca; + TEXTURE10: 0x84ca; + static TEXTURE11: 0x84cb; + TEXTURE11: 0x84cb; + static TEXTURE12: 0x84cc; + TEXTURE12: 0x84cc; + static TEXTURE13: 0x84cd; + TEXTURE13: 0x84cd; + static TEXTURE14: 0x84ce; + TEXTURE14: 0x84ce; + static TEXTURE15: 0x84cf; + TEXTURE15: 0x84cf; + static TEXTURE16: 0x84d0; + TEXTURE16: 0x84d0; + static TEXTURE17: 0x84d1; + TEXTURE17: 0x84d1; + static TEXTURE18: 0x84d2; + TEXTURE18: 0x84d2; + static TEXTURE19: 0x84d3; + TEXTURE19: 0x84d3; + static TEXTURE20: 0x84d4; + TEXTURE20: 0x84d4; + static TEXTURE21: 0x84d5; + TEXTURE21: 0x84d5; + static TEXTURE22: 0x84d6; + TEXTURE22: 0x84d6; + static TEXTURE23: 0x84d7; + TEXTURE23: 0x84d7; + static TEXTURE24: 0x84d8; + TEXTURE24: 0x84d8; + static TEXTURE25: 0x84d9; + TEXTURE25: 0x84d9; + static TEXTURE26: 0x84da; + TEXTURE26: 0x84da; + static TEXTURE27: 0x84db; + TEXTURE27: 0x84db; + static TEXTURE28: 0x84dc; + TEXTURE28: 0x84dc; + static TEXTURE29: 0x84dd; + TEXTURE29: 0x84dd; + static TEXTURE30: 0x84de; + TEXTURE30: 0x84de; + static TEXTURE31: 0x84df; + TEXTURE31: 0x84df; + static ACTIVE_TEXTURE: 0x84e0; + ACTIVE_TEXTURE: 0x84e0; + static REPEAT: 0x2901; + REPEAT: 0x2901; + static CLAMP_TO_EDGE: 0x812f; + CLAMP_TO_EDGE: 0x812f; + static MIRRORED_REPEAT: 0x8370; + MIRRORED_REPEAT: 0x8370; + static FLOAT_VEC2: 0x8b50; + FLOAT_VEC2: 0x8b50; + static FLOAT_VEC3: 0x8b51; + FLOAT_VEC3: 0x8b51; + static FLOAT_VEC4: 0x8b52; + FLOAT_VEC4: 0x8b52; + static INT_VEC2: 0x8b53; + INT_VEC2: 0x8b53; + static INT_VEC3: 0x8b54; + INT_VEC3: 0x8b54; + static INT_VEC4: 0x8b55; + INT_VEC4: 0x8b55; + static BOOL: 0x8b56; + BOOL: 0x8b56; + static BOOL_VEC2: 0x8b57; + BOOL_VEC2: 0x8b57; + static BOOL_VEC3: 0x8b58; + BOOL_VEC3: 0x8b58; + static BOOL_VEC4: 0x8b59; + BOOL_VEC4: 0x8b59; + static FLOAT_MAT2: 0x8b5a; + FLOAT_MAT2: 0x8b5a; + static FLOAT_MAT3: 0x8b5b; + FLOAT_MAT3: 0x8b5b; + static FLOAT_MAT4: 0x8b5c; + FLOAT_MAT4: 0x8b5c; + static SAMPLER_2D: 0x8b5e; + SAMPLER_2D: 0x8b5e; + static SAMPLER_CUBE: 0x8b60; + SAMPLER_CUBE: 0x8b60; + static VERTEX_ATTRIB_ARRAY_ENABLED: 0x8622; + VERTEX_ATTRIB_ARRAY_ENABLED: 0x8622; + static VERTEX_ATTRIB_ARRAY_SIZE: 0x8623; + VERTEX_ATTRIB_ARRAY_SIZE: 0x8623; + static VERTEX_ATTRIB_ARRAY_STRIDE: 0x8624; + VERTEX_ATTRIB_ARRAY_STRIDE: 0x8624; + static VERTEX_ATTRIB_ARRAY_TYPE: 0x8625; + VERTEX_ATTRIB_ARRAY_TYPE: 0x8625; + static VERTEX_ATTRIB_ARRAY_NORMALIZED: 0x886a; + VERTEX_ATTRIB_ARRAY_NORMALIZED: 0x886a; + static VERTEX_ATTRIB_ARRAY_POINTER: 0x8645; + VERTEX_ATTRIB_ARRAY_POINTER: 0x8645; + static VERTEX_ATTRIB_ARRAY_BUFFER_BINDING: 0x889f; + VERTEX_ATTRIB_ARRAY_BUFFER_BINDING: 0x889f; + static IMPLEMENTATION_COLOR_READ_TYPE: 0x8b9a; + IMPLEMENTATION_COLOR_READ_TYPE: 0x8b9a; + static IMPLEMENTATION_COLOR_READ_FORMAT: 0x8b9b; + IMPLEMENTATION_COLOR_READ_FORMAT: 0x8b9b; + static COMPILE_STATUS: 0x8b81; + COMPILE_STATUS: 0x8b81; + static LOW_FLOAT: 0x8df0; + LOW_FLOAT: 0x8df0; + static MEDIUM_FLOAT: 0x8df1; + MEDIUM_FLOAT: 0x8df1; + static HIGH_FLOAT: 0x8df2; + HIGH_FLOAT: 0x8df2; + static LOW_INT: 0x8df3; + LOW_INT: 0x8df3; + static MEDIUM_INT: 0x8df4; + MEDIUM_INT: 0x8df4; + static HIGH_INT: 0x8df5; + HIGH_INT: 0x8df5; + static FRAMEBUFFER: 0x8d40; + FRAMEBUFFER: 0x8d40; + static RENDERBUFFER: 0x8d41; + RENDERBUFFER: 0x8d41; + static RGBA4: 0x8056; + RGBA4: 0x8056; + static RGB5_A1: 0x8057; + RGB5_A1: 0x8057; + static RGB565: 0x8d62; + RGB565: 0x8d62; + static DEPTH_COMPONENT16: 0x81a5; + DEPTH_COMPONENT16: 0x81a5; + static STENCIL_INDEX: 0x1901; + STENCIL_INDEX: 0x1901; + static STENCIL_INDEX8: 0x8d48; + STENCIL_INDEX8: 0x8d48; + static DEPTH_STENCIL: 0x84f9; + DEPTH_STENCIL: 0x84f9; + static RENDERBUFFER_WIDTH: 0x8d42; + RENDERBUFFER_WIDTH: 0x8d42; + static RENDERBUFFER_HEIGHT: 0x8d43; + RENDERBUFFER_HEIGHT: 0x8d43; + static RENDERBUFFER_INTERNAL_FORMAT: 0x8d44; + RENDERBUFFER_INTERNAL_FORMAT: 0x8d44; + static RENDERBUFFER_RED_SIZE: 0x8d50; + RENDERBUFFER_RED_SIZE: 0x8d50; + static RENDERBUFFER_GREEN_SIZE: 0x8d51; + RENDERBUFFER_GREEN_SIZE: 0x8d51; + static RENDERBUFFER_BLUE_SIZE: 0x8d52; + RENDERBUFFER_BLUE_SIZE: 0x8d52; + static RENDERBUFFER_ALPHA_SIZE: 0x8d53; + RENDERBUFFER_ALPHA_SIZE: 0x8d53; + static RENDERBUFFER_DEPTH_SIZE: 0x8d54; + RENDERBUFFER_DEPTH_SIZE: 0x8d54; + static RENDERBUFFER_STENCIL_SIZE: 0x8d55; + RENDERBUFFER_STENCIL_SIZE: 0x8d55; + static FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE: 0x8cd0; + FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE: 0x8cd0; + static FRAMEBUFFER_ATTACHMENT_OBJECT_NAME: 0x8cd1; + FRAMEBUFFER_ATTACHMENT_OBJECT_NAME: 0x8cd1; + static FRAMEBUFFER_ATTACHMENT_TEXTURE_LEVEL: 0x8cd2; + FRAMEBUFFER_ATTACHMENT_TEXTURE_LEVEL: 0x8cd2; + static FRAMEBUFFER_ATTACHMENT_TEXTURE_CUBE_MAP_FACE: 0x8cd3; + FRAMEBUFFER_ATTACHMENT_TEXTURE_CUBE_MAP_FACE: 0x8cd3; + static COLOR_ATTACHMENT0: 0x8ce0; + COLOR_ATTACHMENT0: 0x8ce0; + static DEPTH_ATTACHMENT: 0x8d00; + DEPTH_ATTACHMENT: 0x8d00; + static STENCIL_ATTACHMENT: 0x8d20; + STENCIL_ATTACHMENT: 0x8d20; + static DEPTH_STENCIL_ATTACHMENT: 0x821a; + DEPTH_STENCIL_ATTACHMENT: 0x821a; + static NONE: 0; + NONE: 0; + static FRAMEBUFFER_COMPLETE: 0x8cd5; + FRAMEBUFFER_COMPLETE: 0x8cd5; + static FRAMEBUFFER_INCOMPLETE_ATTACHMENT: 0x8cd6; + FRAMEBUFFER_INCOMPLETE_ATTACHMENT: 0x8cd6; + static FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT: 0x8cd7; + FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT: 0x8cd7; + static FRAMEBUFFER_INCOMPLETE_DIMENSIONS: 0x8cd9; + FRAMEBUFFER_INCOMPLETE_DIMENSIONS: 0x8cd9; + static FRAMEBUFFER_UNSUPPORTED: 0x8cdd; + FRAMEBUFFER_UNSUPPORTED: 0x8cdd; + static FRAMEBUFFER_BINDING: 0x8ca6; + FRAMEBUFFER_BINDING: 0x8ca6; + static RENDERBUFFER_BINDING: 0x8ca7; + RENDERBUFFER_BINDING: 0x8ca7; + static MAX_RENDERBUFFER_SIZE: 0x84e8; + MAX_RENDERBUFFER_SIZE: 0x84e8; + static INVALID_FRAMEBUFFER_OPERATION: 0x0506; + INVALID_FRAMEBUFFER_OPERATION: 0x0506; + static UNPACK_FLIP_Y_WEBGL: 0x9240; + UNPACK_FLIP_Y_WEBGL: 0x9240; + static UNPACK_PREMULTIPLY_ALPHA_WEBGL: 0x9241; + UNPACK_PREMULTIPLY_ALPHA_WEBGL: 0x9241; + static CONTEXT_LOST_WEBGL: 0x9242; + CONTEXT_LOST_WEBGL: 0x9242; + static UNPACK_COLORSPACE_CONVERSION_WEBGL: 0x9243; + UNPACK_COLORSPACE_CONVERSION_WEBGL: 0x9243; + static BROWSER_DEFAULT_WEBGL: 0x9244; + BROWSER_DEFAULT_WEBGL: 0x9244; + + canvas: HTMLCanvasElement; + drawingBufferWidth: number; + drawingBufferHeight: number; + + getContextAttributes(): ?WebGLContextAttributes; + isContextLost(): boolean; + + getSupportedExtensions(): ?Array; + getExtension(name: string): any; + + activeTexture(texture: number): void; + attachShader(program: WebGLProgram, shader: WebGLShader): void; + bindAttribLocation(program: WebGLProgram, index: number, name: string): void; + bindBuffer(target: number, buffer: ?WebGLBuffer): void; + bindFramebuffer(target: number, framebuffer: ?WebGLFramebuffer): void; + bindRenderbuffer(target: number, renderbuffer: ?WebGLRenderbuffer): void; + bindTexture(target: number, texture: ?WebGLTexture): void; + blendColor(red: number, green: number, blue: number, alpha: number): void; + blendEquation(mode: number): void; + blendEquationSeparate(modeRGB: number, modeAlpha: number): void; + blendFunc(sfactor: number, dfactor: number): void; + blendFuncSeparate( + srcRGB: number, + dstRGB: number, + srcAlpha: number, + dstAlpha: number + ): void; + + bufferData(target: number, size: number, usage: number): void; + bufferData(target: number, data: ?ArrayBuffer, usage: number): void; + bufferData(target: number, data: $ArrayBufferView, usage: number): void; + bufferSubData(target: number, offset: number, data: BufferDataSource): void; + + checkFramebufferStatus(target: number): number; + clear(mask: number): void; + clearColor(red: number, green: number, blue: number, alpha: number): void; + clearDepth(depth: number): void; + clearStencil(s: number): void; + colorMask(red: boolean, green: boolean, blue: boolean, alpha: boolean): void; + compileShader(shader: WebGLShader): void; + + compressedTexImage2D( + target: number, + level: number, + internalformat: number, + width: number, + height: number, + border: number, + data: $ArrayBufferView + ): void; + + compressedTexSubImage2D( + target: number, + level: number, + xoffset: number, + yoffset: number, + width: number, + height: number, + format: number, + data: $ArrayBufferView + ): void; + + copyTexImage2D( + target: number, + level: number, + internalformat: number, + x: number, + y: number, + width: number, + height: number, + border: number + ): void; + copyTexSubImage2D( + target: number, + level: number, + xoffset: number, + yoffset: number, + x: number, + y: number, + width: number, + height: number + ): void; + + createBuffer(): ?WebGLBuffer; + createFramebuffer(): ?WebGLFramebuffer; + createProgram(): ?WebGLProgram; + createRenderbuffer(): ?WebGLRenderbuffer; + createShader(type: number): ?WebGLShader; + createTexture(): ?WebGLTexture; + + cullFace(mode: number): void; + + deleteBuffer(buffer: ?WebGLBuffer): void; + deleteFramebuffer(framebuffer: ?WebGLFramebuffer): void; + deleteProgram(program: ?WebGLProgram): void; + deleteRenderbuffer(renderbuffer: ?WebGLRenderbuffer): void; + deleteShader(shader: ?WebGLShader): void; + deleteTexture(texture: ?WebGLTexture): void; + + depthFunc(func: number): void; + depthMask(flag: boolean): void; + depthRange(zNear: number, zFar: number): void; + detachShader(program: WebGLProgram, shader: WebGLShader): void; + disable(cap: number): void; + disableVertexAttribArray(index: number): void; + drawArrays(mode: number, first: number, count: number): void; + drawElements(mode: number, count: number, type: number, offset: number): void; + + enable(cap: number): void; + enableVertexAttribArray(index: number): void; + finish(): void; + flush(): void; + framebufferRenderbuffer( + target: number, + attachment: number, + renderbuffertarget: number, + renderbuffer: ?WebGLRenderbuffer + ): void; + framebufferTexture2D( + target: number, + attachment: number, + textarget: number, + texture: ?WebGLTexture, + level: number + ): void; + frontFace(mode: number): void; + + generateMipmap(target: number): void; + + getActiveAttrib(program: WebGLProgram, index: number): ?WebGLActiveInfo; + getActiveUniform(program: WebGLProgram, index: number): ?WebGLActiveInfo; + getAttachedShaders(program: WebGLProgram): ?Array; + + getAttribLocation(program: WebGLProgram, name: string): number; + + getBufferParameter(target: number, pname: number): any; + getParameter(pname: number): any; + + getError(): number; + + getFramebufferAttachmentParameter( + target: number, + attachment: number, + pname: number + ): any; + getProgramParameter(program: WebGLProgram, pname: number): any; + getProgramInfoLog(program: WebGLProgram): ?string; + getRenderbufferParameter(target: number, pname: number): any; + getShaderParameter(shader: WebGLShader, pname: number): any; + getShaderPrecisionFormat( + shadertype: number, + precisiontype: number + ): ?WebGLShaderPrecisionFormat; + getShaderInfoLog(shader: WebGLShader): ?string; + + getShaderSource(shader: WebGLShader): ?string; + + getTexParameter(target: number, pname: number): any; + + getUniform(program: WebGLProgram, location: WebGLUniformLocation): any; + + getUniformLocation( + program: WebGLProgram, + name: string + ): ?WebGLUniformLocation; + + getVertexAttrib(index: number, pname: number): any; + + getVertexAttribOffset(index: number, pname: number): number; + + hint(target: number, mode: number): void; + isBuffer(buffer: ?WebGLBuffer): boolean; + isEnabled(cap: number): boolean; + isFramebuffer(framebuffer: ?WebGLFramebuffer): boolean; + isProgram(program: ?WebGLProgram): boolean; + isRenderbuffer(renderbuffer: ?WebGLRenderbuffer): boolean; + isShader(shader: ?WebGLShader): boolean; + isTexture(texture: ?WebGLTexture): boolean; + lineWidth(width: number): void; + linkProgram(program: WebGLProgram): void; + pixelStorei(pname: number, param: number): void; + polygonOffset(factor: number, units: number): void; + + readPixels( + x: number, + y: number, + width: number, + height: number, + format: number, + type: number, + pixels: ?$ArrayBufferView + ): void; + + renderbufferStorage( + target: number, + internalformat: number, + width: number, + height: number + ): void; + sampleCoverage(value: number, invert: boolean): void; + scissor(x: number, y: number, width: number, height: number): void; + + shaderSource(shader: WebGLShader, source: string): void; + + stencilFunc(func: number, ref: number, mask: number): void; + stencilFuncSeparate( + face: number, + func: number, + ref: number, + mask: number + ): void; + stencilMask(mask: number): void; + stencilMaskSeparate(face: number, mask: number): void; + stencilOp(fail: number, zfail: number, zpass: number): void; + stencilOpSeparate( + face: number, + fail: number, + zfail: number, + zpass: number + ): void; + + texImage2D( + target: number, + level: number, + internalformat: number, + width: number, + height: number, + border: number, + format: number, + type: number, + pixels: ?$ArrayBufferView + ): void; + texImage2D( + target: number, + level: number, + internalformat: number, + format: number, + type: number, + source: TexImageSource + ): void; + + texParameterf(target: number, pname: number, param: number): void; + texParameteri(target: number, pname: number, param: number): void; + + texSubImage2D( + target: number, + level: number, + xoffset: number, + yoffset: number, + width: number, + height: number, + format: number, + type: number, + pixels: ?$ArrayBufferView + ): void; + texSubImage2D( + target: number, + level: number, + xoffset: number, + yoffset: number, + format: number, + type: number, + source: TexImageSource + ): void; + + uniform1f(location: ?WebGLUniformLocation, x: number): void; + uniform1fv(location: ?WebGLUniformLocation, v: Float32Array): void; + uniform1fv(location: ?WebGLUniformLocation, v: Array): void; + uniform1fv(location: ?WebGLUniformLocation, v: [number]): void; + uniform1i(location: ?WebGLUniformLocation, x: number): void; + uniform1iv(location: ?WebGLUniformLocation, v: Int32Array): void; + uniform1iv(location: ?WebGLUniformLocation, v: Array): void; + uniform1iv(location: ?WebGLUniformLocation, v: [number]): void; + uniform2f(location: ?WebGLUniformLocation, x: number, y: number): void; + uniform2fv(location: ?WebGLUniformLocation, v: Float32Array): void; + uniform2fv(location: ?WebGLUniformLocation, v: Array): void; + uniform2fv(location: ?WebGLUniformLocation, v: [number, number]): void; + uniform2i(location: ?WebGLUniformLocation, x: number, y: number): void; + uniform2iv(location: ?WebGLUniformLocation, v: Int32Array): void; + uniform2iv(location: ?WebGLUniformLocation, v: Array): void; + uniform2iv(location: ?WebGLUniformLocation, v: [number, number]): void; + uniform3f( + location: ?WebGLUniformLocation, + x: number, + y: number, + z: number + ): void; + uniform3fv(location: ?WebGLUniformLocation, v: Float32Array): void; + uniform3fv(location: ?WebGLUniformLocation, v: Array): void; + uniform3fv( + location: ?WebGLUniformLocation, + v: [number, number, number] + ): void; + uniform3i( + location: ?WebGLUniformLocation, + x: number, + y: number, + z: number + ): void; + uniform3iv(location: ?WebGLUniformLocation, v: Int32Array): void; + uniform3iv(location: ?WebGLUniformLocation, v: Array): void; + uniform3iv( + location: ?WebGLUniformLocation, + v: [number, number, number] + ): void; + uniform4f( + location: ?WebGLUniformLocation, + x: number, + y: number, + z: number, + w: number + ): void; + uniform4fv(location: ?WebGLUniformLocation, v: Float32Array): void; + uniform4fv(location: ?WebGLUniformLocation, v: Array): void; + uniform4fv( + location: ?WebGLUniformLocation, + v: [number, number, number, number] + ): void; + uniform4i( + location: ?WebGLUniformLocation, + x: number, + y: number, + z: number, + w: number + ): void; + uniform4iv(location: ?WebGLUniformLocation, v: Int32Array): void; + uniform4iv(location: ?WebGLUniformLocation, v: Array): void; + uniform4iv( + location: ?WebGLUniformLocation, + v: [number, number, number, number] + ): void; + + uniformMatrix2fv( + location: ?WebGLUniformLocation, + transpose: boolean, + value: Float32Array + ): void; + uniformMatrix2fv( + location: ?WebGLUniformLocation, + transpose: boolean, + value: Array + ): void; + uniformMatrix3fv( + location: ?WebGLUniformLocation, + transpose: boolean, + value: Float32Array + ): void; + uniformMatrix3fv( + location: ?WebGLUniformLocation, + transpose: boolean, + value: Array + ): void; + uniformMatrix4fv( + location: ?WebGLUniformLocation, + transpose: boolean, + value: Float32Array + ): void; + uniformMatrix4fv( + location: ?WebGLUniformLocation, + transpose: boolean, + value: Array + ): void; + + useProgram(program: ?WebGLProgram): void; + validateProgram(program: WebGLProgram): void; + + vertexAttrib1f(index: number, x: number): void; + vertexAttrib1fv(index: number, values: VertexAttribFVSource): void; + vertexAttrib2f(index: number, x: number, y: number): void; + vertexAttrib2fv(index: number, values: VertexAttribFVSource): void; + vertexAttrib3f(index: number, x: number, y: number, z: number): void; + vertexAttrib3fv(index: number, values: VertexAttribFVSource): void; + vertexAttrib4f( + index: number, + x: number, + y: number, + z: number, + w: number + ): void; + vertexAttrib4fv(index: number, values: VertexAttribFVSource): void; + vertexAttribPointer( + index: number, + size: number, + type: number, + normalized: boolean, + stride: number, + offset: number + ): void; + + viewport(x: number, y: number, width: number, height: number): void; +} + +declare class WebGLContextEvent extends Event { + statusMessage: string; +} + +declare class MediaKeyStatusMap { + @@iterator(): Iterator<[BufferDataSource, MediaKeyStatus]>; + size: number; + entries(): Iterator<[BufferDataSource, MediaKeyStatus]>; + forEach( + callbackfn: ( + value: MediaKeyStatus, + key: BufferDataSource, + map: MediaKeyStatusMap + ) => any, + thisArg?: any + ): void; + get(key: BufferDataSource): MediaKeyStatus; + has(key: BufferDataSource): boolean; + keys(): Iterator; + values(): Iterator; +} + +declare class MediaKeySession extends EventTarget { + sessionId: string; + expiration: number; + closed: Promise; + keyStatuses: MediaKeyStatusMap; + + generateRequest( + initDataType: string, + initData: BufferDataSource + ): Promise; + load(sessionId: string): Promise; + update(response: BufferDataSource): Promise; + close(): Promise; + remove(): Promise; + + onkeystatuschange: (ev: any) => any; + onmessage: (ev: any) => any; +} + +declare class MediaKeys { + createSession(mediaKeySessionType: MediaKeySessionType): MediaKeySession; + setServerCertificate(serverCertificate: BufferDataSource): Promise; +} + +declare class TextRange { + boundingLeft: number; + htmlText: string; + offsetLeft: number; + boundingWidth: number; + boundingHeight: number; + boundingTop: number; + text: string; + offsetTop: number; + moveToPoint(x: number, y: number): void; + queryCommandValue(cmdID: string): any; + getBookmark(): string; + move(unit: string, count?: number): number; + queryCommandIndeterm(cmdID: string): boolean; + scrollIntoView(fStart?: boolean): void; + findText(string: string, count?: number, flags?: number): boolean; + execCommand(cmdID: string, showUI?: boolean, value?: any): boolean; + getBoundingClientRect(): DOMRect; + moveToBookmark(bookmark: string): boolean; + isEqual(range: TextRange): boolean; + duplicate(): TextRange; + collapse(start?: boolean): void; + queryCommandText(cmdID: string): string; + select(): void; + pasteHTML(html: string): void; + inRange(range: TextRange): boolean; + moveEnd(unit: string, count?: number): number; + getClientRects(): DOMRectList; + moveStart(unit: string, count?: number): number; + parentElement(): Element; + queryCommandState(cmdID: string): boolean; + compareEndPoints(how: string, sourceRange: TextRange): number; + execCommandShowHelp(cmdID: string): boolean; + moveToElementText(element: Element): void; + expand(Unit: string): boolean; + queryCommandSupported(cmdID: string): boolean; + setEndPoint(how: string, SourceRange: TextRange): void; + queryCommandEnabled(cmdID: string): boolean; +} + +// These types used to exist as a copy of DOMRect/DOMRectList, which is +// incorrect because there are no ClientRect/ClientRectList globals on the DOM. +// Keep these as type aliases for backwards compatibility. +declare type ClientRect = DOMRect; +declare type ClientRectList = DOMRectList; + +// TODO: HTML*Element + +declare class DOMImplementation { + createDocumentType( + qualifiedName: string, + publicId: string, + systemId: string + ): DocumentType; + createDocument( + namespaceURI: string | null, + qualifiedName: string, + doctype?: DocumentType | null + ): Document; + hasFeature(feature: string, version?: string): boolean; + + // non-standard + createHTMLDocument(title?: string): Document; +} + +declare class DocumentType extends Node { + name: string; + notations: NamedNodeMap; + systemId: string; + internalSubset: string; + entities: NamedNodeMap; + publicId: string; + + // from ChildNode interface + after(...nodes: Array): void; + before(...nodes: Array): void; + replaceWith(...nodes: Array): void; + remove(): void; +} + +declare class CharacterData extends Node { + length: number; + data: string; + deleteData(offset: number, count: number): void; + replaceData(offset: number, count: number, arg: string): void; + appendData(arg: string): void; + insertData(offset: number, arg: string): void; + substringData(offset: number, count: number): string; + + // from ChildNode interface + after(...nodes: Array): void; + before(...nodes: Array): void; + replaceWith(...nodes: Array): void; + remove(): void; +} + +declare class Text extends CharacterData { + assignedSlot?: HTMLSlotElement; + wholeText: string; + splitText(offset: number): Text; + replaceWholeText(content: string): Text; +} + +declare class Comment extends CharacterData { + text: string; +} + +declare class URL { + static canParse(url: string, base?: string): boolean; + static createObjectURL(blob: Blob): string; + static createObjectURL(mediaSource: MediaSource): string; + static revokeObjectURL(url: string): void; + static parse(url: string, base?: string): URL | null; + constructor(url: string, base?: string | URL): void; + hash: string; + host: string; + hostname: string; + href: string; + +origin: string; + password: string; + pathname: string; + port: string; + protocol: string; + search: string; + +searchParams: URLSearchParams; + username: string; + toString(): string; + toJSON(): string; +} + +declare interface MediaSourceHandle {} + +declare class MediaSource extends EventTarget { + sourceBuffers: SourceBufferList; + activeSourceBuffers: SourceBufferList; + // https://w3c.github.io/media-source/#dom-readystate + readyState: 'closed' | 'open' | 'ended'; + duration: number; + handle: MediaSourceHandle; + addSourceBuffer(type: string): SourceBuffer; + removeSourceBuffer(sourceBuffer: SourceBuffer): void; + endOfStream(error?: string): void; + static isTypeSupported(type: string): boolean; +} + +declare class SourceBuffer extends EventTarget { + mode: 'segments' | 'sequence'; + updating: boolean; + buffered: TimeRanges; + timestampOffset: number; + audioTracks: AudioTrackList; + videoTracks: VideoTrackList; + textTracks: TextTrackList; + appendWindowStart: number; + appendWindowEnd: number; + + appendBuffer(data: ArrayBuffer | $ArrayBufferView): void; + // TODO: Add ReadableStream + // appendStream(stream: ReadableStream, maxSize?: number): void; + abort(): void; + remove(start: number, end: number): void; + + trackDefaults: TrackDefaultList; +} + +declare class SourceBufferList extends EventTarget { + @@iterator(): Iterator; + [index: number]: SourceBuffer; + length: number; +} + +declare class TrackDefaultList { + [index: number]: TrackDefault; + length: number; +} + +declare class TrackDefault { + type: 'audio' | 'video' | 'text'; + byteStreamTrackID: string; + language: string; + label: string; + kinds: Array; +} + +// TODO: The use of `typeof` makes this function signature effectively +// (node: Node) => number, but it should be (node: Node) => 1|2|3 +type NodeFilterCallback = ( + node: Node +) => + | typeof NodeFilter.FILTER_ACCEPT + | typeof NodeFilter.FILTER_REJECT + | typeof NodeFilter.FILTER_SKIP; + +type NodeFilterInterface = + | NodeFilterCallback + | {acceptNode: NodeFilterCallback, ...}; + +// TODO: window.NodeFilter exists at runtime and behaves as a constructor +// as far as `instanceof` is concerned, but it is not callable. +declare class NodeFilter { + static SHOW_ALL: -1; + static SHOW_ELEMENT: 1; + static SHOW_ATTRIBUTE: 2; // deprecated + static SHOW_TEXT: 4; + static SHOW_CDATA_SECTION: 8; // deprecated + static SHOW_ENTITY_REFERENCE: 16; // deprecated + static SHOW_ENTITY: 32; // deprecated + static SHOW_PROCESSING_INSTRUCTION: 64; + static SHOW_COMMENT: 128; + static SHOW_DOCUMENT: 256; + static SHOW_DOCUMENT_TYPE: 512; + static SHOW_DOCUMENT_FRAGMENT: 1024; + static SHOW_NOTATION: 2048; // deprecated + static FILTER_ACCEPT: 1; + static FILTER_REJECT: 2; + static FILTER_SKIP: 3; + acceptNode: NodeFilterCallback; +} + +// TODO: window.NodeIterator exists at runtime and behaves as a constructor +// as far as `instanceof` is concerned, but it is not callable. +declare class NodeIterator { + root: RootNodeT; + whatToShow: number; + filter: NodeFilter; + expandEntityReferences: boolean; + referenceNode: RootNodeT | WhatToShowT; + pointerBeforeReferenceNode: boolean; + detach(): void; + previousNode(): WhatToShowT | null; + nextNode(): WhatToShowT | null; +} + +// TODO: window.TreeWalker exists at runtime and behaves as a constructor +// as far as `instanceof` is concerned, but it is not callable. +declare class TreeWalker { + root: RootNodeT; + whatToShow: number; + filter: NodeFilter; + expandEntityReferences: boolean; + currentNode: RootNodeT | WhatToShowT; + parentNode(): WhatToShowT | null; + firstChild(): WhatToShowT | null; + lastChild(): WhatToShowT | null; + previousSibling(): WhatToShowT | null; + nextSibling(): WhatToShowT | null; + previousNode(): WhatToShowT | null; + nextNode(): WhatToShowT | null; +} + +/* Window file picker */ + +type WindowFileSystemPickerFileType = {| + description?: string, + /* + * An Object with the keys set to the MIME type + * and the values an Array of file extensions + * Example: + * accept: { + * "image/*": [".png", ".gif", ".jpeg", ".jpg"], + * }, + */ + accept: { + [string]: Array, + }, +|}; + +type WindowBaseFilePickerOptions = {| + id?: number, + startIn?: + | FileSystemHandle + | 'desktop' + | 'documents' + | 'downloads' + | 'music' + | 'pictures' + | 'videos', +|}; + +type WindowFilePickerOptions = WindowBaseFilePickerOptions & {| + excludeAcceptAllOption?: boolean, + types?: Array, +|}; + +type WindowOpenFilePickerOptions = WindowFilePickerOptions & {| + multiple?: boolean, +|}; + +type WindowSaveFilePickerOptions = WindowFilePickerOptions & {| + suggestedName?: string, +|}; + +type WindowDirectoryFilePickerOptions = WindowBaseFilePickerOptions & {| + mode?: 'read' | 'readwrite', +|}; + +// https://wicg.github.io/file-system-access/#api-showopenfilepicker +declare function showOpenFilePicker( + options?: WindowOpenFilePickerOptions +): Promise>; + +// https://wicg.github.io/file-system-access/#api-showsavefilepicker +declare function showSaveFilePicker( + options?: WindowSaveFilePickerOptions +): Promise; + +// https://wicg.github.io/file-system-access/#api-showdirectorypicker +declare function showDirectoryPicker( + options?: WindowDirectoryFilePickerOptions +): Promise; + +/* Notification */ +type NotificationPermission = 'default' | 'denied' | 'granted'; +type NotificationDirection = 'auto' | 'ltr' | 'rtl'; +type VibratePattern = number | Array; +type NotificationAction = { + action: string, + title: string, + icon?: string, + ... +}; +type NotificationOptions = { + dir?: NotificationDirection, + lang?: string, + body?: string, + tag?: string, + image?: string, + icon?: string, + badge?: string, + sound?: string, + vibrate?: VibratePattern, + timestamp?: number, + renotify?: boolean, + silent?: boolean, + requireInteraction?: boolean, + data?: ?any, + actions?: Array, + ... +}; + +declare class Notification extends EventTarget { + constructor(title: string, options?: NotificationOptions): void; + static +permission: NotificationPermission; + static requestPermission( + callback?: (perm: NotificationPermission) => mixed + ): Promise; + static +maxActions: number; + onclick: ?(evt: Event) => mixed; + onclose: ?(evt: Event) => mixed; + onerror: ?(evt: Event) => mixed; + onshow: ?(evt: Event) => mixed; + +title: string; + +dir: NotificationDirection; + +lang: string; + +body: string; + +tag: string; + +image?: string; + +icon?: string; + +badge?: string; + +vibrate?: Array; + +timestamp: number; + +renotify: boolean; + +silent: boolean; + +requireInteraction: boolean; + +data: any; + +actions: Array; + + close(): void; +} diff --git a/flow-typed/environments/geometry.js b/flow-typed/environments/geometry.js new file mode 100644 index 0000000000000..dbdd50267b3d0 --- /dev/null +++ b/flow-typed/environments/geometry.js @@ -0,0 +1,270 @@ +// flow-typed signature: c29a716c1825927cdfc3ad29fe929754 +// flow-typed version: 52ab99c6db/geometry/flow_>=v0.261.x + +// https://www.w3.org/TR/geometry-1/ + +type DOMMatrix2DInit = + | {| + a: number, + b: number, + c: number, + d: number, + e: number, + f: number, + |} + | {| + m11: number, + m12: number, + m21: number, + m22: number, + m41: number, + m42: number, + |}; + +type DOMMatrixInit = + | {| + ...DOMMatrix2DInit, + is2D: true, + |} + | {| + ...DOMMatrix2DInit, + is2D: false, + m13: number, + m14: number, + m23: number, + m24: number, + m31: number, + m32: number, + m33: number, + m34: number, + m43: number, + m44: number, + |}; + +type DOMPointInit = {| + w: number, + x: number, + y: number, + z: number, +|}; + +type DOMQuadInit = {| + p1: DOMPointInit, + p2: DOMPointInit, + p3: DOMPointInit, + p4: DOMPointInit, +|}; + +type DOMRectInit = {| + height: number, + width: number, + x: number, + y: number, +|}; + +declare class DOMMatrix extends DOMMatrixReadOnly { + a: number; + b: number; + c: number; + d: number; + e: number; + f: number; + m11: number; + m12: number; + m13: number; + m14: number; + m21: number; + m22: number; + m23: number; + m24: number; + m31: number; + m32: number; + m33: number; + m34: number; + m41: number; + m42: number; + m43: number; + m44: number; + + static fromFloat32Array(array32: Float32Array): DOMMatrix; + static fromFloat64Array(array64: Float64Array): DOMMatrix; + static fromMatrix(other?: DOMMatrixInit): DOMMatrix; + + constructor(init?: string | Array): void; + invertSelf(): DOMMatrix; + multiplySelf(other?: DOMMatrixInit): DOMMatrix; + preMultiplySelf(other?: DOMMatrixInit): DOMMatrix; + rotateAxisAngleSelf( + x?: number, + y?: number, + z?: number, + angle?: number + ): DOMMatrix; + rotateFromVectorSelf(x?: number, y?: number): DOMMatrix; + rotateSelf(rotX?: number, rotY?: number, rotZ?: number): DOMMatrix; + scale3dSelf( + scale?: number, + originX?: number, + originY?: number, + originZ?: number + ): DOMMatrix; + scaleSelf( + scaleX?: number, + scaleY?: number, + scaleZ?: number, + originX?: number, + originY?: number, + originZ?: number + ): DOMMatrix; + setMatrixValue(transformList: string): DOMMatrix; + skewXSelf(sx?: number): DOMMatrix; + skewYSelf(sy?: number): DOMMatrix; + translateSelf(tx?: number, ty?: number, tz?: number): DOMMatrix; +} + +declare class DOMMatrixReadOnly { + +a: number; + +b: number; + +c: number; + +d: number; + +e: number; + +f: number; + +is2D: boolean; + +isIdentity: boolean; + +m11: number; + +m12: number; + +m13: number; + +m14: number; + +m21: number; + +m22: number; + +m23: number; + +m24: number; + +m31: number; + +m32: number; + +m33: number; + +m34: number; + +m41: number; + +m42: number; + +m43: number; + +m44: number; + + static fromFloat32Array(array32: Float32Array): DOMMatrixReadOnly; + static fromFloat64Array(array64: Float64Array): DOMMatrixReadOnly; + static fromMatrix(other?: DOMMatrixInit): DOMMatrixReadOnly; + + constructor(init?: string | Array): void; + flipX(): DOMMatrix; + flipY(): DOMMatrix; + inverse(): DOMMatrix; + multiply(other?: DOMMatrixInit): DOMMatrix; + rotate(rotX?: number, rotY?: number, rotZ?: number): DOMMatrix; + rotateAxisAngle( + x?: number, + y?: number, + z?: number, + angle?: number + ): DOMMatrix; + rotateFromVector(x?: number, y?: number): DOMMatrix; + scale( + scaleX?: number, + scaleY?: number, + scaleZ?: number, + originX?: number, + originY?: number, + originZ?: number + ): DOMMatrix; + scale3d( + scale?: number, + originX?: number, + originY?: number, + originZ?: number + ): DOMMatrix; + scaleNonUniform(scaleX?: number, scaleY?: number): DOMMatrix; + skewX(sx?: number): DOMMatrix; + skewY(sy?: number): DOMMatrix; + toFloat32Array(): Float32Array; + toFloat64Array(): Float64Array; + toJSON(): Object; + transformPoint(point?: DOMPointInit): DOMPoint; + translate(tx?: number, ty?: number, tz?: number): DOMMatrix; + toString(): string; +} + +declare class DOMPoint extends DOMPointReadOnly { + w: number; + x: number; + y: number; + z: number; + + static fromPoint(other?: DOMPointInit): DOMPoint; + + constructor(x?: number, y?: number, z?: number, w?: number): void; +} + +declare class DOMPointReadOnly { + +w: number; + +x: number; + +y: number; + +z: number; + + static fromPoint(other?: DOMPointInit): DOMPointReadOnly; + + constructor(x?: number, y?: number, z?: number, w?: number): void; + matrixTransform(matrix?: DOMMatrixInit): DOMPoint; + toJSON(): Object; +} + +declare class DOMQuad { + +p1: DOMPoint; + +p2: DOMPoint; + +p3: DOMPoint; + +p4: DOMPoint; + + static fromQuad(other?: DOMQuadInit): DOMQuad; + static fromRect(other?: DOMRectInit): DOMQuad; + + constructor( + p1?: DOMPointInit, + p2?: DOMPointInit, + p3?: DOMPointInit, + p4?: DOMPointInit + ): void; + getBounds(): DOMRect; + toJSON(): Object; +} + +declare class DOMRect extends DOMRectReadOnly { + height: number; + width: number; + x: number; + y: number; + + constructor(x?: number, y?: number, width?: number, height?: number): void; + + static fromRect(other?: DOMRectInit): DOMRect; +} + +declare class DOMRectList { + +length: number; + + @@iterator(): Iterator; + + item(index: number): DOMRect; + [index: number]: DOMRect; +} + +declare class DOMRectReadOnly { + +bottom: number; + +height: number; + +left: number; + +right: number; + +top: number; + +width: number; + +x: number; + +y: number; + + constructor(x?: number, y?: number, width?: number, height?: number): void; + + static fromRect(other?: DOMRectInit): DOMRectReadOnly; + toJSON(): Object; +} diff --git a/flow-typed/environments/html.js b/flow-typed/environments/html.js new file mode 100644 index 0000000000000..54e1e48f7396f --- /dev/null +++ b/flow-typed/environments/html.js @@ -0,0 +1,1710 @@ +// flow-typed signature: 760aeea3b9b767e808097fe22b68a20f +// flow-typed version: 8584579196/html/flow_>=v0.261.x + +/* DataTransfer */ + +declare class DataTransfer { + clearData(format?: string): void; + getData(format: string): string; + setData(format: string, data: string): void; + setDragImage(image: Element, x: number, y: number): void; + dropEffect: string; + effectAllowed: string; + files: FileList; // readonly + items: DataTransferItemList; // readonly + types: Array; // readonly +} + +declare class DataTransferItemList { + @@iterator(): Iterator; + length: number; // readonly + [index: number]: DataTransferItem; + add(data: string, type: string): ?DataTransferItem; + add(data: File): ?DataTransferItem; + remove(index: number): void; + clear(): void; +} + +// https://wicg.github.io/file-system-access/#drag-and-drop +declare class DataTransferItem { + kind: string; // readonly + type: string; // readonly + getAsString(_callback: ?(data: string) => mixed): void; + getAsFile(): ?File; + /* + * This is not supported by all browsers, please have a fallback plan for it. + * For more information, please checkout + * https://developer.mozilla.org/en-US/docs/Web/API/DataTransferItem/webkitGetAsEntry + */ + webkitGetAsEntry(): void | (() => any); + /* + * Not supported in all browsers + * For up to date compatibility information, please visit + * https://developer.mozilla.org/en-US/docs/Web/API/DataTransferItem/getAsFileSystemHandle + */ + getAsFileSystemHandle?: () => Promise; +} + +declare type DOMStringMap = {[key: string]: string, ...}; + +declare class DOMStringList { + @@iterator(): Iterator; + +[key: number]: string; + +length: number; + item(number): string | null; + contains(string): boolean; +} + +declare type ElementDefinitionOptions = {|extends?: string|}; + +declare interface CustomElementRegistry { + define( + name: string, + ctor: Class, + options?: ElementDefinitionOptions + ): void; + get(name: string): any; + whenDefined(name: string): Promise; +} + +// https://www.w3.org/TR/eventsource/ +declare class EventSource extends EventTarget { + constructor( + url: string, + configuration?: {withCredentials: boolean, ...} + ): void; + +CLOSED: 2; + +CONNECTING: 0; + +OPEN: 1; + +readyState: 0 | 1 | 2; + +url: string; + +withCredentials: boolean; + onerror: () => void; + onmessage: MessageEventListener; + onopen: () => void; + close: () => void; +} + +// https://html.spec.whatwg.org/multipage/webappapis.html#the-errorevent-interface +declare class ErrorEvent extends Event { + constructor( + type: string, + eventInitDict?: { + ...Event$Init, + message?: string, + filename?: string, + lineno?: number, + colno?: number, + error?: any, + ... + } + ): void; + +message: string; + +filename: string; + +lineno: number; + +colno: number; + +error: any; +} + +// https://html.spec.whatwg.org/multipage/web-messaging.html#broadcasting-to-other-browsing-contexts +declare class BroadcastChannel extends EventTarget { + name: string; + onmessage: ?(event: MessageEvent) => void; + onmessageerror: ?(event: MessageEvent) => void; + + constructor(name: string): void; + postMessage(msg: mixed): void; + close(): void; +} + +// https://www.w3.org/TR/webstorage/#the-storageevent-interface +declare class StorageEvent extends Event { + key: ?string; + oldValue: ?string; + newValue: ?string; + url: string; + storageArea: ?Storage; +} + +// https://www.w3.org/TR/html50/browsers.html#beforeunloadevent +declare class BeforeUnloadEvent extends Event { + returnValue: string; +} + +type ToggleEvent$Init = { + ...Event$Init, + oldState: string, + newState: string, + ... +}; + +declare class ToggleEvent extends Event { + constructor(type: ToggleEventTypes, eventInit?: ToggleEvent$Init): void; + +oldState: string; + +newState: string; +} + +// TODO: HTMLDocument +type FocusOptions = {preventScroll?: boolean, ...}; + +declare class HTMLElement extends Element { + blur(): void; + click(): void; + focus(options?: FocusOptions): void; + getBoundingClientRect(): DOMRect; + forceSpellcheck(): void; + + showPopover(options?: {|source?: HTMLElement|}): void; + hidePopover(): void; + togglePopover( + options?: boolean | {|force?: boolean, source?: HTMLElement|} + ): boolean; + + accessKey: string; + accessKeyLabel: string; + contentEditable: string; + contextMenu: ?HTMLMenuElement; + dataset: DOMStringMap; + dir: 'ltr' | 'rtl' | 'auto'; + draggable: boolean; + dropzone: any; + hidden: boolean; + inert: boolean; + isContentEditable: boolean; + itemProp: any; + itemScope: boolean; + itemType: any; + itemValue: Object; + lang: string; + offsetHeight: number; + offsetLeft: number; + offsetParent: ?Element; + offsetTop: number; + offsetWidth: number; + onabort: ?Function; + onblur: ?Function; + oncancel: ?Function; + oncanplay: ?Function; + oncanplaythrough: ?Function; + onchange: ?Function; + onclick: ?Function; + oncontextmenu: ?Function; + oncuechange: ?Function; + ondblclick: ?Function; + ondurationchange: ?Function; + onemptied: ?Function; + onended: ?Function; + onerror: ?Function; + onfocus: ?Function; + onfullscreenchange: ?Function; + onfullscreenerror: ?Function; + ongotpointercapture: ?Function; + oninput: ?Function; + oninvalid: ?Function; + onkeydown: ?Function; + onkeypress: ?Function; + onkeyup: ?Function; + onload: ?Function; + onloadeddata: ?Function; + onloadedmetadata: ?Function; + onloadstart: ?Function; + onlostpointercapture: ?Function; + onmousedown: ?Function; + onmouseenter: ?Function; + onmouseleave: ?Function; + onmousemove: ?Function; + onmouseout: ?Function; + onmouseover: ?Function; + onmouseup: ?Function; + onmousewheel: ?Function; + onpause: ?Function; + onplay: ?Function; + onplaying: ?Function; + onpointercancel: ?Function; + onpointerdown: ?Function; + onpointerenter: ?Function; + onpointerleave: ?Function; + onpointermove: ?Function; + onpointerout: ?Function; + onpointerover: ?Function; + onpointerup: ?Function; + onprogress: ?Function; + onratechange: ?Function; + onreadystatechange: ?Function; + onreset: ?Function; + onresize: ?Function; + onscroll: ?Function; + onseeked: ?Function; + onseeking: ?Function; + onselect: ?Function; + onshow: ?Function; + onstalled: ?Function; + onsubmit: ?Function; + onsuspend: ?Function; + ontimeupdate: ?Function; + ontoggle: ?Function; + onbeforetoggle: ?Function; + onvolumechange: ?Function; + onwaiting: ?Function; + properties: any; + spellcheck: boolean; + style: CSSStyleDeclaration; + tabIndex: number; + title: string; + translate: boolean; + popover: '' | 'auto' | 'manual' | 'hint'; + + +popoverVisibilityState: 'hidden' | 'showing'; + + +popoverInvoker: HTMLElement | null; +} + +declare class HTMLSlotElement extends HTMLElement { + name: string; + assignedNodes(options?: {flatten: boolean, ...}): Node[]; +} + +declare class HTMLTableElement extends HTMLElement { + tagName: 'TABLE'; + caption: HTMLTableCaptionElement | null; + tHead: HTMLTableSectionElement | null; + tFoot: HTMLTableSectionElement | null; + +tBodies: HTMLCollection; + +rows: HTMLCollection; + createTHead(): HTMLTableSectionElement; + deleteTHead(): void; + createTFoot(): HTMLTableSectionElement; + deleteTFoot(): void; + createCaption(): HTMLTableCaptionElement; + deleteCaption(): void; + insertRow(index?: number): HTMLTableRowElement; + deleteRow(index: number): void; +} + +declare class HTMLTableCaptionElement extends HTMLElement { + tagName: 'CAPTION'; +} + +declare class HTMLTableColElement extends HTMLElement { + tagName: 'COL' | 'COLGROUP'; + span: number; +} + +declare class HTMLTableSectionElement extends HTMLElement { + tagName: 'THEAD' | 'TFOOT' | 'TBODY'; + +rows: HTMLCollection; + insertRow(index?: number): HTMLTableRowElement; + deleteRow(index: number): void; +} + +declare class HTMLTableCellElement extends HTMLElement { + tagName: 'TD' | 'TH'; + colSpan: number; + rowSpan: number; + +cellIndex: number; +} + +declare class HTMLTableRowElement extends HTMLElement { + tagName: 'TR'; + align: 'left' | 'right' | 'center'; + +rowIndex: number; + +sectionRowIndex: number; + +cells: HTMLCollection; + deleteCell(index: number): void; + insertCell(index?: number): HTMLTableCellElement; +} + +declare class HTMLMenuElement extends HTMLElement { + getCompact(): boolean; + setCompact(compact: boolean): void; +} + +declare class HTMLBaseElement extends HTMLElement { + href: string; + target: string; +} + +declare class HTMLTemplateElement extends HTMLElement { + content: DocumentFragment; +} + +declare class CanvasGradient { + addColorStop(offset: number, color: string): void; +} + +declare class CanvasPattern { + setTransform(matrix: SVGMatrix): void; +} + +declare class ImageBitmap { + close(): void; + width: number; + height: number; +} + +type CanvasFillRule = string; + +type CanvasImageSource = + | HTMLImageElement + | HTMLVideoElement + | HTMLCanvasElement + | CanvasRenderingContext2D + | ImageBitmap; + +declare class TextMetrics { + // x-direction + width: number; + actualBoundingBoxLeft: number; + actualBoundingBoxRight: number; + + // y-direction + fontBoundingBoxAscent: number; + fontBoundingBoxDescent: number; + actualBoundingBoxAscent: number; + actualBoundingBoxDescent: number; + emHeightAscent: number; + emHeightDescent: number; + hangingBaseline: number; + alphabeticBaseline: number; + ideographicBaseline: number; +} + +declare class CanvasDrawingStyles { + width: number; + actualBoundingBoxLeft: number; + actualBoundingBoxRight: number; + + // y-direction + fontBoundingBoxAscent: number; + fontBoundingBoxDescent: number; + actualBoundingBoxAscent: number; + actualBoundingBoxDescent: number; + emHeightAscent: number; + emHeightDescent: number; + hangingBaseline: number; + alphabeticBaseline: number; + ideographicBaseline: number; +} + +declare class Path2D { + constructor(path?: Path2D | string): void; + + addPath(path: Path2D, transformation?: ?SVGMatrix): void; + addPathByStrokingPath( + path: Path2D, + styles: CanvasDrawingStyles, + transformation?: ?SVGMatrix + ): void; + addText( + text: string, + styles: CanvasDrawingStyles, + transformation: ?SVGMatrix, + x: number, + y: number, + maxWidth?: number + ): void; + addPathByStrokingText( + text: string, + styles: CanvasDrawingStyles, + transformation: ?SVGMatrix, + x: number, + y: number, + maxWidth?: number + ): void; + addText( + text: string, + styles: CanvasDrawingStyles, + transformation: ?SVGMatrix, + path: Path2D, + maxWidth?: number + ): void; + addPathByStrokingText( + text: string, + styles: CanvasDrawingStyles, + transformation: ?SVGMatrix, + path: Path2D, + maxWidth?: number + ): void; + + // CanvasPathMethods + // shared path API methods + arc( + x: number, + y: number, + radius: number, + startAngle: number, + endAngle: number, + anticlockwise?: boolean + ): void; + arcTo( + x1: number, + y1: number, + x2: number, + y2: number, + radius: number, + _: void, + _: void + ): void; + arcTo( + x1: number, + y1: number, + x2: number, + y2: number, + radiusX: number, + radiusY: number, + rotation: number + ): void; + bezierCurveTo( + cp1x: number, + cp1y: number, + cp2x: number, + cp2y: number, + x: number, + y: number + ): void; + closePath(): void; + ellipse( + x: number, + y: number, + radiusX: number, + radiusY: number, + rotation: number, + startAngle: number, + endAngle: number, + anticlockwise?: boolean + ): void; + lineTo(x: number, y: number): void; + moveTo(x: number, y: number): void; + quadraticCurveTo(cpx: number, cpy: number, x: number, y: number): void; + rect(x: number, y: number, w: number, h: number): void; +} + +declare class ImageData { + width: number; + height: number; + data: Uint8ClampedArray; + + // constructor methods are used in Worker where CanvasRenderingContext2D + // is unavailable. + // https://html.spec.whatwg.org/multipage/scripting.html#dom-imagedata + constructor(data: Uint8ClampedArray, width: number, height: number): void; + constructor(width: number, height: number): void; +} + +declare class CanvasRenderingContext2D { + canvas: HTMLCanvasElement; + + // canvas dimensions + width: number; + height: number; + + // for contexts that aren't directly fixed to a specific canvas + commit(): void; + + // state + save(): void; + restore(): void; + + // transformations + currentTransform: SVGMatrix; + scale(x: number, y: number): void; + rotate(angle: number): void; + translate(x: number, y: number): void; + transform( + a: number, + b: number, + c: number, + d: number, + e: number, + f: number + ): void; + setTransform( + a: number, + b: number, + c: number, + d: number, + e: number, + f: number + ): void; + resetTransform(): void; + + // compositing + globalAlpha: number; + globalCompositeOperation: string; + + // image smoothing + imageSmoothingEnabled: boolean; + imageSmoothingQuality: 'low' | 'medium' | 'high'; + + // filters + filter: string; + + // colours and styles + strokeStyle: string | CanvasGradient | CanvasPattern; + fillStyle: string | CanvasGradient | CanvasPattern; + createLinearGradient( + x0: number, + y0: number, + x1: number, + y1: number + ): CanvasGradient; + createRadialGradient( + x0: number, + y0: number, + r0: number, + x1: number, + y1: number, + r1: number + ): CanvasGradient; + createPattern(image: CanvasImageSource, repetition: ?string): CanvasPattern; + + // shadows + shadowOffsetX: number; + shadowOffsetY: number; + shadowBlur: number; + shadowColor: string; + + // rects + clearRect(x: number, y: number, w: number, h: number): void; + fillRect(x: number, y: number, w: number, h: number): void; + roundRect( + x: number, + y: number, + w: number, + h: number, + radii?: number | DOMPointInit | $ReadOnlyArray + ): void; + strokeRect(x: number, y: number, w: number, h: number): void; + + // path API + beginPath(): void; + fill(fillRule?: CanvasFillRule): void; + fill(path: Path2D, fillRule?: CanvasFillRule): void; + stroke(): void; + stroke(path: Path2D): void; + drawFocusIfNeeded(element: Element): void; + drawFocusIfNeeded(path: Path2D, element: Element): void; + scrollPathIntoView(): void; + scrollPathIntoView(path: Path2D): void; + clip(fillRule?: CanvasFillRule): void; + clip(path: Path2D, fillRule?: CanvasFillRule): void; + resetClip(): void; + isPointInPath(x: number, y: number, fillRule?: CanvasFillRule): boolean; + isPointInPath( + path: Path2D, + x: number, + y: number, + fillRule?: CanvasFillRule + ): boolean; + isPointInStroke(x: number, y: number): boolean; + isPointInStroke(path: Path2D, x: number, y: number): boolean; + + // text (see also the CanvasDrawingStyles interface) + fillText(text: string, x: number, y: number, maxWidth?: number): void; + strokeText(text: string, x: number, y: number, maxWidth?: number): void; + measureText(text: string): TextMetrics; + + // drawing images + drawImage(image: CanvasImageSource, dx: number, dy: number): void; + drawImage( + image: CanvasImageSource, + dx: number, + dy: number, + dw: number, + dh: number + ): void; + drawImage( + image: CanvasImageSource, + sx: number, + sy: number, + sw: number, + sh: number, + dx: number, + dy: number, + dw: number, + dh: number + ): void; + + // hit regions + addHitRegion(options?: HitRegionOptions): void; + removeHitRegion(id: string): void; + clearHitRegions(): void; + + // pixel manipulation + createImageData(sw: number, sh: number): ImageData; + createImageData(imagedata: ImageData): ImageData; + getImageData(sx: number, sy: number, sw: number, sh: number): ImageData; + putImageData(imagedata: ImageData, dx: number, dy: number): void; + putImageData( + imagedata: ImageData, + dx: number, + dy: number, + dirtyX: number, + dirtyY: number, + dirtyWidth: number, + dirtyHeight: number + ): void; + + // CanvasDrawingStyles + // line caps/joins + lineWidth: number; + lineCap: string; + lineJoin: string; + miterLimit: number; + + // dashed lines + setLineDash(segments: Array): void; + getLineDash(): Array; + lineDashOffset: number; + + // text + font: string; + textAlign: string; + textBaseline: string; + direction: string; + + // CanvasPathMethods + // shared path API methods + closePath(): void; + moveTo(x: number, y: number): void; + lineTo(x: number, y: number): void; + quadraticCurveTo(cpx: number, cpy: number, x: number, y: number): void; + bezierCurveTo( + cp1x: number, + cp1y: number, + cp2x: number, + cp2y: number, + x: number, + y: number + ): void; + arcTo(x1: number, y1: number, x2: number, y2: number, radius: number): void; + arcTo( + x1: number, + y1: number, + x2: number, + y2: number, + radiusX: number, + radiusY: number, + rotation: number + ): void; + rect(x: number, y: number, w: number, h: number): void; + arc( + x: number, + y: number, + radius: number, + startAngle: number, + endAngle: number, + anticlockwise?: boolean + ): void; + ellipse( + x: number, + y: number, + radiusX: number, + radiusY: number, + rotation: number, + startAngle: number, + endAngle: number, + anticlockwise?: boolean + ): void; +} + +// http://www.w3.org/TR/html5/scripting-1.html#renderingcontext +type RenderingContext = CanvasRenderingContext2D | WebGLRenderingContext; + +// https://www.w3.org/TR/html5/scripting-1.html#htmlcanvaselement +declare class HTMLCanvasElement extends HTMLElement { + tagName: 'CANVAS'; + width: number; + height: number; + getContext(contextId: '2d', ...args: any): CanvasRenderingContext2D; + getContext( + contextId: 'webgl', + contextAttributes?: Partial + ): ?WebGLRenderingContext; + // IE currently only supports "experimental-webgl" + getContext( + contextId: 'experimental-webgl', + contextAttributes?: Partial + ): ?WebGLRenderingContext; + getContext(contextId: string, ...args: any): ?RenderingContext; // fallback + toDataURL(type?: string, ...args: any): string; + toBlob(callback: (v: File) => void, type?: string, ...args: any): void; + captureStream(frameRate?: number): CanvasCaptureMediaStream; +} + +// https://html.spec.whatwg.org/multipage/forms.html#the-details-element +declare class HTMLDetailsElement extends HTMLElement { + tagName: 'DETAILS'; + open: boolean; +} + +declare class HTMLFormElement extends HTMLElement { + tagName: 'FORM'; + @@iterator(): Iterator; + [index: number | string]: HTMLElement | null; + acceptCharset: string; + action: string; + elements: HTMLCollection; + encoding: string; + enctype: string; + length: number; + method: string; + name: string; + rel: string; + target: string; + + checkValidity(): boolean; + reportValidity(): boolean; + reset(): void; + submit(): void; +} + +// https://www.w3.org/TR/html5/forms.html#the-fieldset-element +declare class HTMLFieldSetElement extends HTMLElement { + tagName: 'FIELDSET'; + disabled: boolean; + elements: HTMLCollection; // readonly + form: HTMLFormElement | null; // readonly + name: string; + type: string; // readonly + + checkValidity(): boolean; + setCustomValidity(error: string): void; +} + +declare class HTMLLegendElement extends HTMLElement { + tagName: 'LEGEND'; + form: HTMLFormElement | null; // readonly +} + +declare class HTMLIFrameElement extends HTMLElement { + tagName: 'IFRAME'; + allowFullScreen: boolean; + contentDocument: Document; + contentWindow: any; + frameBorder: string; + height: string; + marginHeight: string; + marginWidth: string; + name: string; + scrolling: string; + sandbox: DOMTokenList; + src: string; + // flowlint unsafe-getters-setters:off + get srcdoc(): string; + set srcdoc(value: string | TrustedHTML): void; + // flowlint unsafe-getters-setters:error + width: string; +} + +declare class HTMLImageElement extends HTMLElement { + tagName: 'IMG'; + alt: string; + complete: boolean; // readonly + crossOrigin: ?string; + currentSrc: string; // readonly + height: number; + decode(): Promise; + isMap: boolean; + naturalHeight: number; // readonly + naturalWidth: number; // readonly + sizes: string; + src: string; + srcset: string; + useMap: string; + width: number; +} + +declare class Image extends HTMLImageElement { + constructor(width?: number, height?: number): void; +} + +declare class MediaError { + MEDIA_ERR_ABORTED: number; + MEDIA_ERR_NETWORK: number; + MEDIA_ERR_DECODE: number; + MEDIA_ERR_SRC_NOT_SUPPORTED: number; + code: number; + message: ?string; +} + +declare class TimeRanges { + length: number; + start(index: number): number; + end(index: number): number; +} + +declare class Audio extends HTMLAudioElement { + constructor(URLString?: string): void; +} + +declare class AudioTrack { + id: string; + kind: string; + label: string; + language: string; + enabled: boolean; +} + +declare class AudioTrackList extends EventTarget { + length: number; + [index: number]: AudioTrack; + + getTrackById(id: string): ?AudioTrack; + + onchange: (ev: any) => any; + onaddtrack: (ev: any) => any; + onremovetrack: (ev: any) => any; +} + +declare class VideoTrack { + id: string; + kind: string; + label: string; + language: string; + selected: boolean; +} + +declare class VideoTrackList extends EventTarget { + length: number; + [index: number]: VideoTrack; + getTrackById(id: string): ?VideoTrack; + selectedIndex: number; + + onchange: (ev: any) => any; + onaddtrack: (ev: any) => any; + onremovetrack: (ev: any) => any; +} + +declare class TextTrackCue extends EventTarget { + constructor(startTime: number, endTime: number, text: string): void; + + track: TextTrack; + id: string; + startTime: number; + endTime: number; + pauseOnExit: boolean; + vertical: string; + snapToLines: boolean; + lines: number; + position: number; + size: number; + align: string; + text: string; + + getCueAsHTML(): Node; + onenter: (ev: any) => any; + onexit: (ev: any) => any; +} + +declare class TextTrackCueList { + @@iterator(): Iterator; + length: number; + [index: number]: TextTrackCue; + getCueById(id: string): ?TextTrackCue; +} + +declare class TextTrack extends EventTarget { + kind: string; + label: string; + language: string; + + mode: string; + + cues: TextTrackCueList; + activeCues: TextTrackCueList; + + addCue(cue: TextTrackCue): void; + removeCue(cue: TextTrackCue): void; + + oncuechange: (ev: any) => any; +} + +declare class TextTrackList extends EventTarget { + length: number; + [index: number]: TextTrack; + + onaddtrack: (ev: any) => any; + onremovetrack: (ev: any) => any; +} + +declare class HTMLMediaElement extends HTMLElement { + // error state + error: ?MediaError; + + // network state + src: string; + srcObject: ?any; + currentSrc: string; + crossOrigin: ?string; + NETWORK_EMPTY: number; + NETWORK_IDLE: number; + NETWORK_LOADING: number; + NETWORK_NO_SOURCE: number; + networkState: number; + preload: string; + buffered: TimeRanges; + load(): void; + canPlayType(type: string): string; + + // ready state + HAVE_NOTHING: number; + HAVE_METADATA: number; + HAVE_CURRENT_DATA: number; + HAVE_FUTURE_DATA: number; + HAVE_ENOUGH_DATA: number; + readyState: number; + seeking: boolean; + + // playback state + currentTime: number; + duration: number; + startDate: Date; + paused: boolean; + defaultPlaybackRate: number; + playbackRate: number; + played: TimeRanges; + seekable: TimeRanges; + ended: boolean; + autoplay: boolean; + loop: boolean; + play(): Promise; + pause(): void; + fastSeek(): void; + captureStream(): MediaStream; + + // media controller + mediaGroup: string; + controller: ?any; + + // controls + controls: boolean; + volume: number; + muted: boolean; + defaultMuted: boolean; + controlsList?: DOMTokenList; + + // tracks + audioTracks: AudioTrackList; + videoTracks: VideoTrackList; + textTracks: TextTrackList; + addTextTrack(kind: string, label?: string, language?: string): TextTrack; + + // media keys + mediaKeys?: ?MediaKeys; + setMediakeys?: (mediakeys: ?MediaKeys) => Promise; +} + +declare class HTMLAudioElement extends HTMLMediaElement { + tagName: 'AUDIO'; +} + +declare class HTMLVideoElement extends HTMLMediaElement { + tagName: 'VIDEO'; + width: number; + height: number; + videoWidth: number; + videoHeight: number; + poster: string; +} + +declare class HTMLSourceElement extends HTMLElement { + tagName: 'SOURCE'; + src: string; + type: string; + + //when used with the picture element + srcset: string; + sizes: string; + media: string; +} + +declare class ValidityState { + badInput: boolean; + customError: boolean; + patternMismatch: boolean; + rangeOverflow: boolean; + rangeUnderflow: boolean; + stepMismatch: boolean; + tooLong: boolean; + tooShort: boolean; + typeMismatch: boolean; + valueMissing: boolean; + valid: boolean; +} + +// https://w3c.github.io/html/sec-forms.html#dom-selectionapielements-setselectionrange +type SelectionDirection = 'backward' | 'forward' | 'none'; +type SelectionMode = 'select' | 'start' | 'end' | 'preserve'; +declare class HTMLInputElement extends HTMLElement { + tagName: 'INPUT'; + accept: string; + align: string; + alt: string; + autocomplete: string; + autofocus: boolean; + border: string; + checked: boolean; + complete: boolean; + defaultChecked: boolean; + defaultValue: string; + dirname: string; + disabled: boolean; + dynsrc: string; + files: FileList; + form: HTMLFormElement | null; + formAction: string; + formEncType: string; + formMethod: string; + formNoValidate: boolean; + formTarget: string; + height: string; + hspace: number; + indeterminate: boolean; + labels: NodeList; + list: HTMLElement | null; + loop: number; + lowsrc: string; + max: string; + maxLength: number; + min: string; + multiple: boolean; + name: string; + pattern: string; + placeholder: string; + readOnly: boolean; + required: boolean; + selectionDirection: SelectionDirection; + selectionEnd: number; + selectionStart: number; + size: number; + src: string; + start: string; + status: boolean; + step: string; + type: string; + useMap: string; + validationMessage: string; + validity: ValidityState; + value: string; + valueAsDate: Date; + valueAsNumber: number; + vrml: string; + vspace: number; + width: string; + willValidate: boolean; + popoverTargetElement: Element | null; + popoverTargetAction: 'toggle' | 'show' | 'hide'; + + checkValidity(): boolean; + reportValidity(): boolean; + setCustomValidity(error: string): void; + createTextRange(): TextRange; + select(): void; + setRangeText( + replacement: string, + start?: void, + end?: void, + selectMode?: void + ): void; + setRangeText( + replacement: string, + start: number, + end: number, + selectMode?: SelectionMode + ): void; + setSelectionRange( + start: number, + end: number, + direction?: SelectionDirection + ): void; + showPicker(): void; + stepDown(stepDecrement?: number): void; + stepUp(stepIncrement?: number): void; +} + +declare class HTMLButtonElement extends HTMLElement { + tagName: 'BUTTON'; + autofocus: boolean; + disabled: boolean; + form: HTMLFormElement | null; + labels: NodeList | null; + name: string; + type: string; + validationMessage: string; + validity: ValidityState; + value: string; + willValidate: boolean; + + checkValidity(): boolean; + reportValidity(): boolean; + setCustomValidity(error: string): void; + popoverTargetElement: Element | null; + popoverTargetAction: 'toggle' | 'show' | 'hide'; +} + +// https://w3c.github.io/html/sec-forms.html#the-textarea-element +declare class HTMLTextAreaElement extends HTMLElement { + tagName: 'TEXTAREA'; + autofocus: boolean; + cols: number; + dirName: string; + disabled: boolean; + form: HTMLFormElement | null; + maxLength: number; + name: string; + placeholder: string; + readOnly: boolean; + required: boolean; + rows: number; + wrap: string; + + type: string; + defaultValue: string; + value: string; + textLength: number; + + willValidate: boolean; + validity: ValidityState; + validationMessage: string; + checkValidity(): boolean; + setCustomValidity(error: string): void; + + labels: NodeList; + + select(): void; + selectionStart: number; + selectionEnd: number; + selectionDirection: SelectionDirection; + setSelectionRange( + start: number, + end: number, + direction?: SelectionDirection + ): void; +} + +declare class HTMLSelectElement extends HTMLElement { + tagName: 'SELECT'; + autocomplete: string; + autofocus: boolean; + disabled: boolean; + form: HTMLFormElement | null; + labels: NodeList; + length: number; + multiple: boolean; + name: string; + options: HTMLOptionsCollection; + required: boolean; + selectedIndex: number; + selectedOptions: HTMLCollection; + size: number; + type: string; + validationMessage: string; + validity: ValidityState; + value: string; + willValidate: boolean; + + add(element: HTMLElement, before?: HTMLElement): void; + checkValidity(): boolean; + item(index: number): HTMLOptionElement | null; + namedItem(name: string): HTMLOptionElement | null; + remove(index?: number): void; + setCustomValidity(error: string): void; +} + +declare class HTMLOptionsCollection extends HTMLCollection { + selectedIndex: number; + add( + element: HTMLOptionElement | HTMLOptGroupElement, + before?: HTMLElement | number + ): void; + remove(index: number): void; +} + +declare class HTMLOptionElement extends HTMLElement { + tagName: 'OPTION'; + defaultSelected: boolean; + disabled: boolean; + form: HTMLFormElement | null; + index: number; + label: string; + selected: boolean; + text: string; + value: string; +} + +declare class HTMLOptGroupElement extends HTMLElement { + tagName: 'OPTGROUP'; + disabled: boolean; + label: string; +} + +declare class HTMLAnchorElement extends HTMLElement { + tagName: 'A'; + charset: string; + coords: string; + download: string; + hash: string; + host: string; + hostname: string; + href: string; + hreflang: string; + media: string; + name: string; + origin: string; + password: string; + pathname: string; + port: string; + protocol: string; + rel: string; + rev: string; + search: string; + shape: string; + target: string; + text: string; + type: string; + username: string; +} + +// https://w3c.github.io/html/sec-forms.html#the-label-element +declare class HTMLLabelElement extends HTMLElement { + tagName: 'LABEL'; + form: HTMLFormElement | null; + htmlFor: string; + control: HTMLElement | null; +} + +declare class HTMLLinkElement extends HTMLElement { + tagName: 'LINK'; + crossOrigin: ?('anonymous' | 'use-credentials'); + href: string; + hreflang: string; + media: string; + rel: string; + sizes: DOMTokenList; + type: string; + as: string; +} + +declare class HTMLScriptElement extends HTMLElement { + tagName: 'SCRIPT'; + async: boolean; + charset: string; + crossOrigin?: string; + defer: boolean; + // flowlint unsafe-getters-setters:off + get src(): string; + set src(value: string | TrustedScriptURL): void; + get text(): string; + set text(value: string | TrustedScript): void; + // flowlint unsafe-getters-setters:error + type: string; +} + +declare class HTMLStyleElement extends HTMLElement { + tagName: 'STYLE'; + disabled: boolean; + media: string; + scoped: boolean; + sheet: ?CSSStyleSheet; + type: string; +} + +declare class HTMLParagraphElement extends HTMLElement { + tagName: 'P'; + align: 'left' | 'center' | 'right' | 'justify'; // deprecated in HTML 4.01 +} + +declare class HTMLHtmlElement extends HTMLElement { + tagName: 'HTML'; +} + +declare class HTMLBodyElement extends HTMLElement { + tagName: 'BODY'; +} + +declare class HTMLHeadElement extends HTMLElement { + tagName: 'HEAD'; +} + +declare class HTMLDivElement extends HTMLElement { + tagName: 'DIV'; +} + +declare class HTMLSpanElement extends HTMLElement { + tagName: 'SPAN'; +} + +declare class HTMLAppletElement extends HTMLElement {} + +declare class HTMLHeadingElement extends HTMLElement { + tagName: 'H1' | 'H2' | 'H3' | 'H4' | 'H5' | 'H6'; +} + +declare class HTMLHRElement extends HTMLElement { + tagName: 'HR'; +} + +declare class HTMLBRElement extends HTMLElement { + tagName: 'BR'; +} + +declare class HTMLDListElement extends HTMLElement { + tagName: 'DL'; +} + +declare class HTMLAreaElement extends HTMLElement { + tagName: 'AREA'; + alt: string; + coords: string; + shape: string; + target: string; + download: string; + ping: string; + rel: string; + relList: DOMTokenList; + referrerPolicy: string; +} + +declare class HTMLDataElement extends HTMLElement { + tagName: 'DATA'; + value: string; +} + +declare class HTMLDataListElement extends HTMLElement { + tagName: 'DATALIST'; + options: HTMLCollection; +} + +declare class HTMLDialogElement extends HTMLElement { + tagName: 'DIALOG'; + open: boolean; + returnValue: string; + show(): void; + showModal(): void; + close(returnValue: ?string): void; +} + +declare class HTMLEmbedElement extends HTMLElement { + tagName: 'EMBED'; + src: string; + type: string; + width: string; + height: string; + getSVGDocument(): ?Document; +} + +declare class HTMLMapElement extends HTMLElement { + tagName: 'MAP'; + areas: HTMLCollection; + images: HTMLCollection; + name: string; +} + +declare class HTMLMeterElement extends HTMLElement { + tagName: 'METER'; + high: number; + low: number; + max: number; + min: number; + optimum: number; + value: number; + labels: NodeList; +} + +declare class HTMLModElement extends HTMLElement { + tagName: 'DEL' | 'INS'; + cite: string; + dateTime: string; +} + +declare class HTMLObjectElement extends HTMLElement { + tagName: 'OBJECT'; + contentDocument: ?Document; + contentWindow: ?WindowProxy; + data: string; + form: ?HTMLFormElement; + height: string; + name: string; + type: string; + typeMustMatch: boolean; + useMap: string; + validationMessage: string; + validity: ValidityState; + width: string; + willValidate: boolean; + checkValidity(): boolean; + getSVGDocument(): ?Document; + reportValidity(): boolean; + setCustomValidity(error: string): void; +} + +declare class HTMLOutputElement extends HTMLElement { + defaultValue: string; + form: ?HTMLFormElement; + htmlFor: DOMTokenList; + labels: NodeList; + name: string; + type: string; + validationMessage: string; + validity: ValidityState; + value: string; + willValidate: boolean; + checkValidity(): boolean; + reportValidity(): boolean; + setCustomValidity(error: string): void; +} + +declare class HTMLParamElement extends HTMLElement { + tagName: 'PARAM'; + name: string; + value: string; +} + +declare class HTMLProgressElement extends HTMLElement { + tagName: 'PROGRESS'; + labels: NodeList; + max: number; + position: number; + value: number; +} + +declare class HTMLPictureElement extends HTMLElement { + tagName: 'PICTURE'; +} + +declare class HTMLTimeElement extends HTMLElement { + tagName: 'TIME'; + dateTime: string; +} + +declare class HTMLTitleElement extends HTMLElement { + tagName: 'TITLE'; + text: string; +} + +declare class HTMLTrackElement extends HTMLElement { + tagName: 'TRACK'; + static NONE: 0; + static LOADING: 1; + static LOADED: 2; + static ERROR: 3; + + default: boolean; + kind: string; + label: string; + readyState: 0 | 1 | 2 | 3; + src: string; + srclang: string; + track: TextTrack; +} + +declare class HTMLQuoteElement extends HTMLElement { + tagName: 'BLOCKQUOTE' | 'Q'; + cite: string; +} + +declare class HTMLOListElement extends HTMLElement { + tagName: 'OL'; + reversed: boolean; + start: number; + type: string; +} + +declare class HTMLUListElement extends HTMLElement { + tagName: 'UL'; +} + +declare class HTMLLIElement extends HTMLElement { + tagName: 'LI'; + value: number; +} + +declare class HTMLPreElement extends HTMLElement { + tagName: 'PRE'; +} + +declare class HTMLMetaElement extends HTMLElement { + tagName: 'META'; + content: string; + httpEquiv: string; + name: string; +} + +declare class HTMLUnknownElement extends HTMLElement {} + +declare class Storage { + length: number; + getItem(key: string): ?string; + setItem(key: string, data: string): void; + clear(): void; + removeItem(key: string): void; + key(index: number): ?string; + [name: string]: ?string; +} + +/* window */ + +declare type WindowProxy = any; +declare function alert(message?: any): void; +declare function prompt(message?: any, value?: any): string; +declare function close(): void; +declare function confirm(message?: string): boolean; +declare function getComputedStyle( + elt: Element, + pseudoElt?: string +): CSSStyleDeclaration; +declare opaque type AnimationFrameID; +declare function requestAnimationFrame( + callback: (timestamp: number) => void +): AnimationFrameID; +declare function cancelAnimationFrame(requestId: AnimationFrameID): void; +declare opaque type IdleCallbackID; +declare function requestIdleCallback( + cb: (deadline: { + didTimeout: boolean, + timeRemaining: () => number, + ... + }) => void, + opts?: {timeout: number, ...} +): IdleCallbackID; +declare function cancelIdleCallback(id: IdleCallbackID): void; +declare var localStorage: Storage; +declare var devicePixelRatio: number; +declare function focus(): void; +declare function onfocus(ev: Event): any; +declare function open( + url?: string, + target?: string, + features?: string, + replace?: boolean +): any; +declare var parent: WindowProxy; +declare function print(): void; +declare var self: any; +declare var sessionStorage: Storage; +declare var top: WindowProxy; +declare function getSelection(): Selection | null; +declare var customElements: CustomElementRegistry; +declare function scroll(x: number, y: number): void; +declare function scroll(options: ScrollToOptions): void; +declare function scrollTo(x: number, y: number): void; +declare function scrollTo(options: ScrollToOptions): void; +declare function scrollBy(x: number, y: number): void; +declare function scrollBy(options: ScrollToOptions): void; + +type HTMLElementTagNameMap = { + a: HTMLAnchorElement, + abbr: HTMLElement, + address: HTMLElement, + area: HTMLAreaElement, + article: HTMLElement, + aside: HTMLElement, + audio: HTMLAudioElement, + b: HTMLElement, + base: HTMLBaseElement, + bdi: HTMLElement, + bdo: HTMLElement, + blockquote: HTMLQuoteElement, + body: HTMLBodyElement, + br: HTMLBRElement, + button: HTMLButtonElement, + canvas: HTMLCanvasElement, + caption: HTMLTableCaptionElement, + cite: HTMLElement, + code: HTMLElement, + col: HTMLTableColElement, + colgroup: HTMLTableColElement, + data: HTMLDataElement, + datalist: HTMLDataListElement, + dd: HTMLElement, + del: HTMLModElement, + details: HTMLDetailsElement, + dfn: HTMLElement, + dialog: HTMLDialogElement, + div: HTMLDivElement, + dl: HTMLDListElement, + dt: HTMLElement, + em: HTMLElement, + embed: HTMLEmbedElement, + fieldset: HTMLFieldSetElement, + figcaption: HTMLElement, + figure: HTMLElement, + footer: HTMLElement, + form: HTMLFormElement, + h1: HTMLHeadingElement, + h2: HTMLHeadingElement, + h3: HTMLHeadingElement, + h4: HTMLHeadingElement, + h5: HTMLHeadingElement, + h6: HTMLHeadingElement, + head: HTMLHeadElement, + header: HTMLElement, + hgroup: HTMLElement, + hr: HTMLHRElement, + html: HTMLHtmlElement, + i: HTMLElement, + iframe: HTMLIFrameElement, + img: HTMLImageElement, + input: HTMLInputElement, + ins: HTMLModElement, + kbd: HTMLElement, + label: HTMLLabelElement, + legend: HTMLLegendElement, + li: HTMLLIElement, + link: HTMLLinkElement, + main: HTMLElement, + map: HTMLMapElement, + mark: HTMLElement, + menu: HTMLMenuElement, + meta: HTMLMetaElement, + meter: HTMLMeterElement, + nav: HTMLElement, + noscript: HTMLElement, + object: HTMLObjectElement, + ol: HTMLOListElement, + optgroup: HTMLOptGroupElement, + option: HTMLOptionElement, + output: HTMLOutputElement, + p: HTMLParagraphElement, + picture: HTMLPictureElement, + pre: HTMLPreElement, + progress: HTMLProgressElement, + q: HTMLQuoteElement, + rp: HTMLElement, + rt: HTMLElement, + ruby: HTMLElement, + s: HTMLElement, + samp: HTMLElement, + script: HTMLScriptElement, + search: HTMLElement, + section: HTMLElement, + select: HTMLSelectElement, + slot: HTMLSlotElement, + small: HTMLElement, + source: HTMLSourceElement, + span: HTMLSpanElement, + strong: HTMLElement, + style: HTMLStyleElement, + sub: HTMLElement, + summary: HTMLElement, + sup: HTMLElement, + table: HTMLTableElement, + tbody: HTMLTableSectionElement, + td: HTMLTableCellElement, + template: HTMLTemplateElement, + textarea: HTMLTextAreaElement, + tfoot: HTMLTableSectionElement, + th: HTMLTableCellElement, + thead: HTMLTableSectionElement, + time: HTMLTimeElement, + title: HTMLTitleElement, + tr: HTMLTableRowElement, + track: HTMLTrackElement, + u: HTMLElement, + ul: HTMLUListElement, + var: HTMLElement, + video: HTMLVideoElement, + wbr: HTMLElement, + [string]: Element, +}; diff --git a/flow-typed/environments/node.js b/flow-typed/environments/node.js new file mode 100644 index 0000000000000..a3edff20f893b --- /dev/null +++ b/flow-typed/environments/node.js @@ -0,0 +1,4286 @@ +// flow-typed signature: 44d8f5b0b708cdf7288ec50b7c08e1bf +// flow-typed version: 832153ff79/node/flow_>=v0.261.x + +interface ErrnoError extends Error { + address?: string; + code?: string; + dest?: string; + errno?: string | number; + info?: Object; + path?: string; + port?: number; + syscall?: string; +} + +type Node$Conditional = T extends true + ? IfTrue + : T extends false + ? IfFalse + : IfTrue | IfFalse; + +type buffer$NonBufferEncoding = + | 'hex' + | 'HEX' + | 'utf8' + | 'UTF8' + | 'utf-8' + | 'UTF-8' + | 'ascii' + | 'ASCII' + | 'binary' + | 'BINARY' + | 'base64' + | 'BASE64' + | 'ucs2' + | 'UCS2' + | 'ucs-2' + | 'UCS-2' + | 'utf16le' + | 'UTF16LE' + | 'utf-16le' + | 'UTF-16LE' + | 'latin1'; +type buffer$Encoding = buffer$NonBufferEncoding | 'buffer'; +type buffer$ToJSONRet = { + type: string, + data: Array, + ... +}; + +declare class Buffer extends Uint8Array { + constructor( + value: Array | number | string | Buffer | ArrayBuffer, + encoding?: buffer$Encoding + ): void; + [i: number]: number; + length: number; + + compare(otherBuffer: Buffer): number; + copy( + targetBuffer: Buffer, + targetStart?: number, + sourceStart?: number, + sourceEnd?: number + ): number; + entries(): Iterator<[number, number]>; + equals(otherBuffer: Buffer): boolean; + fill( + value: string | Buffer | number, + offset?: number, + end?: number, + encoding?: string + ): this; + fill(value: string, encoding?: string): this; + includes( + value: string | Buffer | number, + offsetOrEncoding?: number | buffer$Encoding, + encoding?: buffer$Encoding + ): boolean; + indexOf( + value: string | Buffer | number, + offsetOrEncoding?: number | buffer$Encoding, + encoding?: buffer$Encoding + ): number; + inspect(): string; + keys(): Iterator; + lastIndexOf( + value: string | Buffer | number, + offsetOrEncoding?: number | buffer$Encoding, + encoding?: buffer$Encoding + ): number; + readDoubleBE(offset?: number, noAssert?: boolean): number; + readDoubleLE(offset?: number, noAssert?: boolean): number; + readFloatBE(offset?: number, noAssert?: boolean): number; + readFloatLE(offset?: number, noAssert?: boolean): number; + readInt16BE(offset?: number, noAssert?: boolean): number; + readInt16LE(offset?: number, noAssert?: boolean): number; + readInt32BE(offset?: number, noAssert?: boolean): number; + readInt32LE(offset?: number, noAssert?: boolean): number; + readInt8(offset?: number, noAssert?: boolean): number; + readIntBE(offset: number, byteLength: number, noAssert?: boolean): number; + readIntLE(offset: number, byteLength: number, noAssert?: boolean): number; + readUInt16BE(offset?: number, noAssert?: boolean): number; + readUInt16LE(offset?: number, noAssert?: boolean): number; + readUInt32BE(offset?: number, noAssert?: boolean): number; + readUInt32LE(offset?: number, noAssert?: boolean): number; + readUInt8(offset?: number, noAssert?: boolean): number; + readUIntBE(offset: number, byteLength: number, noAssert?: boolean): number; + readUIntLE(offset: number, byteLength: number, noAssert?: boolean): number; + slice(start?: number, end?: number): this; + swap16(): Buffer; + swap32(): Buffer; + swap64(): Buffer; + toJSON(): buffer$ToJSONRet; + toString(encoding?: buffer$Encoding, start?: number, end?: number): string; + values(): Iterator; + write( + string: string, + offset?: number, + length?: number, + encoding?: buffer$Encoding + ): number; + writeDoubleBE(value: number, offset?: number, noAssert?: boolean): number; + writeDoubleLE(value: number, offset?: number, noAssert?: boolean): number; + writeFloatBE(value: number, offset?: number, noAssert?: boolean): number; + writeFloatLE(value: number, offset?: number, noAssert?: boolean): number; + writeInt16BE(value: number, offset?: number, noAssert?: boolean): number; + writeInt16LE(value: number, offset?: number, noAssert?: boolean): number; + writeInt32BE(value: number, offset?: number, noAssert?: boolean): number; + writeInt32LE(value: number, offset?: number, noAssert?: boolean): number; + writeInt8(value: number, offset?: number, noAssert?: boolean): number; + writeIntBE( + value: number, + offset: number, + byteLength: number, + noAssert?: boolean + ): number; + writeIntLE( + value: number, + offset: number, + byteLength: number, + noAssert?: boolean + ): number; + writeUInt16BE(value: number, offset?: number, noAssert?: boolean): number; + writeUInt16LE(value: number, offset?: number, noAssert?: boolean): number; + writeUInt32BE(value: number, offset?: number, noAssert?: boolean): number; + writeUInt32LE(value: number, offset?: number, noAssert?: boolean): number; + writeUInt8(value: number, offset?: number, noAssert?: boolean): number; + writeUIntBE( + value: number, + offset: number, + byteLength: number, + noAssert?: boolean + ): number; + writeUIntLE( + value: number, + offset: number, + byteLength: number, + noAssert?: boolean + ): number; + + static alloc( + size: number, + fill?: string | number, + encoding?: buffer$Encoding + ): Buffer; + static allocUnsafe(size: number): Buffer; + static allocUnsafeSlow(size: number): Buffer; + static byteLength( + string: string | Buffer | $TypedArray | DataView | ArrayBuffer, + encoding?: buffer$Encoding + ): number; + static compare(buf1: Buffer, buf2: Buffer): number; + static concat(list: Array, totalLength?: number): Buffer; + + static from(value: Buffer): Buffer; + static from(value: string, encoding?: buffer$Encoding): Buffer; + static from( + value: ArrayBuffer | SharedArrayBuffer, + byteOffset?: number, + length?: number + ): Buffer; + static from(value: Iterable): this; + static isBuffer(obj: any): boolean; + static isEncoding(encoding: string): boolean; +} + +declare type Node$Buffer = typeof Buffer; + +declare module 'buffer' { + declare var kMaxLength: number; + declare var INSPECT_MAX_BYTES: number; + declare function transcode( + source: Node$Buffer, + fromEnc: buffer$Encoding, + toEnc: buffer$Encoding + ): Node$Buffer; + declare var Buffer: Node$Buffer; +} + +type child_process$execOpts = { + cwd?: string, + env?: Object, + encoding?: string, + shell?: string, + timeout?: number, + maxBuffer?: number, + killSignal?: string | number, + uid?: number, + gid?: number, + windowsHide?: boolean, + ... +}; + +declare class child_process$Error extends Error { + code: number | string | null; + errno?: string; + syscall?: string; + path?: string; + spawnargs?: Array; + killed?: boolean; + signal?: string | null; + cmd: string; +} + +type child_process$execCallback = ( + error: ?child_process$Error, + stdout: string | Buffer, + stderr: string | Buffer +) => void; + +type child_process$execSyncOpts = { + cwd?: string, + input?: string | Buffer | $TypedArray | DataView, + stdio?: string | Array, + env?: Object, + shell?: string, + uid?: number, + gid?: number, + timeout?: number, + killSignal?: string | number, + maxBuffer?: number, + encoding?: string, + windowsHide?: boolean, + ... +}; + +type child_process$execFileOpts = { + cwd?: string, + env?: Object, + encoding?: string, + timeout?: number, + maxBuffer?: number, + killSignal?: string | number, + uid?: number, + gid?: number, + windowsHide?: boolean, + windowsVerbatimArguments?: boolean, + shell?: boolean | string, + ... +}; + +type child_process$execFileCallback = ( + error: ?child_process$Error, + stdout: string | Buffer, + stderr: string | Buffer +) => void; + +type child_process$execFileSyncOpts = { + cwd?: string, + input?: string | Buffer | $TypedArray | DataView, + stdio?: string | Array, + env?: Object, + uid?: number, + gid?: number, + timeout?: number, + killSignal?: string | number, + maxBuffer?: number, + encoding?: string, + windowsHide?: boolean, + shell?: boolean | string, + ... +}; + +type child_process$forkOpts = { + cwd?: string, + env?: Object, + execPath?: string, + execArgv?: Array, + silent?: boolean, + stdio?: Array | string, + windowsVerbatimArguments?: boolean, + uid?: number, + gid?: number, + ... +}; + +type child_process$Handle = any; // TODO + +type child_process$spawnOpts = { + cwd?: string, + env?: Object, + argv0?: string, + stdio?: string | Array, + detached?: boolean, + uid?: number, + gid?: number, + shell?: boolean | string, + windowsVerbatimArguments?: boolean, + windowsHide?: boolean, + ... +}; + +type child_process$spawnRet = { + pid: number, + output: Array, + stdout: Buffer | string, + stderr: Buffer | string, + status: number, + signal: string, + error: Error, + ... +}; + +type child_process$spawnSyncOpts = { + cwd?: string, + input?: string | Buffer, + stdio?: string | Array, + env?: Object, + uid?: number, + gid?: number, + timeout?: number, + killSignal?: string, + maxBuffer?: number, + encoding?: string, + shell?: boolean | string, + ... +}; + +type child_process$spawnSyncRet = child_process$spawnRet; + +declare class child_process$ChildProcess extends events$EventEmitter { + channel: Object; + connected: boolean; + killed: boolean; + pid: number; + exitCode: number | null; + stderr: stream$Readable; + stdin: stream$Writable; + stdio: Array; + stdout: stream$Readable; + + disconnect(): void; + kill(signal?: string): void; + send( + message: Object, + sendHandleOrCallback?: child_process$Handle, + optionsOrCallback?: Object | Function, + callback?: Function + ): boolean; + unref(): void; + ref(): void; +} + +declare module 'child_process' { + declare var ChildProcess: typeof child_process$ChildProcess; + + declare function exec( + command: string, + optionsOrCallback?: child_process$execOpts | child_process$execCallback, + callback?: child_process$execCallback + ): child_process$ChildProcess; + + declare function execSync( + command: string, + options: { + encoding: buffer$NonBufferEncoding, + ... + } & child_process$execSyncOpts + ): string; + + declare function execSync( + command: string, + options?: child_process$execSyncOpts + ): Buffer; + + declare function execFile( + file: string, + argsOrOptionsOrCallback?: + | Array + | child_process$execFileOpts + | child_process$execFileCallback, + optionsOrCallback?: + | child_process$execFileOpts + | child_process$execFileCallback, + callback?: child_process$execFileCallback + ): child_process$ChildProcess; + + declare function execFileSync( + command: string, + argsOrOptions?: Array | child_process$execFileSyncOpts, + options?: child_process$execFileSyncOpts + ): Buffer | string; + + declare function fork( + modulePath: string, + argsOrOptions?: Array | child_process$forkOpts, + options?: child_process$forkOpts + ): child_process$ChildProcess; + + declare function spawn( + command: string, + argsOrOptions?: Array | child_process$spawnOpts, + options?: child_process$spawnOpts + ): child_process$ChildProcess; + + declare function spawnSync( + command: string, + argsOrOptions?: Array | child_process$spawnSyncOpts, + options?: child_process$spawnSyncOpts + ): child_process$spawnSyncRet; +} + +declare module 'cluster' { + declare type ClusterSettings = { + execArgv: Array, + exec: string, + args: Array, + cwd: string, + serialization: 'json' | 'advanced', + silent: boolean, + stdio: Array, + uid: number, + gid: number, + inspectPort: number | (() => number), + windowsHide: boolean, + ... + }; + + declare type ClusterSettingsOpt = { + execArgv?: Array, + exec?: string, + args?: Array, + cwd?: string, + serialization?: 'json' | 'advanced', + silent?: boolean, + stdio?: Array, + uid?: number, + gid?: number, + inspectPort?: number | (() => number), + windowsHide?: boolean, + ... + }; + + declare class Worker extends events$EventEmitter { + id: number; + process: child_process$ChildProcess; + suicide: boolean; + + disconnect(): void; + isConnected(): boolean; + isDead(): boolean; + kill(signal?: string): void; + send( + message: Object, + sendHandleOrCallback?: child_process$Handle | Function, + callback?: Function + ): boolean; + } + + declare class Cluster extends events$EventEmitter { + isMaster: boolean; + isWorker: boolean; + settings: ClusterSettings; + worker: Worker; + workers: {[id: number]: Worker}; + + disconnect(callback?: () => void): void; + fork(env?: Object): Worker; + setupMaster(settings?: ClusterSettingsOpt): void; + } + + declare module.exports: Cluster; +} + +type crypto$createCredentialsDetails = any; // TODO + +declare class crypto$Cipher extends stream$Duplex { + final(output_encoding: 'latin1' | 'binary' | 'base64' | 'hex'): string; + final(output_encoding: void): Buffer; + getAuthTag(): Buffer; + setAAD(buffer: Buffer): crypto$Cipher; + setAuthTag(buffer: Buffer): void; + setAutoPadding(auto_padding?: boolean): crypto$Cipher; + update( + data: string, + input_encoding: 'utf8' | 'ascii' | 'latin1' | 'binary', + output_encoding: 'latin1' | 'binary' | 'base64' | 'hex' + ): string; + update( + data: string, + input_encoding: 'utf8' | 'ascii' | 'latin1' | 'binary', + output_encoding: void + ): Buffer; + update( + data: Buffer, + input_encoding: void | 'utf8' | 'ascii' | 'latin1' | 'binary', + output_encoding: 'latin1' | 'binary' | 'base64' | 'hex' + ): string; + update(data: Buffer, input_encoding: void, output_encoding: void): Buffer; +} + +type crypto$Credentials = {...}; + +type crypto$DiffieHellman = { + computeSecret( + other_public_key: string, + input_encoding?: string, + output_encoding?: string + ): any, + generateKeys(encoding?: string): any, + getGenerator(encoding?: string): any, + getPrime(encoding?: string): any, + getPrivateKey(encoding?: string): any, + getPublicKey(encoding?: string): any, + setPrivateKey(private_key: any, encoding?: string): void, + setPublicKey(public_key: any, encoding?: string): void, + ... +}; + +type crypto$ECDH$Encoding = 'latin1' | 'hex' | 'base64'; +type crypto$ECDH$Format = 'compressed' | 'uncompressed'; + +declare class crypto$ECDH { + computeSecret(other_public_key: Buffer | $TypedArray | DataView): Buffer; + computeSecret( + other_public_key: string, + input_encoding: crypto$ECDH$Encoding + ): Buffer; + computeSecret( + other_public_key: Buffer | $TypedArray | DataView, + output_encoding: crypto$ECDH$Encoding + ): string; + computeSecret( + other_public_key: string, + input_encoding: crypto$ECDH$Encoding, + output_encoding: crypto$ECDH$Encoding + ): string; + generateKeys(format?: crypto$ECDH$Format): Buffer; + generateKeys( + encoding: crypto$ECDH$Encoding, + format?: crypto$ECDH$Format + ): string; + getPrivateKey(): Buffer; + getPrivateKey(encoding: crypto$ECDH$Encoding): string; + getPublicKey(format?: crypto$ECDH$Format): Buffer; + getPublicKey( + encoding: crypto$ECDH$Encoding, + format?: crypto$ECDH$Format + ): string; + setPrivateKey(private_key: Buffer | $TypedArray | DataView): void; + setPrivateKey(private_key: string, encoding: crypto$ECDH$Encoding): void; +} + +declare class crypto$Decipher extends stream$Duplex { + final(output_encoding: 'latin1' | 'binary' | 'ascii' | 'utf8'): string; + final(output_encoding: void): Buffer; + getAuthTag(): Buffer; + setAAD(buffer: Buffer): void; + setAuthTag(buffer: Buffer): void; + setAutoPadding(auto_padding?: boolean): crypto$Cipher; + update( + data: string, + input_encoding: 'latin1' | 'binary' | 'base64' | 'hex', + output_encoding: 'latin1' | 'binary' | 'ascii' | 'utf8' + ): string; + update( + data: string, + input_encoding: 'latin1' | 'binary' | 'base64' | 'hex', + output_encoding: void + ): Buffer; + update( + data: Buffer, + input_encoding: void, + output_encoding: 'latin1' | 'binary' | 'ascii' | 'utf8' + ): string; + update(data: Buffer, input_encoding: void, output_encoding: void): Buffer; +} + +declare class crypto$Hash extends stream$Duplex { + digest(encoding: 'hex' | 'latin1' | 'binary' | 'base64'): string; + digest(encoding: 'buffer'): Buffer; + digest(encoding: void): Buffer; + update( + data: string | Buffer, + input_encoding?: 'utf8' | 'ascii' | 'latin1' | 'binary' + ): crypto$Hash; +} + +declare class crypto$Hmac extends stream$Duplex { + digest(encoding: 'hex' | 'latin1' | 'binary' | 'base64'): string; + digest(encoding: 'buffer'): Buffer; + digest(encoding: void): Buffer; + update( + data: string | Buffer, + input_encoding?: 'utf8' | 'ascii' | 'latin1' | 'binary' + ): crypto$Hmac; +} + +type crypto$Sign$private_key = + | string + | { + key: string, + passphrase: string, + ... + }; +declare class crypto$Sign extends stream$Writable { + static (algorithm: string, options?: writableStreamOptions): crypto$Sign; + constructor(algorithm: string, options?: writableStreamOptions): void; + sign( + private_key: crypto$Sign$private_key, + output_format: 'latin1' | 'binary' | 'hex' | 'base64' + ): string; + sign(private_key: crypto$Sign$private_key, output_format: void): Buffer; + update( + data: string | Buffer, + input_encoding?: 'utf8' | 'ascii' | 'latin1' | 'binary' + ): crypto$Sign; +} + +declare class crypto$Verify extends stream$Writable { + static (algorithm: string, options?: writableStreamOptions): crypto$Verify; + constructor(algorithm: string, options?: writableStreamOptions): void; + update( + data: string | Buffer, + input_encoding?: 'utf8' | 'ascii' | 'latin1' | 'binary' + ): crypto$Verify; + verify( + object: string, + signature: string | Buffer | $TypedArray | DataView, + signature_format: 'latin1' | 'binary' | 'hex' | 'base64' + ): boolean; + verify(object: string, signature: Buffer, signature_format: void): boolean; +} + +type crypto$key = + | string + | { + key: string, + passphrase?: string, + // TODO: enum type in crypto.constants + padding?: string, + ... + }; + +declare module 'crypto' { + declare var DEFAULT_ENCODING: string; + + declare class Sign extends crypto$Sign {} + declare class Verify extends crypto$Verify {} + + declare function createCipher( + algorithm: string, + password: string | Buffer + ): crypto$Cipher; + declare function createCipheriv( + algorithm: string, + key: string | Buffer, + iv: string | Buffer + ): crypto$Cipher; + declare function createCredentials( + details?: crypto$createCredentialsDetails + ): crypto$Credentials; + declare function createDecipher( + algorithm: string, + password: string | Buffer + ): crypto$Decipher; + declare function createDecipheriv( + algorithm: string, + key: string | Buffer, + iv: string | Buffer + ): crypto$Decipher; + declare function createDiffieHellman( + prime_length: number + ): crypto$DiffieHellman; + declare function createDiffieHellman( + prime: number, + encoding?: string + ): crypto$DiffieHellman; + declare function createECDH(curveName: string): crypto$ECDH; + declare function createHash(algorithm: string): crypto$Hash; + declare function createHmac( + algorithm: string, + key: string | Buffer + ): crypto$Hmac; + declare function createSign(algorithm: string): crypto$Sign; + declare function createVerify(algorithm: string): crypto$Verify; + declare function getCiphers(): Array; + declare function getCurves(): Array; + declare function getDiffieHellman(group_name: string): crypto$DiffieHellman; + declare function getHashes(): Array; + declare function pbkdf2( + password: string | Buffer, + salt: string | Buffer, + iterations: number, + keylen: number, + digest: string, + callback: (err: ?Error, derivedKey: Buffer) => void + ): void; + declare function pbkdf2( + password: string | Buffer, + salt: string | Buffer, + iterations: number, + keylen: number, + callback: (err: ?Error, derivedKey: Buffer) => void + ): void; + declare function pbkdf2Sync( + password: string | Buffer, + salt: string | Buffer, + iterations: number, + keylen: number, + digest?: string + ): Buffer; + declare function scrypt( + password: string | Buffer, + salt: string | Buffer, + keylen: number, + options: + | {|N?: number, r?: number, p?: number, maxmem?: number|} + | {| + cost?: number, + blockSize?: number, + parallelization?: number, + maxmem?: number, + |}, + callback: (err: ?Error, derivedKey: Buffer) => void + ): void; + declare function scrypt( + password: string | Buffer, + salt: string | Buffer, + keylen: number, + callback: (err: ?Error, derivedKey: Buffer) => void + ): void; + declare function scryptSync( + password: string | Buffer, + salt: string | Buffer, + keylen: number, + options?: + | {|N?: number, r?: number, p?: number, maxmem?: number|} + | {| + cost?: number, + blockSize?: number, + parallelization?: number, + maxmem?: number, + |} + ): Buffer; + declare function privateDecrypt( + private_key: crypto$key, + buffer: Buffer + ): Buffer; + declare function privateEncrypt( + private_key: crypto$key, + buffer: Buffer + ): Buffer; + declare function publicDecrypt(key: crypto$key, buffer: Buffer): Buffer; + declare function publicEncrypt(key: crypto$key, buffer: Buffer): Buffer; + // `UNUSED` argument strictly enforces arity to enable overloading this + // function with 1-arg and 2-arg variants. + declare function pseudoRandomBytes(size: number, UNUSED: void): Buffer; + declare function pseudoRandomBytes( + size: number, + callback: (err: ?Error, buffer: Buffer) => void + ): void; + // `UNUSED` argument strictly enforces arity to enable overloading this + // function with 1-arg and 2-arg variants. + declare function randomBytes(size: number, UNUSED: void): Buffer; + declare function randomBytes( + size: number, + callback: (err: ?Error, buffer: Buffer) => void + ): void; + declare function randomFillSync( + buffer: Buffer | $TypedArray | DataView + ): void; + declare function randomFillSync( + buffer: Buffer | $TypedArray | DataView, + offset: number + ): void; + declare function randomFillSync( + buffer: Buffer | $TypedArray | DataView, + offset: number, + size: number + ): void; + declare function randomFill( + buffer: Buffer | $TypedArray | DataView, + callback: (err: ?Error, buffer: Buffer) => void + ): void; + declare function randomFill( + buffer: Buffer | $TypedArray | DataView, + offset: number, + callback: (err: ?Error, buffer: Buffer) => void + ): void; + declare function randomFill( + buffer: Buffer | $TypedArray | DataView, + offset: number, + size: number, + callback: (err: ?Error, buffer: Buffer) => void + ): void; + declare function randomUUID( + options?: $ReadOnly<{|disableEntropyCache?: boolean|}> + ): string; + declare function timingSafeEqual( + a: Buffer | $TypedArray | DataView, + b: Buffer | $TypedArray | DataView + ): boolean; +} + +type net$Socket$address = { + address: string, + family: string, + port: number, + ... +}; +type dgram$Socket$rinfo = { + address: string, + family: 'IPv4' | 'IPv6', + port: number, + size: number, + ... +}; + +declare class dgram$Socket extends events$EventEmitter { + addMembership(multicastAddress: string, multicastInterface?: string): void; + address(): net$Socket$address; + bind(port?: number, address?: string, callback?: () => void): void; + close(callback?: () => void): void; + dropMembership(multicastAddress: string, multicastInterface?: string): void; + ref(): void; + send( + msg: Buffer, + port: number, + address: string, + callback?: (err: ?Error, bytes: any) => mixed + ): void; + send( + msg: Buffer, + offset: number, + length: number, + port: number, + address: string, + callback?: (err: ?Error, bytes: any) => mixed + ): void; + setBroadcast(flag: boolean): void; + setMulticastLoopback(flag: boolean): void; + setMulticastTTL(ttl: number): void; + setTTL(ttl: number): void; + unref(): void; +} + +declare module 'dgram' { + declare function createSocket( + options: string | {type: string, ...}, + callback?: () => void + ): dgram$Socket; +} + +declare module 'dns' { + declare var ADDRGETNETWORKPARAMS: string; + declare var BADFAMILY: string; + declare var BADFLAGS: string; + declare var BADHINTS: string; + declare var BADQUERY: string; + declare var BADNAME: string; + declare var BADRESP: string; + declare var BADSTR: string; + declare var CANCELLED: string; + declare var CONNREFUSED: string; + declare var DESTRUCTION: string; + declare var EOF: string; + declare var FILE: string; + declare var FORMER: string; + declare var LOADIPHLPAPI: string; + declare var NODATA: string; + declare var NOMEM: string; + declare var NONAME: string; + declare var NOTFOUND: string; + declare var NOTIMP: string; + declare var NOTINITIALIZED: string; + declare var REFUSED: string; + declare var SERVFAIL: string; + declare var TIMEOUT: string; + declare var ADDRCONFIG: number; + declare var V4MAPPED: number; + + declare type LookupOptions = { + family?: number, + hints?: number, + verbatim?: boolean, + all?: boolean, + ... + }; + + declare function lookup( + domain: string, + options: number | LookupOptions, + callback: (err: ?Error, address: string, family: number) => void + ): void; + declare function lookup( + domain: string, + callback: (err: ?Error, address: string, family: number) => void + ): void; + + declare function resolve( + domain: string, + rrtype?: string, + callback?: (err: ?Error, addresses: Array) => void + ): void; + + declare function resolve4( + domain: string, + callback: (err: ?Error, addresses: Array) => void + ): void; + + declare function resolve6( + domain: string, + callback: (err: ?Error, addresses: Array) => void + ): void; + + declare function resolveCname( + domain: string, + callback: (err: ?Error, addresses: Array) => void + ): void; + + declare function resolveMx( + domain: string, + callback: (err: ?Error, addresses: Array) => void + ): void; + + declare function resolveNs( + domain: string, + callback: (err: ?Error, addresses: Array) => void + ): void; + + declare function resolveSrv( + domain: string, + callback: (err: ?Error, addresses: Array) => void + ): void; + + declare function resolveTxt( + domain: string, + callback: (err: ?Error, addresses: Array) => void + ): void; + + declare function reverse( + ip: string, + callback: (err: ?Error, domains: Array) => void + ): void; + declare function timingSafeEqual( + a: Buffer | $TypedArray | DataView, + b: Buffer | $TypedArray | DataView + ): boolean; +} + +declare class events$EventEmitter { + // deprecated + static listenerCount(emitter: events$EventEmitter, event: string): number; + static defaultMaxListeners: number; + + addListener(event: string, listener: Function): this; + emit(event: string, ...args: Array): boolean; + eventNames(): Array; + listeners(event: string): Array; + listenerCount(event: string): number; + on(event: string, listener: Function): this; + once(event: string, listener: Function): this; + prependListener(event: string, listener: Function): this; + prependOnceListener(event: string, listener: Function): this; + removeAllListeners(event?: string): this; + removeListener(event: string, listener: Function): this; + off(event: string, listener: Function): this; + setMaxListeners(n: number): this; + getMaxListeners(): number; + rawListeners(event: string): Array; +} + +declare module 'events' { + // TODO: See the comment above the events$EventEmitter declaration + declare class EventEmitter extends events$EventEmitter { + static EventEmitter: typeof EventEmitter; + } + + declare module.exports: typeof EventEmitter; +} + +declare class domain$Domain extends events$EventEmitter { + members: Array; + + add(emitter: events$EventEmitter): void; + bind(callback: Function): Function; + dispose(): void; + enter(): void; + exit(): void; + intercept(callback: Function): Function; + remove(emitter: events$EventEmitter): void; + run(fn: Function): void; +} + +declare module 'domain' { + declare function create(): domain$Domain; +} + +declare module 'fs' { + declare class Stats { + dev: number; + ino: number; + mode: number; + nlink: number; + uid: number; + gid: number; + rdev: number; + size: number; + blksize: number; + blocks: number; + atimeMs: number; + mtimeMs: number; + ctimeMs: number; + birthtimeMs: number; + atime: Date; + mtime: Date; + ctime: Date; + birthtime: Date; + + isFile(): boolean; + isDirectory(): boolean; + isBlockDevice(): boolean; + isCharacterDevice(): boolean; + isSymbolicLink(): boolean; + isFIFO(): boolean; + isSocket(): boolean; + } + + declare type PathLike = string | Buffer | URL; + + declare class FSWatcher extends events$EventEmitter { + close(): void; + } + + declare class ReadStream extends stream$Readable { + close(): void; + } + + declare class WriteStream extends stream$Writable { + close(): void; + bytesWritten: number; + } + + declare class Dirent { + name: string | Buffer; + + isBlockDevice(): boolean; + isCharacterDevice(): boolean; + isDirectory(): boolean; + isFIFO(): boolean; + isFile(): boolean; + isSocket(): boolean; + isSymbolicLink(): boolean; + } + + declare function rename( + oldPath: string, + newPath: string, + callback?: (err: ?ErrnoError) => void + ): void; + declare function renameSync(oldPath: string, newPath: string): void; + declare function ftruncate( + fd: number, + len: number, + callback?: (err: ?ErrnoError) => void + ): void; + declare function ftruncateSync(fd: number, len: number): void; + declare function truncate( + path: string, + len: number, + callback?: (err: ?ErrnoError) => void + ): void; + declare function truncateSync(path: string, len: number): void; + declare function chown( + path: string, + uid: number, + gid: number, + callback?: (err: ?ErrnoError) => void + ): void; + declare function chownSync(path: string, uid: number, gid: number): void; + declare function fchown( + fd: number, + uid: number, + gid: number, + callback?: (err: ?ErrnoError) => void + ): void; + declare function fchownSync(fd: number, uid: number, gid: number): void; + declare function lchown( + path: string, + uid: number, + gid: number, + callback?: (err: ?ErrnoError) => void + ): void; + declare function lchownSync(path: string, uid: number, gid: number): void; + declare function chmod( + path: string, + mode: number | string, + callback?: (err: ?ErrnoError) => void + ): void; + declare function chmodSync(path: string, mode: number | string): void; + declare function fchmod( + fd: number, + mode: number | string, + callback?: (err: ?ErrnoError) => void + ): void; + declare function fchmodSync(fd: number, mode: number | string): void; + declare function lchmod( + path: string, + mode: number | string, + callback?: (err: ?ErrnoError) => void + ): void; + declare function lchmodSync(path: string, mode: number | string): void; + declare function stat( + path: string, + callback?: (err: ?ErrnoError, stats: Stats) => any + ): void; + declare function statSync(path: string): Stats; + declare function fstat( + fd: number, + callback?: (err: ?ErrnoError, stats: Stats) => any + ): void; + declare function fstatSync(fd: number): Stats; + declare function lstat( + path: string, + callback?: (err: ?ErrnoError, stats: Stats) => any + ): void; + declare function lstatSync(path: string): Stats; + declare function link( + srcpath: string, + dstpath: string, + callback?: (err: ?ErrnoError) => void + ): void; + declare function linkSync(srcpath: string, dstpath: string): void; + declare function symlink( + srcpath: string, + dtspath: string, + type?: string, + callback?: (err: ?ErrnoError) => void + ): void; + declare function symlinkSync( + srcpath: string, + dstpath: string, + type?: string + ): void; + declare function readlink( + path: string, + callback: (err: ?ErrnoError, linkString: string) => void + ): void; + declare function readlinkSync(path: string): string; + declare function realpath( + path: string, + cache?: Object, + callback?: (err: ?ErrnoError, resolvedPath: string) => void + ): void; + declare function realpathSync(path: string, cache?: Object): string; + declare function unlink( + path: string, + callback?: (err: ?ErrnoError) => void + ): void; + declare function unlinkSync(path: string): void; + + declare type RmDirOptions = {| + /** + * If an `EBUSY`, `EMFILE`, `ENFILE`, `ENOTEMPTY`, or + * `EPERM` error is encountered, Node.js will retry the operation with a linear + * backoff wait of `retryDelay` ms longer on each try. This option represents the + * number of retries. This option is ignored if the `recursive` option is not + * `true`. + * @default 0 + */ + maxRetries?: number | void, + /** + * @deprecated since v14.14.0 In future versions of Node.js and will trigger a warning + * `fs.rmdir(path, { recursive: true })` will throw if `path` does not exist or is a file. + * Use `fs.rm(path, { recursive: true, force: true })` instead. + * + * If `true`, perform a recursive directory removal. In + * recursive mode soperations are retried on failure. + * @default false + */ + recursive?: boolean | void, + /** + * The amount of time in milliseconds to wait between retries. + * This option is ignored if the `recursive` option is not `true`. + * @default 100 + */ + retryDelay?: number | void, + |}; + + declare function rmdir( + path: PathLike, + callback?: (err: ?ErrnoError) => void + ): void; + declare function rmdir( + path: PathLike, + options: RmDirOptions, + callback?: (err: ?ErrnoError) => void + ): void; + declare function rmdirSync(path: PathLike, options?: RmDirOptions): void; + + declare type RmOptions = {| + /** + * When `true`, exceptions will be ignored if `path` does not exist. + * @default false + */ + force?: boolean | void, + /** + * If an `EBUSY`, `EMFILE`, `ENFILE`, `ENOTEMPTY`, or + * `EPERM` error is encountered, Node.js will retry the operation with a linear + * backoff wait of `retryDelay` ms longer on each try. This option represents the + * number of retries. This option is ignored if the `recursive` option is not + * `true`. + * @default 0 + */ + maxRetries?: number | void, + /** + * If `true`, perform a recursive directory removal. In + * recursive mode, operations are retried on failure. + * @default false + */ + recursive?: boolean | void, + /** + * The amount of time in milliseconds to wait between retries. + * This option is ignored if the `recursive` option is not `true`. + * @default 100 + */ + retryDelay?: number | void, + |}; + + /** + * Asynchronously removes files and directories (modeled on the standard POSIX `rm`utility). No arguments other than a possible exception are given to the + * completion callback. + * @since v14.14.0 + */ + declare function rm( + path: PathLike, + callback?: (err: ?ErrnoError) => void + ): void; + declare function rm( + path: PathLike, + options: RmOptions, + callback?: (err: ?ErrnoError) => void + ): void; + + /** + * Synchronously removes files and directories (modeled on the standard POSIX `rm`utility). Returns `undefined`. + * @since v14.14.0 + */ + declare function rmSync(path: PathLike, options?: RmOptions): void; + + declare function mkdir( + path: string, + mode?: + | number + | { + recursive?: boolean, + mode?: number, + ... + }, + callback?: (err: ?ErrnoError) => void + ): void; + declare function mkdirSync( + path: string, + mode?: + | number + | { + recursive?: boolean, + mode?: number, + ... + } + ): void; + declare function mkdtemp( + prefix: string, + callback: (err: ?ErrnoError, folderPath: string) => void + ): void; + declare function mkdtempSync(prefix: string): string; + declare function readdir( + path: string, + options: string | {encoding?: string, withFileTypes?: false, ...}, + callback: (err: ?ErrnoError, files: Array) => void + ): void; + declare function readdir( + path: string, + options: {encoding?: string, withFileTypes: true, ...}, + callback: (err: ?ErrnoError, files: Array) => void + ): void; + declare function readdir( + path: string, + callback: (err: ?ErrnoError, files: Array) => void + ): void; + declare function readdirSync( + path: string, + options?: string | {encoding?: string, withFileTypes?: false, ...} + ): Array; + declare function readdirSync( + path: string, + options?: string | {encoding?: string, withFileTypes: true, ...} + ): Array; + declare function close( + fd: number, + callback: (err: ?ErrnoError) => void + ): void; + declare function closeSync(fd: number): void; + declare function open( + path: string | Buffer | URL, + flags: string | number, + mode: number, + callback: (err: ?ErrnoError, fd: number) => void + ): void; + declare function open( + path: string | Buffer | URL, + flags: string | number, + callback: (err: ?ErrnoError, fd: number) => void + ): void; + declare function openSync( + path: string | Buffer, + flags: string | number, + mode?: number + ): number; + declare function utimes( + path: string, + atime: number, + mtime: number, + callback?: (err: ?ErrnoError) => void + ): void; + declare function utimesSync(path: string, atime: number, mtime: number): void; + declare function futimes( + fd: number, + atime: number, + mtime: number, + callback?: (err: ?ErrnoError) => void + ): void; + declare function futimesSync(fd: number, atime: number, mtime: number): void; + declare function fsync( + fd: number, + callback?: (err: ?ErrnoError) => void + ): void; + declare function fsyncSync(fd: number): void; + declare function write( + fd: number, + buffer: Buffer, + offset: number, + length: number, + position: number, + callback: (err: ?ErrnoError, write: number, buf: Buffer) => void + ): void; + declare function write( + fd: number, + buffer: Buffer, + offset: number, + length: number, + callback: (err: ?ErrnoError, write: number, buf: Buffer) => void + ): void; + declare function write( + fd: number, + buffer: Buffer, + offset: number, + callback: (err: ?ErrnoError, write: number, buf: Buffer) => void + ): void; + declare function write( + fd: number, + buffer: Buffer, + callback: (err: ?ErrnoError, write: number, buf: Buffer) => void + ): void; + declare function write( + fd: number, + data: string, + position: number, + encoding: string, + callback: (err: ?ErrnoError, write: number, str: string) => void + ): void; + declare function write( + fd: number, + data: string, + position: number, + callback: (err: ?ErrnoError, write: number, str: string) => void + ): void; + declare function write( + fd: number, + data: string, + callback: (err: ?ErrnoError, write: number, str: string) => void + ): void; + declare function writeSync( + fd: number, + buffer: Buffer, + offset: number, + length: number, + position: number + ): number; + declare function writeSync( + fd: number, + buffer: Buffer, + offset: number, + length: number + ): number; + declare function writeSync( + fd: number, + buffer: Buffer, + offset?: number + ): number; + declare function writeSync( + fd: number, + str: string, + position: number, + encoding: string + ): number; + declare function writeSync( + fd: number, + str: string, + position?: number + ): number; + declare function read( + fd: number, + buffer: Buffer, + offset: number, + length: number, + position: ?number, + callback: (err: ?ErrnoError, bytesRead: number, buffer: Buffer) => void + ): void; + declare function readSync( + fd: number, + buffer: Buffer, + offset: number, + length: number, + position: number + ): number; + declare function readFile( + path: string | Buffer | URL | number, + callback: (err: ?ErrnoError, data: Buffer) => void + ): void; + declare function readFile( + path: string | Buffer | URL | number, + encoding: string, + callback: (err: ?ErrnoError, data: string) => void + ): void; + declare function readFile( + path: string | Buffer | URL | number, + options: { + encoding: string, + flag?: string, + ... + }, + callback: (err: ?ErrnoError, data: string) => void + ): void; + declare function readFile( + path: string | Buffer | URL | number, + options: {encoding?: null | void, flag?: string, ...}, + callback: (err: ?ErrnoError, data: Buffer) => void + ): void; + declare function readFileSync(path: string | Buffer | URL | number): Buffer; + declare function readFileSync( + path: string | Buffer | URL | number, + encoding: string + ): string; + declare function readFileSync( + path: string | Buffer | URL | number, + options: { + encoding: string, + flag?: string, + ... + } + ): string; + declare function readFileSync( + path: string | Buffer | URL | number, + options: { + encoding?: void, + flag?: string, + ... + } + ): Buffer; + declare function writeFile( + filename: string | Buffer | number, + data: Buffer | string, + options: + | string + | { + encoding?: ?string, + mode?: number, + flag?: string, + ... + }, + callback: (err: ?ErrnoError) => void + ): void; + declare function writeFile( + filename: string | Buffer | number, + data: Buffer | string, + callback?: (err: ?ErrnoError) => void + ): void; + declare function writeFileSync( + filename: string, + data: Buffer | string, + options?: + | string + | { + encoding?: ?string, + mode?: number, + flag?: string, + ... + } + ): void; + declare function appendFile( + filename: string | Buffer | number, + data: string | Buffer, + options: + | string + | { + encoding?: ?string, + mode?: number, + flag?: string, + ... + }, + callback: (err: ?ErrnoError) => void + ): void; + declare function appendFile( + filename: string | Buffer | number, + data: string | Buffer, + callback: (err: ?ErrnoError) => void + ): void; + declare function appendFileSync( + filename: string | Buffer | number, + data: string | Buffer, + options?: + | string + | { + encoding?: ?string, + mode?: number, + flag?: string, + ... + } + ): void; + declare function watchFile( + filename: string, + options?: Object, + listener?: (curr: Stats, prev: Stats) => void + ): void; + declare function unwatchFile( + filename: string, + listener?: (curr: Stats, prev: Stats) => void + ): void; + declare function watch( + filename: string, + options?: Object, + listener?: (event: string, filename: string) => void + ): FSWatcher; + declare function exists( + path: string, + callback?: (exists: boolean) => void + ): void; + declare function existsSync(path: string): boolean; + declare function access( + path: string, + mode?: number, + callback?: (err: ?ErrnoError) => void + ): void; + declare function accessSync(path: string, mode?: number): void; + declare function createReadStream(path: string, options?: Object): ReadStream; + declare function createWriteStream( + path: string, + options?: Object + ): WriteStream; + declare function fdatasync( + fd: number, + callback: (err: ?ErrnoError) => void + ): void; + declare function fdatasyncSync(fd: number): void; + declare function copyFile( + src: string, + dest: string, + callback: (err: ErrnoError) => void + ): void; + declare function copyFile( + src: string, + dest: string, + flags?: number, + callback: (err: ErrnoError) => void + ): void; + declare function copyFileSync( + src: string, + dest: string, + flags?: number + ): void; + + declare type GlobOptions = $ReadOnly<{ + /** + * Current working directory. + * @default process.cwd() + */ + cwd?: string | void, + /** + * `true` if the glob should return paths as `Dirent`s, `false` otherwise. + * @default false + * @since v22.2.0 + */ + withFileTypes?: WithFileTypes, + /** + * Function to filter out files/directories or a + * list of glob patterns to be excluded. If a function is provided, return + * `true` to exclude the item, `false` to include it. + * @default undefined + */ + exclude?: + | ((fileName: Node$Conditional) => boolean) + | $ReadOnlyArray, + ... + }>; + + /** + * Retrieves the files matching the specified pattern. + * + * ```js + * import { glob } from 'node:fs'; + * + * glob('*.js', (err, matches) => { + * if (err) throw err; + * console.log(matches); + * }); + * ``` + * @since v22.0.0 + */ + declare function glob( + pattern: string | $ReadOnlyArray, + callback: (err: ?ErrnoError, matches: Array) => void + ): void; + + declare function glob( + pattern: string | $ReadOnlyArray, + options: GlobOptions, + callback: ( + err: ?ErrnoError, + matches: Node$Conditional, Array> + ) => void + ): void; + + /** + * ```js + * import { globSync } from 'node:fs'; + * + * console.log(globSync('*.js')); + * ``` + * @since v22.0.0 + * @returns paths of files that match the pattern. + */ + declare function globSync( + pattern: string | $ReadOnlyArray, + options?: GlobOptions + ): Node$Conditional, Array>; + + declare var F_OK: number; + declare var R_OK: number; + declare var W_OK: number; + declare var X_OK: number; + // new var from node 6.x + // https://nodejs.org/dist/latest-v6.x/docs/api/fs.html#fs_fs_constants_1 + declare var constants: { + F_OK: number, // 0 + R_OK: number, // 4 + W_OK: number, // 2 + X_OK: number, // 1 + COPYFILE_EXCL: number, // 1 + COPYFILE_FICLONE: number, // 2 + COPYFILE_FICLONE_FORCE: number, // 4 + O_RDONLY: number, // 0 + O_WRONLY: number, // 1 + O_RDWR: number, // 2 + S_IFMT: number, // 61440 + S_IFREG: number, // 32768 + S_IFDIR: number, // 16384 + S_IFCHR: number, // 8192 + S_IFBLK: number, // 24576 + S_IFIFO: number, // 4096 + S_IFLNK: number, // 40960 + S_IFSOCK: number, // 49152 + O_CREAT: number, // 64 + O_EXCL: number, // 128 + O_NOCTTY: number, // 256 + O_TRUNC: number, // 512 + O_APPEND: number, // 1024 + O_DIRECTORY: number, // 65536 + O_NOATIME: number, // 262144 + O_NOFOLLOW: number, // 131072 + O_SYNC: number, // 1052672 + O_DSYNC: number, // 4096 + O_SYMLINK: number, // 2097152 + O_DIRECT: number, // 16384 + O_NONBLOCK: number, // 2048 + S_IRWXU: number, // 448 + S_IRUSR: number, // 256 + S_IWUSR: number, // 128 + S_IXUSR: number, // 64 + S_IRWXG: number, // 56 + S_IRGRP: number, // 32 + S_IWGRP: number, // 16 + S_IXGRP: number, // 8 + S_IRWXO: number, // 7 + S_IROTH: number, // 4 + S_IWOTH: number, // 2 + S_IXOTH: number, // 1 + ... + }; + + declare type BufferEncoding = 'buffer' | {encoding: 'buffer', ...}; + declare type EncodingOptions = {encoding?: string, ...}; + declare type EncodingFlag = EncodingOptions & {flag?: string, ...}; + declare type WriteOptions = EncodingFlag & {mode?: number, ...}; + declare type RemoveOptions = { + force?: boolean, + maxRetries?: number, + recursive?: boolean, + retryDelay?: number, + ... + }; + declare class FileHandle { + appendFile( + data: string | Buffer, + options: WriteOptions | string + ): Promise; + chmod(mode: number): Promise; + chown(uid: number, guid: number): Promise; + close(): Promise; + datasync(): Promise; + fd: number; + read( + buffer: T, + offset: number, + length: number, + position: number + ): Promise<{ + bytesRead: number, + buffer: T, + ... + }>; + readFile(options: EncodingFlag): Promise; + readFile(options: string): Promise; + stat(): Promise; + sync(): Promise; + truncate(len?: number): Promise; + utimes( + atime: number | string | Date, + mtime: number | string | Date + ): Promise; + write( + buffer: Buffer | Uint8Array, + offset: number, + length: number, + position: number + ): Promise; + writeFile( + data: string | Buffer | Uint8Array, + options: WriteOptions | string + ): Promise; + } + + declare type FSPromisePath = string | Buffer | URL; + declare type FSPromise = { + access(path: FSPromisePath, mode?: number): Promise, + appendFile( + path: FSPromisePath | FileHandle, + data: string | Buffer, + options?: WriteOptions | string + ): Promise, + chmod(path: FSPromisePath, mode: number): Promise, + chown(path: FSPromisePath, uid: number, gid: number): Promise, + copyFile( + src: FSPromisePath, + dest: FSPromisePath, + flags?: number + ): Promise, + fchmod(filehandle: FileHandle, mode: number): Promise, + fchown(filehandle: FileHandle, uid: number, guid: number): Promise, + fdatasync(filehandle: FileHandle): Promise, + fstat(filehandle: FileHandle): Promise, + fsync(filehandle: FileHandle): Promise, + ftruncate(filehandle: FileHandle, len?: number): Promise, + futimes( + filehandle: FileHandle, + atime: number | string | Date, + mtime: number | string | Date + ): Promise, + lchmod(path: FSPromisePath, mode: number): Promise, + glob( + pattern: string | $ReadOnlyArray, + options?: GlobOptions + ): Node$Conditional< + WithFileTypes, + AsyncIterator, + AsyncIterator, + >, + lchown(path: FSPromisePath, uid: number, guid: number): Promise, + link(existingPath: FSPromisePath, newPath: FSPromisePath): Promise, + lstat(path: FSPromisePath): Promise, + mkdir( + path: FSPromisePath, + mode?: + | number + | { + recursive?: boolean, + mode?: number, + ... + } + ): Promise, + mkdtemp(prefix: string, options?: EncodingOptions): Promise, + open( + path: FSPromisePath, + flags?: string | number, + mode?: number + ): Promise, + read( + filehandle: FileHandle, + buffer: T, + offset: number, + length: number, + position?: number + ): Promise<{ + bytesRead: number, + buffer: T, + ... + }>, + readdir: (( + path: FSPromisePath, + options: string | {encoding?: string, withFileTypes?: false, ...} + ) => Promise>) & + (( + path: FSPromisePath, + options: {encoding?: string, withFileTypes: true, ...} + ) => Promise>) & + ((path: FSPromisePath) => Promise>), + readFile: (( + path: FSPromisePath | FileHandle, + options: string + ) => Promise) & + (( + path: FSPromisePath | FileHandle, + options?: EncodingFlag + ) => Promise), + readlink: (( + path: FSPromisePath, + options: BufferEncoding + ) => Promise) & + (( + path: FSPromisePath, + options?: string | EncodingOptions + ) => Promise), + realpath: (( + path: FSPromisePath, + options: BufferEncoding + ) => Promise) & + (( + path: FSPromisePath, + options?: string | EncodingOptions + ) => Promise), + rename(oldPath: FSPromisePath, newPath: FSPromisePath): Promise, + rm(path: FSPromisePath, options?: RemoveOptions): Promise, + rmdir(path: FSPromisePath): Promise, + stat(path: FSPromisePath): Promise, + symlink( + target: FSPromisePath, + path: FSPromisePath, + type?: 'dir' | 'file' | 'junction' + ): Promise, + truncate(path: FSPromisePath, len?: number): Promise, + unlink(path: FSPromisePath): Promise, + utimes( + path: FSPromisePath, + atime: number | string | Date, + mtime: number | string | Date + ): Promise, + write( + filehandle: FileHandle, + buffer: T, + offset: number, + length: number, + position?: number + ): Promise<{ + bytesRead: number, + buffer: T, + ... + }>, + writeFile( + FSPromisePath | FileHandle, + data: string | Buffer | Uint8Array, + options?: string | WriteOptions + ): Promise, + ... + }; + + declare var promises: FSPromise; +} + +type http$agentOptions = { + keepAlive?: boolean, + keepAliveMsecs?: number, + maxSockets?: number, + maxFreeSockets?: number, + ... +}; + +declare class http$Agent<+SocketT = net$Socket> { + constructor(options: http$agentOptions): void; + destroy(): void; + freeSockets: {[name: string]: $ReadOnlyArray, ...}; + getName(options: { + host: string, + port: number, + localAddress: string, + ... + }): string; + maxFreeSockets: number; + maxSockets: number; + requests: {[name: string]: $ReadOnlyArray>, ...}; + sockets: {[name: string]: $ReadOnlyArray, ...}; +} + +declare class http$IncomingMessage + extends stream$Readable +{ + headers: Object; + rawHeaders: Array; + httpVersion: string; + method: string; + trailers: Object; + setTimeout(msecs: number, callback: Function): void; + socket: SocketT; + statusCode: number; + statusMessage: string; + url: string; + aborted: boolean; + complete: boolean; + rawTrailers: Array; +} + +declare class http$ClientRequest<+SocketT = net$Socket> + extends stream$Writable +{ + abort(): void; + aborted: boolean; + +connection: SocketT | null; + flushHeaders(): void; + getHeader(name: string): string; + removeHeader(name: string): void; + setHeader(name: string, value: string | Array): void; + setNoDelay(noDelay?: boolean): void; + setSocketKeepAlive(enable?: boolean, initialDelay?: number): void; + setTimeout(msecs: number, callback?: Function): void; + +socket: SocketT | null; +} + +declare class http$ServerResponse extends stream$Writable { + addTrailers(headers: {[key: string]: string, ...}): void; + connection: net$Socket; + finished: boolean; + flushHeaders(): void; + getHeader(name: string): string; + getHeaderNames(): Array; + getHeaders(): {[key: string]: string | Array, ...}; + hasHeader(name: string): boolean; + headersSent: boolean; + removeHeader(name: string): void; + sendDate: boolean; + setHeader(name: string, value: string | Array): void; + setTimeout(msecs: number, callback?: Function): http$ServerResponse; + socket: net$Socket; + statusCode: number; + statusMessage: string; + writeContinue(): void; + writeHead( + status: number, + statusMessage?: string, + headers?: {[key: string]: string, ...} + ): void; + writeHead(status: number, headers?: {[key: string]: string, ...}): void; + writeProcessing(): void; +} + +declare class http$Server extends net$Server { + listen( + port?: number, + hostname?: string, + backlog?: number, + callback?: Function + ): this; + // The following signatures are added to allow omitting intermediate arguments + listen(port?: number, backlog?: number, callback?: Function): this; + listen(port?: number, hostname?: string, callback?: Function): this; + listen(port?: number, callback?: Function): this; + listen(path: string, callback?: Function): this; + listen( + handle: { + port?: number, + host?: string, + path?: string, + backlog?: number, + exclusive?: boolean, + readableAll?: boolean, + writableAll?: boolean, + ipv6Only?: boolean, + ... + }, + callback?: Function + ): this; + listening: boolean; + close(callback?: (error: ?Error) => mixed): this; + closeAllConnections(): void; + closeIdleConnections(): void; + maxHeadersCount: number; + keepAliveTimeout: number; + headersTimeout: number; + setTimeout(msecs: number, callback: Function): this; + timeout: number; +} + +declare class https$Server extends tls$Server { + listen( + port?: number, + hostname?: string, + backlog?: number, + callback?: Function + ): this; + // The following signatures are added to allow omitting intermediate arguments + listen(port?: number, backlog?: number, callback?: Function): this; + listen(port?: number, hostname?: string, callback?: Function): this; + listen(port?: number, callback?: Function): this; + listen(path: string, callback?: Function): this; + listen( + handle: { + port?: number, + host?: string, + path?: string, + backlog?: number, + exclusive?: boolean, + readableAll?: boolean, + writableAll?: boolean, + ipv6Only?: boolean, + ... + }, + callback?: Function + ): this; + close(callback?: (error: ?Error) => mixed): this; + closeAllConnections(): void; + closeIdleConnections(): void; + keepAliveTimeout: number; + headersTimeout: number; + setTimeout(msecs: number, callback: Function): this; + timeout: number; +} + +type requestOptions = {| + auth?: string, + defaultPort?: number, + family?: number, + headers?: {[key: string]: mixed, ...}, + host?: string, + hostname?: string, + localAddress?: string, + method?: string, + path?: string, + port?: number, + protocol?: string, + setHost?: boolean, + socketPath?: string, + timeout?: number, +|}; + +type http$requestOptions = { + ...requestOptions, + agent?: boolean | http$Agent, + createConnection?: ( + options: net$connectOptions, + callback?: Function + ) => net$Socket, + ... +}; + +declare module 'http' { + declare class Server extends http$Server {} + declare class Agent extends http$Agent { + createConnection( + options: net$connectOptions, + callback?: Function + ): net$Socket; + } + declare class ClientRequest extends http$ClientRequest {} + declare class IncomingMessage extends http$IncomingMessage {} + declare class ServerResponse extends http$ServerResponse {} + + declare function createServer( + requestListener?: ( + request: IncomingMessage, + response: ServerResponse + ) => void + ): Server; + declare function request( + options: http$requestOptions, + callback?: (response: IncomingMessage) => void + ): ClientRequest; + declare function request( + url: string, + options?: http$requestOptions, + callback?: (response: IncomingMessage) => void + ): ClientRequest; + declare function get( + options: http$requestOptions, + callback?: (response: IncomingMessage) => void + ): ClientRequest; + declare function get( + url: string, + options?: http$requestOptions, + callback?: (response: IncomingMessage) => void + ): ClientRequest; + + declare var METHODS: Array; + declare var STATUS_CODES: {[key: number]: string, ...}; + declare var globalAgent: Agent; +} + +type https$requestOptions = { + ...requestOptions, + agent?: boolean | http$Agent, + createConnection?: ( + options: tls$connectOptions, + callback?: Function + ) => tls$TLSSocket, + ... +}; + +declare module 'https' { + declare class Server extends https$Server {} + declare class Agent extends http$Agent { + createConnection( + port: ?number, + host: ?string, + options: tls$connectOptions + ): tls$TLSSocket; + createConnection(port: ?number, options: tls$connectOptions): tls$TLSSocket; + createConnection(options: tls$connectOptions): tls$TLSSocket; + } + + declare class ClientRequest extends http$ClientRequest {} + declare class IncomingMessage extends http$IncomingMessage {} + declare class ServerResponse extends http$ServerResponse {} + + declare function createServer( + options: Object, + requestListener?: ( + request: IncomingMessage, + response: ServerResponse + ) => void + ): Server; + declare function request( + options: https$requestOptions, + callback?: (response: IncomingMessage) => void + ): ClientRequest; + declare function request( + url: string, + options?: https$requestOptions, + callback?: (response: IncomingMessage) => void + ): ClientRequest; + declare function get( + options: https$requestOptions, + callback?: (response: IncomingMessage) => void + ): ClientRequest; + declare function get( + url: string, + options?: https$requestOptions, + callback?: (response: IncomingMessage) => void + ): ClientRequest; + + declare var globalAgent: Agent; +} + +type module$Module = { + builtinModules: Array, + createRequire(filename: string | URL): typeof require, + syncBuiltinESMExports(): void, + Module: module$Module, + ... +}; + +declare module 'module' { + declare module.exports: module$Module; +} + +declare class net$Socket extends stream$Duplex { + constructor(options?: Object): void; + address(): net$Socket$address; + bufferSize: number; + bytesRead: number; + bytesWritten: number; + connect(path: string, connectListener?: () => mixed): net$Socket; + connect( + port: number, + host?: string, + connectListener?: () => mixed + ): net$Socket; + connect(port: number, connectListener?: () => mixed): net$Socket; + connect(options: Object, connectListener?: () => mixed): net$Socket; + destroyed: boolean; + end( + chunkOrEncodingOrCallback?: + | Buffer + | Uint8Array + | string + | ((data: any) => void), + encodingOrCallback?: string | ((data: any) => void), + callback?: (data: any) => void + ): this; + localAddress: string; + localPort: number; + pause(): this; + ref(): this; + remoteAddress: string | void; + remoteFamily: string; + remotePort: number; + resume(): this; + setEncoding(encoding?: string): this; + setKeepAlive(enable?: boolean, initialDelay?: number): this; + setNoDelay(noDelay?: boolean): this; + setTimeout(timeout: number, callback?: Function): this; + unref(): this; + write( + chunk: Buffer | Uint8Array | string, + encodingOrCallback?: string | ((data: any) => void), + callback?: (data: any) => void + ): boolean; +} + +declare class net$Server extends events$EventEmitter { + listen( + port?: number, + hostname?: string, + backlog?: number, + callback?: Function + ): net$Server; + listen(path: string, callback?: Function): net$Server; + listen(handle: Object, callback?: Function): net$Server; + listening: boolean; + close(callback?: Function): net$Server; + address(): net$Socket$address; + connections: number; + maxConnections: number; + getConnections(callback: Function): void; + ref(): net$Server; + unref(): net$Server; +} + +type net$connectOptions = { + port?: number, + host?: string, + localAddress?: string, + localPort?: number, + family?: number, + lookup?: ( + domain: string, + options?: ?number | ?Object, + callback?: (err: ?Error, address: string, family: number) => void + ) => mixed, + path?: string, + ... +}; + +declare module 'net' { + declare class Server extends net$Server {} + declare class Socket extends net$Socket {} + + declare function isIP(input: string): number; + declare function isIPv4(input: string): boolean; + declare function isIPv6(input: string): boolean; + + declare type connectionListener = (socket: Socket) => any; + declare function createServer( + options?: + | { + allowHalfOpen?: boolean, + pauseOnConnect?: boolean, + ... + } + | connectionListener, + connectionListener?: connectionListener + ): Server; + + declare type connectListener = () => any; + declare function connect( + pathOrPortOrOptions: string | number | net$connectOptions, + hostOrConnectListener?: string | connectListener, + connectListener?: connectListener + ): Socket; + + declare function createConnection( + pathOrPortOrOptions: string | number | net$connectOptions, + hostOrConnectListener?: string | connectListener, + connectListener?: connectListener + ): Socket; +} + +type os$CPU = { + model: string, + speed: number, + times: { + idle: number, + irq: number, + nice: number, + sys: number, + user: number, + ... + }, + ... +}; + +type os$NetIFAddr = { + address: string, + family: string, + internal: boolean, + mac: string, + netmask: string, + ... +}; + +type os$UserInfo$buffer = { + uid: number, + gid: number, + username: Buffer, + homedir: Buffer, + shell: ?Buffer, + ... +}; + +type os$UserInfo$string = { + uid: number, + gid: number, + username: string, + homedir: string, + shell: ?string, + ... +}; + +declare module 'os' { + declare function arch(): 'x64' | 'arm' | 'ia32'; + declare function availableParallelism(): number; + declare function cpus(): Array; + declare function endianness(): 'BE' | 'LE'; + declare function freemem(): number; + declare function homedir(): string; + declare function hostname(): string; + declare function loadavg(): [number, number, number]; + declare function networkInterfaces(): { + [ifName: string]: Array, + ... + }; + declare function platform(): string; + declare function release(): string; + declare function tmpdir(): string; + declare function totalmem(): number; + declare function type(): string; + declare function uptime(): number; + declare function userInfo(options: { + encoding: 'buffer', + ... + }): os$UserInfo$buffer; + declare function userInfo(options?: { + encoding: 'utf8', + ... + }): os$UserInfo$string; + declare var EOL: string; +} + +declare module 'path' { + declare function normalize(path: string): string; + declare function join(...parts: Array): string; + declare function resolve(...parts: Array): string; + declare function isAbsolute(path: string): boolean; + declare function relative(from: string, to: string): string; + declare function dirname(path: string): string; + declare function basename(path: string, ext?: string): string; + declare function extname(path: string): string; + declare var sep: string; + declare var delimiter: string; + declare function parse(pathString: string): { + root: string, + dir: string, + base: string, + ext: string, + name: string, + ... + }; + declare function format(pathObject: { + root?: string, + dir?: string, + base?: string, + ext?: string, + name?: string, + ... + }): string; + declare var posix: any; + declare var win32: any; +} + +declare module 'punycode' { + declare function decode(string: string): string; + declare function encode(string: string): string; + declare function toASCII(domain: string): string; + declare function toUnicode(domain: string): string; + declare var ucs2: { + decode: (str: string) => Array, + encode: (codePoints: Array) => string, + ... + }; + declare var version: string; +} + +declare module 'querystring' { + declare function stringify( + obj: Object, + separator?: string, + equal?: string, + options?: {encodeURIComponent?: (str: string) => string, ...} + ): string; + declare function parse( + str: string, + separator: ?string, + equal: ?string, + options?: { + decodeURIComponent?: (str: string) => string, + maxKeys?: number, + ... + } + ): any; + declare function escape(str: string): string; + declare function unescape(str: string, decodeSpaces?: boolean): string; +} + +type readline$InterfaceCompleter = ( + line: string +) => + | [Array, string] + | (( + line: string, + ((err: ?Error, data: [Array, string]) => void) + ) => void); + +declare class readline$Interface extends events$EventEmitter { + close(): void; + pause(): void; + prompt(preserveCursor?: boolean): void; + question( + query: string, + optionsOrCallback: {|signal?: AbortSignal|} | ((answer: string) => void), + callback?: (answer: string) => void + ): void; + resume(): void; + setPrompt(prompt: string): void; + write( + val: string | void | null, + key?: { + name: string, + ctrl?: boolean, + shift?: boolean, + meta?: boolean, + ... + } + ): void; + @@asyncIterator(): AsyncIterator; +} + +declare module 'readline' { + declare var Interface: typeof readline$Interface; + declare function clearLine( + stream: stream$Stream, + dir: -1 | 1 | 0, + callback?: () => void + ): void; + declare function clearScreenDown( + stream: stream$Stream, + callback?: () => void + ): void; + declare function createInterface(opts: { + completer?: readline$InterfaceCompleter, + crlfDelay?: number, + escapeCodeTimeout?: number, + historySize?: number, + input: stream$Readable, + output?: ?stream$Stream, + prompt?: string, + removeHistoryDuplicates?: boolean, + terminal?: boolean, + ... + }): readline$Interface; + declare function cursorTo( + stream: stream$Stream, + x?: number, + y?: number, + callback?: () => void + ): void; + declare function moveCursor( + stream: stream$Stream, + dx: number, + dy: number, + callback?: () => void + ): void; + declare function emitKeypressEvents( + stream: stream$Stream, + readlineInterface?: readline$Interface + ): void; +} + +declare class stream$Stream extends events$EventEmitter {} + +type readableStreamOptions = { + highWaterMark?: number, + encoding?: string, + objectMode?: boolean, + read?: (size: number) => void, + destroy?: (error: ?Error, callback: (error?: Error) => void) => void, + autoDestroy?: boolean, + ... +}; +declare class stream$Readable extends stream$Stream { + static from( + iterable: Iterable | AsyncIterable, + options?: readableStreamOptions + ): stream$Readable; + + constructor(options?: readableStreamOptions): void; + destroy(error?: Error): this; + isPaused(): boolean; + pause(): this; + pipe(dest: T, options?: {end?: boolean, ...}): T; + read(size?: number): ?(string | Buffer); + readable: boolean; + readableHighWaterMark: number; + readableLength: number; + resume(): this; + setEncoding(encoding: string): this; + unpipe(dest?: stream$Writable): this; + unshift(chunk: Buffer | Uint8Array | string): void; + wrap(oldReadable: stream$Stream): this; + _read(size: number): void; + _destroy(error: ?Error, callback: (error?: Error) => void): void; + push(chunk: ?(Buffer | Uint8Array | string), encoding?: string): boolean; + @@asyncIterator(): AsyncIterator; +} + +type writableStreamOptions = { + highWaterMark?: number, + decodeStrings?: boolean, + defaultEncoding?: string, + objectMode?: boolean, + emitClose?: boolean, + write?: ( + chunk: Buffer | string, + encoding: string, + callback: (error?: Error) => void + ) => void, + writev?: ( + chunks: Array<{ + chunk: Buffer | string, + encoding: string, + ... + }>, + callback: (error?: Error) => void + ) => void, + destroy?: (error: ?Error, callback: (error?: Error) => void) => void, + final?: (callback: (error?: Error) => void) => void, + autoDestroy?: boolean, + ... +}; +declare class stream$Writable extends stream$Stream { + constructor(options?: writableStreamOptions): void; + cork(): void; + destroy(error?: Error): this; + end(callback?: () => void): this; + end(chunk?: string | Buffer | Uint8Array, callback?: () => void): this; + end( + chunk?: string | Buffer | Uint8Array, + encoding?: string, + callback?: () => void + ): this; + setDefaultEncoding(encoding: string): this; + uncork(): void; + writable: boolean; + writableHighWaterMark: number; + writableLength: number; + write( + chunk: string | Buffer | Uint8Array, + callback?: (error?: Error) => void + ): boolean; + write( + chunk: string | Buffer | Uint8Array, + encoding?: string, + callback?: (error?: Error) => void + ): boolean; + _write( + chunk: Buffer | string, + encoding: string, + callback: (error?: Error) => void + ): void; + _writev( + chunks: Array<{ + chunk: Buffer | string, + encoding: string, + ... + }>, + callback: (error?: Error) => void + ): void; + _destroy(error: ?Error, callback: (error?: Error) => void): void; + _final(callback: (error?: Error) => void): void; +} + +//According to the NodeJS docs: +//"Since JavaScript doesn't have multiple prototypal inheritance, this class +//prototypally inherits from Readable, and then parasitically from Writable." +//Source: void) => void, + transform?: ( + chunk: Buffer | string, + encoding: string, + callback: (error: ?Error, data: ?(Buffer | string)) => void + ) => void, + ... +}; +declare class stream$Transform extends stream$Duplex { + constructor(options?: transformStreamOptions): void; + _flush(callback: (error: ?Error, data: ?(Buffer | string)) => void): void; + _transform( + chunk: Buffer | string, + encoding: string, + callback: (error: ?Error, data: ?(Buffer | string)) => void + ): void; +} +declare class stream$PassThrough extends stream$Transform {} + +declare module 'stream' { + declare var Stream: typeof stream$Stream; + declare var Readable: typeof stream$Readable; + declare var Writable: typeof stream$Writable; + declare var Duplex: typeof stream$Duplex; + declare var Transform: typeof stream$Transform; + declare var PassThrough: typeof stream$PassThrough; + declare function finished( + stream: stream$Stream, + callback: (error?: Error) => void + ): () => void; + declare function finished( + stream: stream$Stream, + options: ?{ + error?: boolean, + readable?: boolean, + writable?: boolean, + ... + }, + callback: (error?: Error) => void + ): () => void; + declare function pipeline( + s1: stream$Readable, + last: T, + cb: (error?: Error) => void + ): T; + declare function pipeline( + s1: stream$Readable, + s2: stream$Duplex, + last: T, + cb: (error?: Error) => void + ): T; + declare function pipeline( + s1: stream$Readable, + s2: stream$Duplex, + s3: stream$Duplex, + last: T, + cb: (error?: Error) => void + ): T; + declare function pipeline( + s1: stream$Readable, + s2: stream$Duplex, + s3: stream$Duplex, + s4: stream$Duplex, + last: T, + cb: (error?: Error) => void + ): T; + declare function pipeline( + s1: stream$Readable, + s2: stream$Duplex, + s3: stream$Duplex, + s4: stream$Duplex, + s5: stream$Duplex, + last: T, + cb: (error?: Error) => void + ): T; + declare function pipeline( + s1: stream$Readable, + s2: stream$Duplex, + s3: stream$Duplex, + s4: stream$Duplex, + s5: stream$Duplex, + s6: stream$Duplex, + last: T, + cb: (error?: Error) => void + ): T; + declare function pipeline( + streams: Array, + cb: (error?: Error) => void + ): stream$Stream; + + declare interface StreamPipelineOptions { + +signal?: AbortSignal; + +end?: boolean; + } + + declare type StreamPromise = { + pipeline( + s1: stream$Readable, + last: stream$Writable, + options?: StreamPipelineOptions + ): Promise, + pipeline( + s1: stream$Readable, + s2: stream$Duplex, + last: stream$Writable, + options?: StreamPipelineOptions + ): Promise, + pipeline( + s1: stream$Readable, + s2: stream$Duplex, + s3: stream$Duplex, + last: stream$Writable, + options?: StreamPipelineOptions + ): Promise, + pipeline( + s1: stream$Readable, + s2: stream$Duplex, + s3: stream$Duplex, + s4: stream$Duplex, + last: stream$Writable, + options?: StreamPipelineOptions + ): Promise, + pipeline( + s1: stream$Readable, + s2: stream$Duplex, + s3: stream$Duplex, + s4: stream$Duplex, + s5: stream$Duplex, + last: stream$Writable, + options?: StreamPipelineOptions + ): Promise, + pipeline( + s1: stream$Readable, + s2: stream$Duplex, + s3: stream$Duplex, + s4: stream$Duplex, + s5: stream$Duplex, + s6: stream$Duplex, + last: stream$Writable, + options?: StreamPipelineOptions + ): Promise, + pipeline( + streams: $ReadOnlyArray, + options?: StreamPipelineOptions + ): Promise, + ... + }; + + declare var promises: StreamPromise; +} + +declare class tty$ReadStream extends net$Socket { + constructor(fd: number, options?: Object): void; + isRaw: boolean; + setRawMode(mode: boolean): void; + isTTY: true; +} +declare class tty$WriteStream extends net$Socket { + constructor(fd: number): void; + /** + * Clears the current line of this `WriteStream` in a direction identified by `dir`. + * + * TODO: takes a callback and returns `boolean` in v12+ + */ + clearLine(dir: -1 | 0 | 1): void; + columns: number; + /** + * Moves this WriteStream's cursor to the specified position + * + * TODO: takes a callback and returns `boolean` in v12+ + */ + cursorTo(x: number, y?: number): void; + isTTY: true; + /** + * Moves this WriteStream's cursor relative to its current position + * + * TODO: takes a callback and returns `boolean` in v12+ + */ + moveCursor(dx: number, dy: number): void; + rows: number; + + /** + * Clears this WriteStream from the current cursor down. + */ + clearScreenDown(callback?: () => void): boolean; + + /** + * Use this to determine what colors the terminal supports. Due to the nature of colors in terminals it is possible to either have false positives or false negatives. It depends on process information and the environment variables that may lie about what terminal is used. It is possible to pass in an env object to simulate the usage of a specific terminal. This can be useful to check how specific environment settings behave. + * To enforce a specific color support, use one of the below environment settings. + * + * 2 colors: FORCE_COLOR = 0 (Disables colors) + * 16 colors: FORCE_COLOR = 1 + * 256 colors: FORCE_COLOR = 2 + * 16,777,216 colors: FORCE_COLOR = 3 + * Disabling color support is also possible by using the NO_COLOR and NODE_DISABLE_COLORS environment variables. + */ + getColorDepth(env?: typeof process.env): + | 1 // 2 + | 4 // 16 + | 8 // 256 + | 24; // 16,777,216 + + /** + * Returns the size of the TTY corresponding to this WriteStream. The array is of the type [numColumns, numRows] where numColumns and numRows represent the number of columns and rows in the corresponding TTY. + */ + getWindowSize(): [ + number, // columns + number, // rows + ]; + + /** + * - count The number of colors that are requested (minimum 2). Default: 16. + * - env An object containing the environment variables to check. This enables simulating the usage of a specific terminal. Default: process.env. + * - Returns: + * + * Returns true if the writeStream supports at least as many colors as provided in count. Minimum support is 2 (black and white). + * + * This has the same false positives and negatives as described in tty$WriteStream#getColorDepth(). + */ + hasColors(count?: number, env?: typeof process.env): boolean; +} + +declare module 'tty' { + declare function isatty(fd: number): boolean; + declare function setRawMode(mode: boolean): void; + declare var ReadStream: typeof tty$ReadStream; + declare var WriteStream: typeof tty$WriteStream; +} + +declare class string_decoder$StringDecoder { + constructor(encoding?: 'utf8' | 'ucs2' | 'utf16le' | 'base64'): void; + end(): string; + write(buffer: Buffer): string; +} + +declare module 'string_decoder' { + declare var StringDecoder: typeof string_decoder$StringDecoder; +} + +type tls$connectOptions = { + port?: number, + host?: string, + socket?: net$Socket, + rejectUnauthorized?: boolean, + path?: string, + lookup?: ( + domain: string, + options?: ?number | ?Object, + callback?: (err: ?Error, address: string, family: number) => void + ) => mixed, + requestOCSP?: boolean, + ... +}; + +type tls$Certificate$Subject = { + C?: string, + ST?: string, + L?: string, + O?: string, + OU?: string, + CN?: string, + ... +}; + +type tls$Certificate = { + raw: Buffer, + subject: tls$Certificate$Subject, + issuer: tls$Certificate$Subject, + valid_from: string, + valid_to: string, + serialNumber: string, + fingerprint: string, + fingerprint256: string, + ext_key_usage?: Array, + subjectaltname?: string, + infoAccess?: {[string]: Array, ...}, + issuerCertificate?: tls$Certificate, + ... +}; + +declare class tls$TLSSocket extends net$Socket { + constructor(socket: net$Socket, options?: Object): void; + authorized: boolean; + authorizationError: string | null; + encrypted: true; + getCipher(): { + name: string, + version: string, + ... + } | null; + getEphemeralKeyInfo(): + | { + type: 'DH', + size: number, + ... + } + | { + type: 'EDHC', + name: string, + size: number, + ... + } + | null; + getPeerCertificate(detailed?: boolean): tls$Certificate | null; + getSession(): ?Buffer; + getTLSTicket(): Buffer | void; + renegotiate(options: Object, callback: Function): boolean | void; + setMaxSendFragment(size: number): boolean; +} + +declare class tls$Server extends net$Server { + listen( + port?: number, + hostname?: string, + backlog?: number, + callback?: Function + ): tls$Server; + listen(path: string, callback?: Function): tls$Server; + listen(handle: Object, callback?: Function): tls$Server; + close(callback?: Function): tls$Server; + addContext(hostname: string, context: Object): void; + getTicketKeys(): Buffer; + setTicketKeys(keys: Buffer): void; +} + +declare module 'tls' { + declare var CLIENT_RENEG_LIMIT: number; + declare var CLIENT_RENEG_WINDOW: number; + declare var SLAB_BUFFER_SIZE: number; + declare var DEFAULT_CIPHERS: string; + declare var DEFAULT_ECDH_CURVE: string; + declare function getCiphers(): Array; + declare function convertNPNProtocols( + NPNProtocols: Array, + out: Object + ): void; + declare function checkServerIdentity( + servername: string, + cert: string + ): Error | void; + declare function parseCertString(s: string): Object; + declare function createSecureContext(details: Object): Object; + declare var SecureContext: Object; + declare var TLSSocket: typeof tls$TLSSocket; + declare var Server: typeof tls$Server; + declare function createServer( + options: Object, + secureConnectionListener?: Function + ): tls$Server; + declare function connect( + options: tls$connectOptions, + callback?: Function + ): tls$TLSSocket; + declare function connect( + port: number, + host?: string, + options?: tls$connectOptions, + callback?: Function + ): tls$TLSSocket; + declare function createSecurePair( + context?: Object, + isServer?: boolean, + requestCert?: boolean, + rejectUnauthorized?: boolean, + options?: Object + ): Object; +} + +type url$urlObject = { + +href?: string, + +protocol?: string | null, + +slashes?: boolean | null, + +auth?: string | null, + +hostname?: string | null, + +port?: string | number | null, + +host?: string | null, + +pathname?: string | null, + +search?: string | null, + +query?: Object | null, + +hash?: string | null, + ... +}; + +declare module 'url' { + declare type Url = {| + protocol: string | null, + slashes: boolean | null, + auth: string | null, + host: string | null, + port: string | null, + hostname: string | null, + hash: string | null, + search: string | null, + query: string | null | {[string]: string, ...}, + pathname: string | null, + path: string | null, + href: string, + |}; + + declare type UrlWithStringQuery = {| + ...Url, + query: string | null, + |}; + + declare type UrlWithParsedQuery = {| + ...Url, + query: {[string]: string, ...}, + |}; + + declare function parse( + urlStr: string, + parseQueryString: true, + slashesDenoteHost?: boolean + ): UrlWithParsedQuery; + declare function parse( + urlStr: string, + parseQueryString?: false | void, + slashesDenoteHost?: boolean + ): UrlWithStringQuery; + declare function parse( + urlStr: string, + parseQueryString?: boolean, + slashesDenoteHost?: boolean + ): Url; + declare function format(urlObj: url$urlObject): string; + declare function resolve(from: string, to: string): string; + declare function domainToASCII(domain: string): string; + declare function domainToUnicode(domain: string): string; + declare function pathToFileURL(path: string): url$urlObject; + declare function fileURLToPath(path: url$urlObject | string): string; + declare class URLSearchParams { + @@iterator(): Iterator<[string, string]>; + + size: number; + + constructor( + init?: + | string + | URLSearchParams + | Array<[string, string]> + | {[string]: string, ...} + ): void; + append(name: string, value: string): void; + delete(name: string, value?: void): void; + entries(): Iterator<[string, string]>; + forEach( + callback: ( + this: This, + value: string, + name: string, + searchParams: URLSearchParams + ) => mixed, + thisArg?: This + ): void; + get(name: string): string | null; + getAll(name: string): string[]; + has(name: string, value?: string): boolean; + keys(): Iterator; + set(name: string, value: string): void; + sort(): void; + values(): Iterator; + toString(): string; + } + declare class URL { + static canParse(url: string, base?: string): boolean; + static createObjectURL(blob: Blob): string; + static createObjectURL(mediaSource: MediaSource): string; + static revokeObjectURL(url: string): void; + constructor(input: string, base?: string | URL): void; + hash: string; + host: string; + hostname: string; + href: string; + +origin: string; + password: string; + pathname: string; + port: string; + protocol: string; + search: string; + +searchParams: URLSearchParams; + username: string; + toString(): string; + toJSON(): string; + } +} + +type util$InspectOptions = { + showHidden?: boolean, + depth?: ?number, + colors?: boolean, + customInspect?: boolean, + ... +}; + +declare type util$ParseArgsOption = + | {| + type: 'boolean', + multiple?: false, + short?: string, + default?: boolean, + |} + | {| + type: 'boolean', + multiple: true, + short?: string, + default?: Array, + |} + | {| + type: 'string', + multiple?: false, + short?: string, + default?: string, + |} + | {| + type: 'string', + multiple: true, + short?: string, + default?: Array, + |}; + +type util$ParseArgsOptionToValue = TOption['type'] extends 'boolean' + ? TOption['multiple'] extends true + ? Array + : boolean + : TOption['type'] extends 'string' + ? TOption['multiple'] extends true + ? Array + : string + : empty; + +type util$ParseArgsOptionsToValues = { + [key in keyof TOptions]: util$ParseArgsOptionToValue<{| + multiple: false, + ...TOptions[key], + |}>, +}; + +type util$ParseArgsToken = + | {| + kind: 'option', + index: number, + name: string, + rawName: string, + value?: string, + inlineValue?: boolean, + |} + | {| + kind: 'positional', + index: number, + value: string, + |} + | {| + kind: 'option-terminator', + index: number, + |}; + +declare module 'util' { + declare function debuglog(section: string): (data: any, ...args: any) => void; + declare function format(format: string, ...placeholders: any): string; + declare function log(string: string): void; + declare function inspect(object: any, options?: util$InspectOptions): string; + declare function isArray(object: any): boolean; + declare function isRegExp(object: any): boolean; + declare function isDate(object: any): boolean; + declare function isError(object: any): boolean; + declare function inherits( + constructor: Function, + superConstructor: Function + ): void; + declare function deprecate(f: Function, string: string): Function; + declare function promisify(f: Function): Function; + declare function callbackify(f: Function): Function; + declare function stripVTControlCharacters(str: string): string; + + declare function parseArgs< + TOptions: {[string]: util$ParseArgsOption} = {||}, + >(config: {| + args?: Array, + options?: TOptions, + strict?: boolean, + allowPositionals?: boolean, + tokens?: false, + |}): {| + values: util$ParseArgsOptionsToValues, + positionals: Array, + |}; + + declare function parseArgs< + TOptions: {[string]: util$ParseArgsOption} = {||}, + >(config: {| + args?: Array, + options?: TOptions, + strict?: boolean, + allowPositionals?: boolean, + tokens: true, + |}): {| + values: util$ParseArgsOptionsToValues, + positionals: Array, + tokens: Array, + |}; + + declare class TextDecoder { + constructor( + encoding?: string, + options?: { + fatal?: boolean, + ignoreBOM?: boolean, + ... + } + ): void; + decode( + input?: ArrayBuffer | DataView | $TypedArray, + options?: {stream?: boolean, ...} + ): string; + encoding: string; + fatal: boolean; + ignoreBOM: boolean; + } + + declare class TextEncoder { + constructor(): void; + encode(input: string): Uint8Array; + encodeInto( + input: string, + buffer: Uint8Array + ): {written: number, read: number}; + encoding: 'utf-8'; + } + + declare var types: { + isAnyArrayBuffer: (value: mixed) => boolean, + isArgumentsObject: (value: mixed) => boolean, + isArrayBuffer: (value: mixed) => boolean, + isAsyncFunction: (value: mixed) => boolean, + isBigInt64Array: (value: mixed) => boolean, + isBigUint64Array: (value: mixed) => boolean, + isBooleanObject: (value: mixed) => boolean, + isBoxedPrimitive: (value: mixed) => boolean, + isDataView: (value: mixed) => boolean, + isDate: (value: mixed) => boolean, + isExternal: (value: mixed) => boolean, + isFloat32Array: (value: mixed) => boolean, + isFloat64Array: (value: mixed) => boolean, + isGeneratorFunction: (value: mixed) => boolean, + isGeneratorObject: (value: mixed) => boolean, + isInt8Array: (value: mixed) => boolean, + isInt16Array: (value: mixed) => boolean, + isInt32Array: (value: mixed) => boolean, + isMap: (value: mixed) => boolean, + isMapIterator: (value: mixed) => boolean, + isModuleNamespaceObject: (value: mixed) => boolean, + isNativeError: (value: mixed) => boolean, + isNumberObject: (value: mixed) => boolean, + isPromise: (value: mixed) => boolean, + isProxy: (value: mixed) => boolean, + isRegExp: (value: mixed) => boolean, + isSet: (value: mixed) => boolean, + isSetIterator: (value: mixed) => boolean, + isSharedArrayBuffer: (value: mixed) => boolean, + isStringObject: (value: mixed) => boolean, + isSymbolObject: (value: mixed) => boolean, + isTypedArray: (value: mixed) => boolean, + isUint8Array: (value: mixed) => boolean, + isUint8ClampedArray: (value: mixed) => boolean, + isUint16Array: (value: mixed) => boolean, + isUint32Array: (value: mixed) => boolean, + isWeakMap: (value: mixed) => boolean, + isWeakSet: (value: mixed) => boolean, + isWebAssemblyCompiledModule: (value: mixed) => boolean, + ... + }; +} + +type vm$ScriptOptions = { + cachedData?: Buffer, + columnOffset?: number, + displayErrors?: boolean, + filename?: string, + lineOffset?: number, + produceCachedData?: boolean, + timeout?: number, + ... +}; + +type vm$CreateContextOptions = { + name?: string, + origin?: string, + codeGeneration?: { + strings?: boolean, + wasm?: boolean, + ... + }, + ... +}; + +type vm$CompileFunctionOptions = { + filename?: string, + lineOffset?: number, + columnOffset?: number, + cachedData?: Buffer, + produceCachedData?: boolean, + parsingContext?: {[key: string]: any, ...}, + contextExtensions?: Array<{[key: string]: any, ...}>, + ... +}; + +declare class vm$Script { + constructor(code: string, options?: vm$ScriptOptions | string): void; + cachedData: ?Buffer; + cachedDataRejected: ?boolean; + cachedDataProduced: ?boolean; + runInContext( + contextifiedSandbox: vm$Context, + options?: vm$ScriptOptions + ): any; + runInNewContext( + sandbox?: {[key: string]: any, ...}, + options?: vm$ScriptOptions + ): any; + runInThisContext(options?: vm$ScriptOptions): any; + createCachedData(): Buffer; +} + +declare class vm$Context {} + +declare module 'vm' { + declare var Script: typeof vm$Script; + declare function createContext( + sandbox?: interface {[key: string]: any}, + options?: vm$CreateContextOptions + ): vm$Context; + declare function isContext(sandbox: {[key: string]: any, ...}): boolean; + declare function runInContext( + code: string, + contextifiedSandbox: vm$Context, + options?: vm$ScriptOptions | string + ): any; + declare function runInDebugContext(code: string): any; + declare function runInNewContext( + code: string, + sandbox?: {[key: string]: any, ...}, + options?: vm$ScriptOptions | string + ): any; + declare function runInThisContext( + code: string, + options?: vm$ScriptOptions | string + ): any; + declare function compileFunction( + code: string, + params: string[], + options: vm$CompileFunctionOptions + ): Function; +} + +type zlib$options = { + flush?: number, + chunkSize?: number, + windowBits?: number, + level?: number, + memLevel?: number, + strategy?: number, + dictionary?: Buffer, + ... +}; + +type zlib$brotliOptions = { + flush?: number, + finishFlush?: number, + chunkSize?: number, + params?: { + [number]: boolean | number, + ... + }, + maxOutputLength?: number, + ... +}; + +type zlib$syncFn = ( + buffer: Buffer | $TypedArray | DataView | ArrayBuffer | string, + options?: zlib$options +) => Buffer; + +type zlib$asyncFn = ( + buffer: Buffer | $TypedArray | DataView | ArrayBuffer | string, + options?: zlib$options, + callback?: (error: ?Error, result: Buffer) => void +) => void; + +type zlib$brotliSyncFn = ( + buffer: Buffer | $TypedArray | DataView | ArrayBuffer | string, + options?: zlib$brotliOptions +) => Buffer; + +type zlib$brotliAsyncFn = ( + buffer: Buffer | $TypedArray | DataView | ArrayBuffer | string, + options?: zlib$brotliOptions, + callback?: (error: ?Error, result: Buffer) => void +) => void; + +// Accessing the constants directly from the module is currently still +// possible but should be considered deprecated. +// ref: https://github.com/nodejs/node/blob/master/doc/api/zlib.md +declare module 'zlib' { + declare var Z_NO_FLUSH: number; + declare var Z_PARTIAL_FLUSH: number; + declare var Z_SYNC_FLUSH: number; + declare var Z_FULL_FLUSH: number; + declare var Z_FINISH: number; + declare var Z_BLOCK: number; + declare var Z_TREES: number; + declare var Z_OK: number; + declare var Z_STREAM_END: number; + declare var Z_NEED_DICT: number; + declare var Z_ERRNO: number; + declare var Z_STREAM_ERROR: number; + declare var Z_DATA_ERROR: number; + declare var Z_MEM_ERROR: number; + declare var Z_BUF_ERROR: number; + declare var Z_VERSION_ERROR: number; + declare var Z_NO_COMPRESSION: number; + declare var Z_BEST_SPEED: number; + declare var Z_BEST_COMPRESSION: number; + declare var Z_DEFAULT_COMPRESSION: number; + declare var Z_FILTERED: number; + declare var Z_HUFFMAN_ONLY: number; + declare var Z_RLE: number; + declare var Z_FIXED: number; + declare var Z_DEFAULT_STRATEGY: number; + declare var Z_BINARY: number; + declare var Z_TEXT: number; + declare var Z_ASCII: number; + declare var Z_UNKNOWN: number; + declare var Z_DEFLATED: number; + declare var Z_NULL: number; + declare var Z_DEFAULT_CHUNK: number; + declare var Z_DEFAULT_LEVEL: number; + declare var Z_DEFAULT_MEMLEVEL: number; + declare var Z_DEFAULT_WINDOWBITS: number; + declare var Z_MAX_CHUNK: number; + declare var Z_MAX_LEVEL: number; + declare var Z_MAX_MEMLEVEL: number; + declare var Z_MAX_WINDOWBITS: number; + declare var Z_MIN_CHUNK: number; + declare var Z_MIN_LEVEL: number; + declare var Z_MIN_MEMLEVEL: number; + declare var Z_MIN_WINDOWBITS: number; + declare var constants: { + Z_NO_FLUSH: number, + Z_PARTIAL_FLUSH: number, + Z_SYNC_FLUSH: number, + Z_FULL_FLUSH: number, + Z_FINISH: number, + Z_BLOCK: number, + Z_TREES: number, + Z_OK: number, + Z_STREAM_END: number, + Z_NEED_DICT: number, + Z_ERRNO: number, + Z_STREAM_ERROR: number, + Z_DATA_ERROR: number, + Z_MEM_ERROR: number, + Z_BUF_ERROR: number, + Z_VERSION_ERROR: number, + Z_NO_COMPRESSION: number, + Z_BEST_SPEED: number, + Z_BEST_COMPRESSION: number, + Z_DEFAULT_COMPRESSION: number, + Z_FILTERED: number, + Z_HUFFMAN_ONLY: number, + Z_RLE: number, + Z_FIXED: number, + Z_DEFAULT_STRATEGY: number, + Z_BINARY: number, + Z_TEXT: number, + Z_ASCII: number, + Z_UNKNOWN: number, + Z_DEFLATED: number, + Z_NULL: number, + Z_DEFAULT_CHUNK: number, + Z_DEFAULT_LEVEL: number, + Z_DEFAULT_MEMLEVEL: number, + Z_DEFAULT_WINDOWBITS: number, + Z_MAX_CHUNK: number, + Z_MAX_LEVEL: number, + Z_MAX_MEMLEVEL: number, + Z_MAX_WINDOWBITS: number, + Z_MIN_CHUNK: number, + Z_MIN_LEVEL: number, + Z_MIN_MEMLEVEL: number, + Z_MIN_WINDOWBITS: number, + + BROTLI_DECODE: number, + BROTLI_ENCODE: number, + BROTLI_OPERATION_PROCESS: number, + BROTLI_OPERATION_FLUSH: number, + BROTLI_OPERATION_FINISH: number, + BROTLI_OPERATION_EMIT_METADATA: number, + BROTLI_PARAM_MODE: number, + BROTLI_MODE_GENERIC: number, + BROTLI_MODE_TEXT: number, + BROTLI_MODE_FONT: number, + BROTLI_DEFAULT_MODE: number, + BROTLI_PARAM_QUALITY: number, + BROTLI_MIN_QUALITY: number, + BROTLI_MAX_QUALITY: number, + BROTLI_DEFAULT_QUALITY: number, + BROTLI_PARAM_LGWIN: number, + BROTLI_MIN_WINDOW_BITS: number, + BROTLI_MAX_WINDOW_BITS: number, + BROTLI_LARGE_MAX_WINDOW_BITS: number, + BROTLI_DEFAULT_WINDOW: number, + BROTLI_PARAM_LGBLOCK: number, + BROTLI_MIN_INPUT_BLOCK_BITS: number, + BROTLI_MAX_INPUT_BLOCK_BITS: number, + BROTLI_PARAM_DISABLE_LITERAL_CONTEXT_MODELING: number, + BROTLI_PARAM_SIZE_HINT: number, + BROTLI_PARAM_LARGE_WINDOW: number, + BROTLI_PARAM_NPOSTFIX: number, + BROTLI_PARAM_NDIRECT: number, + BROTLI_DECODER_RESULT_ERROR: number, + BROTLI_DECODER_RESULT_SUCCESS: number, + BROTLI_DECODER_RESULT_NEEDS_MORE_INPUT: number, + BROTLI_DECODER_RESULT_NEEDS_MORE_OUTPUT: number, + BROTLI_DECODER_PARAM_DISABLE_RING_BUFFER_REALLOCATION: number, + BROTLI_DECODER_PARAM_LARGE_WINDOW: number, + BROTLI_DECODER_NO_ERROR: number, + BROTLI_DECODER_SUCCESS: number, + BROTLI_DECODER_NEEDS_MORE_INPUT: number, + BROTLI_DECODER_NEEDS_MORE_OUTPUT: number, + BROTLI_DECODER_ERROR_FORMAT_EXUBERANT_NIBBLE: number, + BROTLI_DECODER_ERROR_FORMAT_RESERVED: number, + BROTLI_DECODER_ERROR_FORMAT_EXUBERANT_META_NIBBLE: number, + BROTLI_DECODER_ERROR_FORMAT_SIMPLE_HUFFMAN_ALPHABET: number, + BROTLI_DECODER_ERROR_FORMAT_SIMPLE_HUFFMAN_SAME: number, + BROTLI_DECODER_ERROR_FORMAT_CL_SPACE: number, + BROTLI_DECODER_ERROR_FORMAT_HUFFMAN_SPACE: number, + BROTLI_DECODER_ERROR_FORMAT_CONTEXT_MAP_REPEAT: number, + BROTLI_DECODER_ERROR_FORMAT_BLOCK_LENGTH_1: number, + BROTLI_DECODER_ERROR_FORMAT_BLOCK_LENGTH_2: number, + BROTLI_DECODER_ERROR_FORMAT_TRANSFORM: number, + BROTLI_DECODER_ERROR_FORMAT_DICTIONARY: number, + BROTLI_DECODER_ERROR_FORMAT_WINDOW_BITS: number, + BROTLI_DECODER_ERROR_FORMAT_PADDING_1: number, + BROTLI_DECODER_ERROR_FORMAT_PADDING_2: number, + BROTLI_DECODER_ERROR_FORMAT_DISTANCE: number, + BROTLI_DECODER_ERROR_DICTIONARY_NOT_SET: number, + BROTLI_DECODER_ERROR_INVALID_ARGUMENTS: number, + BROTLI_DECODER_ERROR_ALLOC_CONTEXT_MODES: number, + BROTLI_DECODER_ERROR_ALLOC_TREE_GROUPS: number, + BROTLI_DECODER_ERROR_ALLOC_CONTEXT_MAP: number, + BROTLI_DECODER_ERROR_ALLOC_RING_BUFFER_1: number, + BROTLI_DECODER_ERROR_ALLOC_RING_BUFFER_2: number, + BROTLI_DECODER_ERROR_ALLOC_BLOCK_TYPE_TREES: number, + BROTLI_DECODER_ERROR_UNREACHABL: number, + ... + }; + declare var codes: { + Z_OK: number, + Z_STREAM_END: number, + Z_NEED_DICT: number, + Z_ERRNO: number, + Z_STREAM_ERROR: number, + Z_DATA_ERROR: number, + Z_MEM_ERROR: number, + Z_BUF_ERROR: number, + Z_VERSION_ERROR: number, + ... + }; + declare class Zlib extends stream$Duplex { + // TODO + } + declare class BrotliCompress extends Zlib {} + declare class BrotliDecompress extends Zlib {} + declare class Deflate extends Zlib {} + declare class Inflate extends Zlib {} + declare class Gzip extends Zlib {} + declare class Gunzip extends Zlib {} + declare class DeflateRaw extends Zlib {} + declare class InflateRaw extends Zlib {} + declare class Unzip extends Zlib {} + declare function createBrotliCompress( + options?: zlib$brotliOptions + ): BrotliCompress; + declare function createBrotliDecompress( + options?: zlib$brotliOptions + ): BrotliDecompress; + declare function createDeflate(options?: zlib$options): Deflate; + declare function createInflate(options?: zlib$options): Inflate; + declare function createDeflateRaw(options?: zlib$options): DeflateRaw; + declare function createInflateRaw(options?: zlib$options): InflateRaw; + declare function createGzip(options?: zlib$options): Gzip; + declare function createGunzip(options?: zlib$options): Gunzip; + declare function createUnzip(options?: zlib$options): Unzip; + declare var brotliCompress: zlib$brotliAsyncFn; + declare var brotliCompressSync: zlib$brotliSyncFn; + declare var brotliDeompress: zlib$brotliAsyncFn; + declare var brotliDecompressSync: zlib$brotliSyncFn; + declare var deflate: zlib$asyncFn; + declare var deflateSync: zlib$syncFn; + declare var gzip: zlib$asyncFn; + declare var gzipSync: zlib$syncFn; + declare var deflateRaw: zlib$asyncFn; + declare var deflateRawSync: zlib$syncFn; + declare var unzip: zlib$asyncFn; + declare var unzipSync: zlib$syncFn; + declare var inflate: zlib$asyncFn; + declare var inflateSync: zlib$syncFn; + declare var gunzip: zlib$asyncFn; + declare var gunzipSync: zlib$syncFn; + declare var inflateRaw: zlib$asyncFn; + declare var inflateRawSync: zlib$syncFn; +} + +declare module 'assert' { + declare class AssertionError extends Error {} + declare type AssertStrict = { + (value: any, message?: string): void, + ok(value: any, message?: string): void, + fail(message?: string | Error): void, + // deprecated since v10.15 + fail(actual: any, expected: any, message: string, operator: string): void, + equal(actual: any, expected: any, message?: string): void, + notEqual(actual: any, expected: any, message?: string): void, + deepEqual(actual: any, expected: any, message?: string): void, + notDeepEqual(actual: any, expected: any, message?: string): void, + throws( + block: Function, + error?: Function | RegExp | ((err: any) => boolean), + message?: string + ): void, + doesNotThrow(block: Function, message?: string): void, + ifError(value: any): void, + AssertionError: typeof AssertionError, + strict: AssertStrict, + ... + }; + declare module.exports: { + (value: any, message?: string): void, + ok(value: any, message?: string): void, + fail(message?: string | Error): void, + // deprecated since v10.15 + fail(actual: any, expected: any, message: string, operator: string): void, + equal(actual: any, expected: any, message?: string): void, + notEqual(actual: any, expected: any, message?: string): void, + deepEqual(actual: any, expected: any, message?: string): void, + notDeepEqual(actual: any, expected: any, message?: string): void, + strictEqual(actual: any, expected: any, message?: string): void, + notStrictEqual(actual: any, expected: any, message?: string): void, + deepStrictEqual(actual: any, expected: any, message?: string): void, + notDeepStrictEqual(actual: any, expected: any, message?: string): void, + throws( + block: Function, + error?: Function | RegExp | ((err: any) => boolean), + message?: string + ): void, + doesNotThrow(block: Function, message?: string): void, + ifError(value: any): void, + AssertionError: typeof AssertionError, + strict: AssertStrict, + ... + }; +} + +type HeapCodeStatistics = { + code_and_metadata_size: number, + bytecode_and_metadata_size: number, + external_script_source_size: number, + ... +}; + +type HeapStatistics = { + total_heap_size: number, + total_heap_size_executable: number, + total_physical_size: number, + total_available_size: number, + used_heap_size: number, + heap_size_limit: number, + malloced_memory: number, + peak_malloced_memory: number, + does_zap_garbage: 0 | 1, + number_of_native_contexts: number, + number_of_detached_contexts: number, + ... +}; + +type HeapSpaceStatistics = { + space_name: string, + space_size: number, + space_used_size: number, + space_available_size: number, + physical_space_size: number, + ... +}; + +// Adapted from DefinitelyTyped for Node v14: +// https://github.com/DefinitelyTyped/DefinitelyTyped/blob/dea4d99dc302a0b0a25270e46e72c1fe9b741a17/types/node/v14/v8.d.ts +declare module 'v8' { + /** + * Returns an integer representing a "version tag" derived from the V8 version, command line flags and detected CPU features. + * This is useful for determining whether a vm.Script cachedData buffer is compatible with this instance of V8. + */ + declare function cachedDataVersionTag(): number; + + /** + * Generates a snapshot of the current V8 heap and returns a Readable + * Stream that may be used to read the JSON serialized representation. + * This conversation was marked as resolved by joyeecheung + * This JSON stream format is intended to be used with tools such as + * Chrome DevTools. The JSON schema is undocumented and specific to the + * V8 engine, and may change from one version of V8 to the next. + */ + declare function getHeapSnapshot(): stream$Readable; + + /** + * + * @param fileName The file path where the V8 heap snapshot is to be + * saved. If not specified, a file name with the pattern + * `'Heap-${yyyymmdd}-${hhmmss}-${pid}-${thread_id}.heapsnapshot'` will be + * generated, where `{pid}` will be the PID of the Node.js process, + * `{thread_id}` will be `0` when `writeHeapSnapshot()` is called from + * the main Node.js thread or the id of a worker thread. + */ + declare function writeHeapSnapshot(fileName?: string): string; + + declare function getHeapCodeStatistics(): HeapCodeStatistics; + + declare function getHeapStatistics(): HeapStatistics; + declare function getHeapSpaceStatistics(): Array; + declare function setFlagsFromString(flags: string): void; + + declare class Serializer { + constructor(): void; + + /** + * Writes out a header, which includes the serialization format version. + */ + writeHeader(): void; + + /** + * Serializes a JavaScript value and adds the serialized representation to the internal buffer. + * This throws an error if value cannot be serialized. + */ + writeValue(val: any): boolean; + + /** + * Returns the stored internal buffer. + * This serializer should not be used once the buffer is released. + * Calling this method results in undefined behavior if a previous write has failed. + */ + releaseBuffer(): Buffer; + + /** + * Marks an ArrayBuffer as having its contents transferred out of band.\ + * Pass the corresponding ArrayBuffer in the deserializing context to deserializer.transferArrayBuffer(). + */ + transferArrayBuffer(id: number, arrayBuffer: ArrayBuffer): void; + + /** + * Write a raw 32-bit unsigned integer. + */ + writeUint32(value: number): void; + + /** + * Write a raw 64-bit unsigned integer, split into high and low 32-bit parts. + */ + writeUint64(hi: number, lo: number): void; + + /** + * Write a JS number value. + */ + writeDouble(value: number): void; + + /** + * Write raw bytes into the serializer’s internal buffer. + * The deserializer will require a way to compute the length of the buffer. + */ + writeRawBytes(buffer: Buffer | $TypedArray | DataView): void; + } + + /** + * A subclass of `Serializer` that serializes `TypedArray` (in particular `Buffer`) and `DataView` objects as host objects, + * and only stores the part of their underlying `ArrayBuffers` that they are referring to. + */ + declare class DefaultSerializer extends Serializer {} + + declare class Deserializer { + constructor(data: Buffer | $TypedArray | DataView): void; + + /** + * Reads and validates a header (including the format version). + * May, for example, reject an invalid or unsupported wire format. + * In that case, an Error is thrown. + */ + readHeader(): boolean; + + /** + * Deserializes a JavaScript value from the buffer and returns it. + */ + readValue(): any; + + /** + * Marks an ArrayBuffer as having its contents transferred out of band. + * Pass the corresponding `ArrayBuffer` in the serializing context to serializer.transferArrayBuffer() + * (or return the id from serializer._getSharedArrayBufferId() in the case of SharedArrayBuffers). + */ + transferArrayBuffer(id: number, arrayBuffer: ArrayBuffer): void; + + /** + * Reads the underlying wire format version. + * Likely mostly to be useful to legacy code reading old wire format versions. + * May not be called before .readHeader(). + */ + getWireFormatVersion(): number; + + /** + * Read a raw 32-bit unsigned integer and return it. + */ + readUint32(): number; + + /** + * Read a raw 64-bit unsigned integer and return it as an array [hi, lo] with two 32-bit unsigned integer entries. + */ + readUint64(): [number, number]; + + /** + * Read a JS number value. + */ + readDouble(): number; + + /** + * Read raw bytes from the deserializer’s internal buffer. + * The length parameter must correspond to the length of the buffer that was passed to serializer.writeRawBytes(). + */ + readRawBytes(length: number): Buffer; + } + + /** + * A subclass of `Serializer` that serializes `TypedArray` (in particular `Buffer`) and `DataView` objects as host objects, + * and only stores the part of their underlying `ArrayBuffers` that they are referring to. + */ + declare class DefaultDeserializer extends Deserializer {} + + /** + * Uses a `DefaultSerializer` to serialize value into a buffer. + */ + declare function serialize(value: any): Buffer; + + /** + * Uses a `DefaultDeserializer` with default options to read a JS value from a buffer. + */ + declare function deserialize(data: Buffer | $TypedArray | DataView): any; +} + +type repl$DefineCommandOptions = (...args: Array) => void | { + action: (...args: Array) => void, + help?: string, + ... +}; + +declare class $SymbolReplModeMagic mixins Symbol {} +declare class $SymbolReplModeSloppy mixins Symbol {} +declare class $SymbolReplModeStrict mixins Symbol {} + +declare module 'repl' { + declare var REPL_MODE_MAGIC: $SymbolReplModeMagic; + declare var REPL_MODE_SLOPPY: $SymbolReplModeSloppy; + declare var REPL_MODE_STRICT: $SymbolReplModeStrict; + + declare class REPLServer extends readline$Interface { + context: vm$Context; + defineCommand(command: string, options: repl$DefineCommandOptions): void; + displayPrompt(preserveCursor?: boolean): void; + } + + declare function start(prompt: string): REPLServer; + declare function start(options: { + prompt?: string, + input?: stream$Readable, + output?: stream$Writable, + terminal?: boolean, + eval?: Function, + useColors?: boolean, + useGlobal?: boolean, + ignoreUndefined?: boolean, + writer?: (object: any, options?: util$InspectOptions) => string, + completer?: readline$InterfaceCompleter, + replMode?: + | $SymbolReplModeMagic + | $SymbolReplModeSloppy + | $SymbolReplModeStrict, + breakEvalOnSigint?: boolean, + ... + }): REPLServer; + + declare class Recoverable extends SyntaxError { + constructor(err: Error): void; + } +} + +declare module 'inspector' { + declare function open(port?: number, host?: string, wait?: boolean): void; + + declare function close(): void; + declare function url(): string | void; + declare var console: Object; + declare function waitForDebugger(): void; + + declare class Session extends events$EventEmitter { + constructor(): void; + connect(): void; + connectToMainThread(): void; + disconnect(): void; + post(method: string, params?: Object, callback?: Function): void; + } +} + +/* globals: https://nodejs.org/api/globals.html */ + +type process$CPUUsage = { + user: number, + system: number, + ... +}; + +declare class Process extends events$EventEmitter { + abort(): void; + allowedNodeEnvironmentFlags: Set; + arch: string; + argv: Array; + chdir(directory: string): void; + config: Object; + connected: boolean; + cpuUsage(previousValue?: process$CPUUsage): process$CPUUsage; + cwd(): string; + disconnect?: () => void; + domain?: domain$Domain; + env: {[key: string]: string | void, ...}; + emitWarning(warning: string | Error): void; + emitWarning( + warning: string, + typeOrCtor: string | ((...empty) => mixed) + ): void; + emitWarning( + warning: string, + type: string, + codeOrCtor: string | ((...empty) => mixed) + ): void; + emitWarning( + warning: string, + type: string, + code: string, + ctor?: (...empty) => mixed + ): void; + execArgv: Array; + execPath: string; + exit(code?: number): empty; + exitCode?: number; + getegid?: () => number; + geteuid?: () => number; + getgid?: () => number; + getgroups?: () => Array; + getuid?: () => number; + hrtime: { + (time?: [number, number]): [number, number], + bigint: () => bigint, + ... + }; + initgroups?: (user: number | string, extra_group: number | string) => void; + kill(pid: number, signal?: string | number): void; + mainModule: Object; + memoryUsage(): { + arrayBuffers: number, + rss: number, + heapTotal: number, + heapUsed: number, + external: number, + ... + }; + nextTick: (cb: (...T) => mixed, ...T) => void; + pid: number; + platform: string; + release: { + name: string, + lts?: string, + sourceUrl: string, + headersUrl: string, + libUrl: string, + ... + }; + send?: ( + message: any, + sendHandleOrCallback?: net$Socket | net$Server | Function, + callback?: Function + ) => void; + setegid?: (id: number | string) => void; + seteuid?: (id: number | string) => void; + setgid?: (id: number | string) => void; + setgroups?: (groups: Array) => void; + setuid?: (id: number | string) => void; + stderr: stream$Writable | tty$WriteStream; + stdin: stream$Readable | tty$ReadStream; + stdout: stream$Writable | tty$WriteStream; + title: string; + umask(mask?: number): number; + uptime(): number; + version: string; + versions: { + [key: string]: ?string, + node: string, + v8: string, + ... + }; +} +declare var process: Process; + +declare var __filename: string; +declare var __dirname: string; + +declare function setImmediate( + callback: (...args: Array) => mixed, + ...args: Array +): Object; +declare function clearImmediate(immediateObject: any): Object; + +// https://nodejs.org/api/esm.html#node-imports + +declare module 'node:assert' { + declare module.exports: $Exports<'assert'>; +} + +declare module 'node:assert/strict' { + declare module.exports: $Exports<'assert'>['strict']; +} + +declare module 'node:events' { + declare module.exports: $Exports<'events'>; +} + +declare module 'node:fs' { + declare module.exports: $Exports<'fs'>; +} + +declare module 'node:os' { + declare module.exports: $Exports<'os'>; +} + +declare module 'fs/promises' { + declare module.exports: $Exports<'fs'>['promises']; +} + +declare module 'node:fs/promises' { + declare module.exports: $Exports<'fs'>['promises']; +} + +declare module 'node:path' { + declare module.exports: $Exports<'path'>; +} + +declare module 'process' { + declare module.exports: Process; +} + +declare module 'node:process' { + declare module.exports: $Exports<'process'>; +} + +declare module 'node:util' { + declare module.exports: $Exports<'util'>; +} + +declare module 'node:url' { + declare module.exports: $Exports<'url'>; +} + +declare module 'worker_threads' { + declare var isMainThread: boolean; + declare var parentPort: null | MessagePort; + declare var threadId: number; + declare var workerData: any; + + declare class MessageChannel { + +port1: MessagePort; + +port2: MessagePort; + } + + declare class MessagePort extends events$EventEmitter { + close(): void; + postMessage( + value: any, + transferList?: Array + ): void; + ref(): void; + unref(): void; + start(): void; + + addListener(event: 'close', listener: () => void): this; + addListener(event: 'message', listener: (value: any) => void): this; + addListener( + event: string | Symbol, + listener: (...args: any[]) => void + ): this; + + emit(event: 'close'): boolean; + emit(event: 'message', value: any): boolean; + emit(event: string | Symbol, ...args: any[]): boolean; + + on(event: 'close', listener: () => void): this; + on(event: 'message', listener: (value: any) => void): this; + on(event: string | Symbol, listener: (...args: any[]) => void): this; + + once(event: 'close', listener: () => void): this; + once(event: 'message', listener: (value: any) => void): this; + once(event: string | Symbol, listener: (...args: any[]) => void): this; + + prependListener(event: 'close', listener: () => void): this; + prependListener(event: 'message', listener: (value: any) => void): this; + prependListener( + event: string | Symbol, + listener: (...args: any[]) => void + ): this; + + prependOnceListener(event: 'close', listener: () => void): this; + prependOnceListener(event: 'message', listener: (value: any) => void): this; + prependOnceListener( + event: string | Symbol, + listener: (...args: any[]) => void + ): this; + + removeListener(event: 'close', listener: () => void): this; + removeListener(event: 'message', listener: (value: any) => void): this; + removeListener( + event: string | Symbol, + listener: (...args: any[]) => void + ): this; + + off(event: 'close', listener: () => void): this; + off(event: 'message', listener: (value: any) => void): this; + off(event: string | Symbol, listener: (...args: any[]) => void): this; + } + + declare type WorkerOptions = {| + env?: Object, + eval?: boolean, + workerData?: any, + stdin?: boolean, + stdout?: boolean, + stderr?: boolean, + execArgv?: string[], + |}; + + declare class Worker extends events$EventEmitter { + +stdin: stream$Writable | null; + +stdout: stream$Readable; + +stderr: stream$Readable; + +threadId: number; + + constructor(filename: string, options?: WorkerOptions): void; + + postMessage( + value: any, + transferList?: Array + ): void; + ref(): void; + unref(): void; + terminate(callback?: (err: Error, exitCode: number) => void): void; + /** + * Transfer a `MessagePort` to a different `vm` Context. The original `port` + * object will be rendered unusable, and the returned `MessagePort` instance will + * take its place. + * + * The returned `MessagePort` will be an object in the target context, and will + * inherit from its global `Object` class. Objects passed to the + * `port.onmessage()` listener will also be created in the target context + * and inherit from its global `Object` class. + * + * However, the created `MessagePort` will no longer inherit from + * `EventEmitter`, and only `port.onmessage()` can be used to receive + * events using it. + */ + moveMessagePortToContext( + port: MessagePort, + context: vm$Context + ): MessagePort; + + addListener(event: 'error', listener: (err: Error) => void): this; + addListener(event: 'exit', listener: (exitCode: number) => void): this; + addListener(event: 'message', listener: (value: any) => void): this; + addListener(event: 'online', listener: () => void): this; + addListener( + event: string | Symbol, + listener: (...args: any[]) => void + ): this; + + emit(event: 'error', err: Error): boolean; + emit(event: 'exit', exitCode: number): boolean; + emit(event: 'message', value: any): boolean; + emit(event: 'online'): boolean; + emit(event: string | Symbol, ...args: any[]): boolean; + + on(event: 'error', listener: (err: Error) => void): this; + on(event: 'exit', listener: (exitCode: number) => void): this; + on(event: 'message', listener: (value: any) => void): this; + on(event: 'online', listener: () => void): this; + on(event: string | Symbol, listener: (...args: any[]) => void): this; + + once(event: 'error', listener: (err: Error) => void): this; + once(event: 'exit', listener: (exitCode: number) => void): this; + once(event: 'message', listener: (value: any) => void): this; + once(event: 'online', listener: () => void): this; + once(event: string | Symbol, listener: (...args: any[]) => void): this; + + prependListener(event: 'error', listener: (err: Error) => void): this; + prependListener(event: 'exit', listener: (exitCode: number) => void): this; + prependListener(event: 'message', listener: (value: any) => void): this; + prependListener(event: 'online', listener: () => void): this; + prependListener( + event: string | Symbol, + listener: (...args: any[]) => void + ): this; + + prependOnceListener(event: 'error', listener: (err: Error) => void): this; + prependOnceListener( + event: 'exit', + listener: (exitCode: number) => void + ): this; + prependOnceListener(event: 'message', listener: (value: any) => void): this; + prependOnceListener(event: 'online', listener: () => void): this; + prependOnceListener( + event: string | Symbol, + listener: (...args: any[]) => void + ): this; + + removeListener(event: 'error', listener: (err: Error) => void): this; + removeListener(event: 'exit', listener: (exitCode: number) => void): this; + removeListener(event: 'message', listener: (value: any) => void): this; + removeListener(event: 'online', listener: () => void): this; + removeListener( + event: string | Symbol, + listener: (...args: any[]) => void + ): this; + + off(event: 'error', listener: (err: Error) => void): this; + off(event: 'exit', listener: (exitCode: number) => void): this; + off(event: 'message', listener: (value: any) => void): this; + off(event: 'online', listener: () => void): this; + off(event: string | Symbol, listener: (...args: any[]) => void): this; + } +} + +declare module 'node:worker_threads' { + declare module.exports: $Exports<'worker_threads'>; +} diff --git a/flow-typed/environments/serviceworkers.js b/flow-typed/environments/serviceworkers.js new file mode 100644 index 0000000000000..86479648e2e88 --- /dev/null +++ b/flow-typed/environments/serviceworkers.js @@ -0,0 +1,248 @@ +// flow-typed signature: f6bda44505d6258bae702a65ee2878f2 +// flow-typed version: 840509ea9d/serviceworkers/flow_>=v0.261.x + +type FrameType = 'auxiliary' | 'top-level' | 'nested' | 'none'; +type VisibilityState = 'hidden' | 'visible' | 'prerender' | 'unloaded'; + +declare class WindowClient extends Client { + visibilityState: VisibilityState; + focused: boolean; + focus(): Promise; + navigate(url: string): Promise; +} + +declare class Client { + id: string; + reserved: boolean; + url: string; + frameType: FrameType; + postMessage(message: any, transfer?: Iterator | Array): void; +} + +declare class ExtendableEvent extends Event { + waitUntil(f: Promise): void; +} + +type NotificationEvent$Init = { + ...Event$Init, + notification: Notification, + action?: string, + ... +}; + +declare class NotificationEvent extends ExtendableEvent { + constructor(type: string, eventInitDict?: NotificationEvent$Init): void; + +notification: Notification; + +action: string; +} + +type ForeignFetchOptions = { + scopes: Iterator, + origins: Iterator, + ... +}; + +declare class InstallEvent extends ExtendableEvent { + registerForeignFetch(options: ForeignFetchOptions): void; +} + +declare class FetchEvent extends ExtendableEvent { + request: Request; + clientId: string; + isReload: boolean; + respondWith(response: Response | Promise): void; + preloadResponse: Promise; +} + +type ClientType = 'window' | 'worker' | 'sharedworker' | 'all'; +type ClientQueryOptions = { + includeUncontrolled?: boolean, + includeReserved?: boolean, + type?: ClientType, + ... +}; + +declare class Clients { + get(id: string): Promise; + matchAll(options?: ClientQueryOptions): Promise>; + openWindow(url: string): Promise; + claim(): Promise; +} + +type ServiceWorkerState = + | 'installing' + | 'installed' + | 'activating' + | 'activated' + | 'redundant'; + +declare class ServiceWorker extends EventTarget { + scriptURL: string; + state: ServiceWorkerState; + + postMessage(message: any, transfer?: Iterator): void; + + onstatechange?: EventHandler; +} + +declare class NavigationPreloadState { + enabled: boolean; + headerValue: string; +} + +declare class NavigationPreloadManager { + enable: Promise; + disable: Promise; + setHeaderValue(value: string): Promise; + getState: Promise; +} + +type PushSubscriptionOptions = { + userVisibleOnly?: boolean, + applicationServerKey?: string | ArrayBuffer | $ArrayBufferView, + ... +}; + +declare class PushSubscriptionJSON { + endpoint: string; + expirationTime: number | null; + keys: {[string]: string, ...}; +} + +declare class PushSubscription { + +endpoint: string; + +expirationTime: number | null; + +options: PushSubscriptionOptions; + getKey(name: string): ArrayBuffer | null; + toJSON(): PushSubscriptionJSON; + unsubscribe(): Promise; +} + +declare class PushManager { + +supportedContentEncodings: Array; + subscribe(options?: PushSubscriptionOptions): Promise; + getSubscription(): Promise; + permissionState( + options?: PushSubscriptionOptions + ): Promise<'granted' | 'denied' | 'prompt'>; +} + +type ServiceWorkerUpdateViaCache = 'imports' | 'all' | 'none'; + +type GetNotificationOptions = { + tag?: string, + ... +}; + +declare class ServiceWorkerRegistration extends EventTarget { + +installing: ?ServiceWorker; + +waiting: ?ServiceWorker; + +active: ?ServiceWorker; + +navigationPreload: NavigationPreloadManager; + +scope: string; + +updateViaCache: ServiceWorkerUpdateViaCache; + +pushManager: PushManager; + + getNotifications?: ( + filter?: GetNotificationOptions + ) => Promise<$ReadOnlyArray>; + showNotification?: ( + title: string, + options?: NotificationOptions + ) => Promise; + update(): Promise; + unregister(): Promise; + + onupdatefound?: EventHandler; +} + +type WorkerType = 'classic' | 'module'; + +type RegistrationOptions = { + scope?: string, + type?: WorkerType, + updateViaCache?: ServiceWorkerUpdateViaCache, + ... +}; + +declare class ServiceWorkerContainer extends EventTarget { + +controller: ?ServiceWorker; + +ready: Promise; + + getRegistration( + clientURL?: string + ): Promise; + getRegistrations(): Promise>; + register( + scriptURL: string | TrustedScriptURL, + options?: RegistrationOptions + ): Promise; + startMessages(): void; + + oncontrollerchange?: EventHandler; + onmessage?: EventHandler; + onmessageerror?: EventHandler; +} + +/** + * This feature has been removed from the Web standards. + */ +declare class ServiceWorkerMessageEvent extends Event { + data: any; + lastEventId: string; + origin: string; + ports: Array; + source: ?(ServiceWorker | MessagePort); +} + +declare class ExtendableMessageEvent extends ExtendableEvent { + data: any; + lastEventId: string; + origin: string; + ports: Array; + source: ?(ServiceWorker | MessagePort); +} + +type CacheQueryOptions = { + ignoreSearch?: boolean, + ignoreMethod?: boolean, + ignoreVary?: boolean, + cacheName?: string, + ... +}; + +declare class Cache { + match(request: RequestInfo, options?: CacheQueryOptions): Promise; + matchAll( + request: RequestInfo, + options?: CacheQueryOptions + ): Promise>; + add(request: RequestInfo): Promise; + addAll(requests: Array): Promise; + put(request: RequestInfo, response: Response): Promise; + delete(request: RequestInfo, options?: CacheQueryOptions): Promise; + keys( + request?: RequestInfo, + options?: CacheQueryOptions + ): Promise>; +} + +declare class CacheStorage { + match(request: RequestInfo, options?: CacheQueryOptions): Promise; + has(cacheName: string): Promise; + open(cacheName: string): Promise; + delete(cacheName: string): Promise; + keys(): Promise>; +} + +// Service worker global scope +// https://www.w3.org/TR/service-workers/#service-worker-global-scope +declare var clients: Clients; +declare var caches: CacheStorage; +declare var registration: ServiceWorkerRegistration; +declare function skipWaiting(): Promise; +declare var onactivate: ?EventHandler; +declare var oninstall: ?EventHandler; +declare var onfetch: ?EventHandler; +declare var onforeignfetch: ?EventHandler; +declare var onmessage: ?EventHandler; diff --git a/flow-typed/environments/streams.js b/flow-typed/environments/streams.js new file mode 100644 index 0000000000000..17bfae29e612c --- /dev/null +++ b/flow-typed/environments/streams.js @@ -0,0 +1,136 @@ +// flow-typed signature: e6e6768618776352dd676f63502aea4d +// flow-typed version: 40e7dfcbd5/streams/flow_>=v0.261.x + +type TextEncodeOptions = {options?: boolean, ...}; + +declare class ReadableStreamController { + constructor( + stream: ReadableStream, + underlyingSource: UnderlyingSource, + size: number, + highWaterMark: number + ): void; + + desiredSize: number; + + close(): void; + enqueue(chunk: any): void; + error(error: Error): void; +} + +declare class ReadableStreamBYOBRequest { + constructor(controller: ReadableStreamController, view: $TypedArray): void; + + view: $TypedArray; + + respond(bytesWritten: number): ?any; + respondWithNewView(view: $TypedArray): ?any; +} + +declare class ReadableByteStreamController extends ReadableStreamController { + constructor( + stream: ReadableStream, + underlyingSource: UnderlyingSource, + highWaterMark: number + ): void; + + byobRequest: ReadableStreamBYOBRequest; +} + +declare class ReadableStreamReader { + constructor(stream: ReadableStream): void; + + closed: boolean; + + cancel(reason: string): void; + read(): Promise<{ + value: ?any, + done: boolean, + ... + }>; + releaseLock(): void; +} + +declare interface UnderlyingSource { + autoAllocateChunkSize?: number; + type?: string; + + start?: (controller: ReadableStreamController) => ?Promise; + pull?: (controller: ReadableStreamController) => ?Promise; + cancel?: (reason: string) => ?Promise; +} + +declare class TransformStream { + readable: ReadableStream; + writable: WritableStream; +} + +interface PipeThroughTransformStream { + readable: ReadableStream; + writable: WritableStream; +} + +type PipeToOptions = { + preventClose?: boolean, + preventAbort?: boolean, + preventCancel?: boolean, + ... +}; + +type QueuingStrategy = { + highWaterMark: number, + size(chunk: ?any): number, + ... +}; + +declare class ReadableStream { + constructor( + underlyingSource: ?UnderlyingSource, + queuingStrategy: ?QueuingStrategy + ): void; + + locked: boolean; + + cancel(reason: string): void; + getReader(): ReadableStreamReader; + pipeThrough(transform: PipeThroughTransformStream, options: ?any): void; + pipeTo(dest: WritableStream, options: ?PipeToOptions): Promise; + tee(): [ReadableStream, ReadableStream]; +} + +declare interface WritableStreamController { + error(error: Error): void; +} + +declare interface UnderlyingSink { + autoAllocateChunkSize?: number; + type?: string; + + abort?: (reason: string) => ?Promise; + close?: (controller: WritableStreamController) => ?Promise; + start?: (controller: WritableStreamController) => ?Promise; + write?: (chunk: any, controller: WritableStreamController) => ?Promise; +} + +declare interface WritableStreamWriter { + closed: Promise; + desiredSize?: number; + ready: Promise; + + abort(reason: string): ?Promise; + close(): Promise; + releaseLock(): void; + write(chunk: any): Promise; +} + +declare class WritableStream { + constructor( + underlyingSink: ?UnderlyingSink, + queuingStrategy: QueuingStrategy + ): void; + + locked: boolean; + + abort(reason: string): void; + getWriter(): WritableStreamWriter; +} diff --git a/flow-typed/environments/web-animations.js b/flow-typed/environments/web-animations.js new file mode 100644 index 0000000000000..ac059631f7c35 --- /dev/null +++ b/flow-typed/environments/web-animations.js @@ -0,0 +1,193 @@ +// flow-typed signature: 4631a74b6a0e6a1b4de2ba8c7bb141d6 +// flow-typed version: 3e51657e95/web-animations/flow_>=v0.261.x + +// https://www.w3.org/TR/web-animations-1/ + +type AnimationPlayState = 'idle' | 'running' | 'paused' | 'finished'; + +type AnimationReplaceState = 'active' | 'removed' | 'persisted'; + +type CompositeOperation = 'replace' | 'add' | 'accumulate'; + +type CompositeOperationOrAuto = 'replace' | 'add' | 'accumulate' | 'auto'; + +type FillMode = 'none' | 'forwards' | 'backwards' | 'both' | 'auto'; + +// This is actually web-animations-2 +type IterationCompositeOperation = 'replace' | 'accumulate'; + +type PlaybackDirection = + | 'normal' + | 'reverse' + | 'alternate' + | 'alternate-reverse'; + +type AnimationPlaybackEvent$Init = Event$Init & { + currentTime?: number | null, + timelineTime?: number | null, + ... +}; + +type BaseComputedKeyframe = {| + composite: CompositeOperationOrAuto, + computedOffset: number, + easing: string, + offset: number | null, +|}; + +type BaseKeyframe = {| + composite: CompositeOperationOrAuto, + easing: string, + offset: number | null, +|}; + +type BasePropertyIndexedKeyframe = {| + composite: CompositeOperationOrAuto | Array, + easing: string | Array, + offset: number | null | Array, +|}; + +type ComputedEffectTiming = {| + ...EffectTiming, + currentIteration: number | null, + progress: number | null, +|}; + +type ComputedKeyframe = { + composite: CompositeOperationOrAuto, + computedOffset: number, + easing: string, + offset: number | null, + [property: string]: string | number | null | void, + ... +}; + +type DocumentTimelineOptions = {| + originTime: number, +|}; + +type EffectTiming = {| + direction: PlaybackDirection, + easing: string, + fill: FillMode, + iterations: number, + iterationStart: number, +|}; + +type GetAnimationsOptions = {| + pseudoElement: string | null, + subtree: boolean, +|}; + +type KeyframeAnimationOptions = {| + ...KeyframeEffectOptions, + id: string, + timeline: AnimationTimeline | null, +|}; + +type KeyframeEffectOptions = {| + ...EffectTiming, + composite: CompositeOperation, + pseudoElement: string | null, +|}; + +type Keyframe = { + composite?: CompositeOperationOrAuto, + easing?: string, + offset?: number | null, + [property: string]: string | number | null | void, + ... +}; + +type OptionalEffectTiming = Partial; + +type PropertyIndexedKeyframes = { + composite?: CompositeOperationOrAuto | CompositeOperationOrAuto[], + easing?: string | string[], + offset?: number | (number | null)[], + [property: string]: + | string + | string[] + | number + | null + | (number | null)[] + | void, + ... +}; + +declare class Animation extends EventTarget { + constructor( + effect?: AnimationEffect | null, + timeline?: AnimationTimeline | null + ): void; + + id: string; + effect: AnimationEffect | null; + timeline: AnimationTimeline | null; + startTime: number | null; + currentTime: number | null; + playbackRate: number; + +playState: AnimationPlayState; + +replaceState: AnimationReplaceState; + +pending: boolean; + +ready: Promise; + +finished: Promise; + onfinish: ?(ev: AnimationPlaybackEvent) => mixed; + oncancel: ?(ev: AnimationPlaybackEvent) => mixed; + onremove: ?(ev: AnimationPlaybackEvent) => mixed; + cancel(): void; + finish(): void; + play(): void; + pause(): void; + updatePlaybackRate(playbackRate: number): void; + reverse(): void; + persist(): void; + commitStyles(): void; +} + +declare class AnimationEffect { + getTiming(): EffectTiming; + getComputedTiming(): ComputedEffectTiming; + updateTiming(timing?: OptionalEffectTiming): void; +} + +declare class AnimationPlaybackEvent extends Event { + constructor( + type: string, + animationEventInitDict?: AnimationPlaybackEvent$Init + ): void; + +currentTime: number | null; + +timelineTime: number | null; +} + +declare class AnimationTimeline { + +currentTime: number | null; +} + +declare class DocumentTimeline extends AnimationTimeline { + constructor(options?: DocumentTimelineOptions): void; +} + +declare class KeyframeEffect extends AnimationEffect { + constructor( + target: Element | null, + keyframes: Keyframe[] | PropertyIndexedKeyframes | null, + options?: number | KeyframeEffectOptions + ): void; + constructor(source: KeyframeEffect): void; + + target: Element | null; + composite: CompositeOperation; + // This is actually web-animations-2 + iterationComposite: IterationCompositeOperation; + getKeyframes(): ComputedKeyframe[]; + setKeyframes(keyframes: Keyframe[] | PropertyIndexedKeyframes | null): void; +} + +declare class mixin$Animatable { + animate( + keyframes: Keyframe[] | PropertyIndexedKeyframes | null, + options?: number | KeyframeAnimationOptions + ): Animation; + getAnimations(options?: GetAnimationsOptions): Array; +} diff --git a/flow-typed/npm/error-stack-parser_v2.x.x.js b/flow-typed/npm/error-stack-parser_v2.x.x.js new file mode 100644 index 0000000000000..d23b837af2524 --- /dev/null +++ b/flow-typed/npm/error-stack-parser_v2.x.x.js @@ -0,0 +1,60 @@ +// flow-typed signature: 132e48034ef4756600e1d98681a166b5 +// flow-typed version: c6154227d1/error-stack-parser_v2.x.x/flow_>=v0.104.x + +declare module 'error-stack-parser' { + declare interface StackFrame { + constructor(object: StackFrame): StackFrame; + + isConstructor?: boolean; + getIsConstructor(): boolean; + setIsConstructor(): void; + + isEval?: boolean; + getIsEval(): boolean; + setIsEval(): void; + + isNative?: boolean; + getIsNative(): boolean; + setIsNative(): void; + + isTopLevel?: boolean; + getIsTopLevel(): boolean; + setIsTopLevel(): void; + + columnNumber?: number; + getColumnNumber(): number; + setColumnNumber(): void; + + lineNumber?: number; + getLineNumber(): number; + setLineNumber(): void; + + fileName?: string; + getFileName(): string; + setFileName(): void; + + functionName?: string; + getFunctionName(): string; + setFunctionName(): void; + + source?: string; + getSource(): string; + setSource(): void; + + args?: any[]; + getArgs(): any[]; + setArgs(): void; + + evalOrigin?: StackFrame; + getEvalOrigin(): StackFrame; + setEvalOrigin(): void; + + toString(): string; + } + + declare class ErrorStackParser { + parse(error: Error): Array; + } + + declare module.exports: ErrorStackParser; +} diff --git a/flow-typed/npm/minimist_v1.x.x.js b/flow-typed/npm/minimist_v1.x.x.js new file mode 100644 index 0000000000000..9da29ffe60c05 --- /dev/null +++ b/flow-typed/npm/minimist_v1.x.x.js @@ -0,0 +1,27 @@ +// flow-typed signature: d48da8db828529253fc20b80747846ea +// flow-typed version: c6154227d1/minimist_v1.x.x/flow_>=v0.104.x + +declare module 'minimist' { + declare type minimistOptions = { + string?: string | Array, + boolean?: boolean | string | Array, + alias?: {[arg: string]: string | Array, ...}, + default?: {[arg: string]: any, ...}, + stopEarly?: boolean, + // TODO: Strings as keys don't work... + // '--'? boolean, + unknown?: (param: string) => boolean, + ... + }; + + declare type minimistOutput = { + [flag: string]: string | boolean, + _: Array, + ... + }; + + declare module.exports: ( + argv: Array, + opts?: minimistOptions + ) => minimistOutput; +} diff --git a/package.json b/package.json index 875986ebdaf6a..3439ed756a346 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "@babel/plugin-transform-modules-commonjs": "^7.10.4", "@babel/plugin-transform-object-super": "^7.10.4", "@babel/plugin-transform-parameters": "^7.10.5", + "@babel/plugin-transform-private-methods": "^7.10.4", "@babel/plugin-transform-react-jsx": "^7.23.4", "@babel/plugin-transform-react-jsx-development": "^7.22.5", "@babel/plugin-transform-react-jsx-source": "^7.10.5", @@ -50,6 +51,7 @@ "@typescript-eslint/parser": "^6.21.0", "abortcontroller-polyfill": "^1.7.5", "art": "0.10.1", + "babel-plugin-syntax-hermes-parser": "^0.32.0", "babel-plugin-syntax-trailing-function-commas": "^6.5.0", "chalk": "^3.0.0", "cli-table": "^0.3.1", @@ -72,14 +74,15 @@ "eslint-plugin-react-internal": "link:./scripts/eslint-rules", "fbjs-scripts": "^3.0.1", "filesize": "^6.0.1", - "flow-bin": "^0.245.2", - "flow-remove-types": "^2.245.2", + "flow-bin": "^0.279.0", + "flow-remove-types": "^2.279.0", + "flow-typed": "^4.1.1", "glob": "^7.1.6", "glob-stream": "^6.1.0", "google-closure-compiler": "^20230206.0.0", "gzip-size": "^5.1.1", - "hermes-eslint": "^0.25.1", - "hermes-parser": "^0.25.1", + "hermes-eslint": "^0.32.0", + "hermes-parser": "^0.32.0", "jest": "^29.4.2", "jest-cli": "^29.4.2", "jest-diff": "^29.4.2", @@ -91,7 +94,6 @@ "ncp": "^2.0.0", "prettier": "^3.3.3", "prettier-2": "npm:prettier@^2", - "prettier-plugin-hermes-parser": "^0.23.0", "pretty-format": "^29.4.1", "prop-types": "^15.6.2", "random-seed": "^0.3.0", @@ -125,12 +127,12 @@ "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.react-server,react-dom/index,react-dom/client,react-dom/server,react-dom.react-server,react-dom-server.node,react-dom-server-legacy.node,scheduler,react-server-dom-webpack/ --type=NODE_DEV,ESM_PROD,NODE_ES2015 && mv ./build/node_modules ./build/oss-experimental", "build-for-vt-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 --type=NODE_DEV && mv ./build/node_modules ./build/oss-experimental", + "flow-typed-install": "yarn flow-typed install --skip --skipFlowRestart --ignore-deps=dev", "linc": "node ./scripts/tasks/linc.js", "lint": "node ./scripts/tasks/eslint.js", "lint-build": "node ./scripts/rollup/validate/index.js", "extract-errors": "node scripts/error-codes/extract-errors.js", "postinstall": "node ./scripts/flow/createFlowConfigs.js", - "pretest": "./scripts/react-compiler/build-compiler.sh && ./scripts/react-compiler/link-compiler.sh", "test": "node ./scripts/jest/jest-cli.js", "test-stable": "node ./scripts/jest/jest-cli.js --release-channel=stable", "test-www": "node ./scripts/jest/jest-cli.js --release-channel=www-modern", diff --git a/packages/eslint-plugin-react-hooks/README.md b/packages/eslint-plugin-react-hooks/README.md index 10020afd61038..20d32fe9fd1bc 100644 --- a/packages/eslint-plugin-react-hooks/README.md +++ b/packages/eslint-plugin-react-hooks/README.md @@ -22,15 +22,22 @@ yarn add eslint-plugin-react-hooks --dev #### >= 6.0.0 -For users of 6.0 and beyond, simply add the `recommended` config. +For users of 6.0 and beyond, add the `recommended` config. ```js -import * as reactHooks from 'eslint-plugin-react-hooks'; +// eslint.config.js +import reactHooks from 'eslint-plugin-react-hooks'; +import { defineConfig } from 'eslint/config'; -export default [ - // ... - reactHooks.configs.recommended, -]; +export default defineConfig([ + { + files: ["src/**/*.{js,jsx,ts,tsx}"], + plugins: { + 'react-hooks': reactHooks, + }, + extends: ['react-hooks/recommended'], + }, +]); ``` #### 5.2.0 @@ -38,12 +45,18 @@ export default [ For users of 5.2.0 (the first version with flat config support), add the `recommended-latest` config. ```js -import * as reactHooks from 'eslint-plugin-react-hooks'; +import reactHooks from 'eslint-plugin-react-hooks'; +import { defineConfig } from 'eslint/config'; -export default [ - // ... - reactHooks.configs['recommended-latest'], -]; +export default defineConfig([ + { + files: ["src/**/*.{js,jsx,ts,tsx}"], + plugins: { + 'react-hooks': reactHooks, + }, + extends: ['react-hooks/recommended-latest'], + }, +]); ``` ### Legacy Config (.eslintrc) diff --git a/packages/eslint-plugin-react-hooks/__tests__/ESLintRuleExhaustiveDeps-test.js b/packages/eslint-plugin-react-hooks/__tests__/ESLintRuleExhaustiveDeps-test.js index 055474ea321e0..812c2010a042d 100644 --- a/packages/eslint-plugin-react-hooks/__tests__/ESLintRuleExhaustiveDeps-test.js +++ b/packages/eslint-plugin-react-hooks/__tests__/ESLintRuleExhaustiveDeps-test.js @@ -12,7 +12,8 @@ const ESLintTesterV7 = require('eslint-v7').RuleTester; const ESLintTesterV9 = require('eslint-v9').RuleTester; const ReactHooksESLintPlugin = require('eslint-plugin-react-hooks'); -const ReactHooksESLintRule = ReactHooksESLintPlugin.rules['exhaustive-deps']; +const ReactHooksESLintRule = + ReactHooksESLintPlugin.default.rules['exhaustive-deps']; /** * A string template tag that removes padding from the left side of multi-line strings @@ -515,6 +516,22 @@ const tests = { `, options: [{additionalHooks: 'useCustomEffect'}], }, + { + // behaves like no deps + code: normalizeIndent` + function MyComponent(props) { + useSpecialEffect(() => { + console.log(props.foo); + }, null); + } + `, + options: [ + { + additionalHooks: 'useSpecialEffect', + experimental_autoDependenciesHooks: ['useSpecialEffect'], + }, + ], + }, { code: normalizeIndent` function MyComponent(props) { @@ -1470,6 +1487,38 @@ const tests = { }, ], invalid: [ + { + code: normalizeIndent` + function MyComponent(props) { + useSpecialEffect(() => { + console.log(props.foo); + }, null); + } + `, + options: [{additionalHooks: 'useSpecialEffect'}], + errors: [ + { + message: + "React Hook useSpecialEffect was passed a dependency list that is not an array literal. This means we can't statically verify whether you've passed the correct dependencies.", + }, + { + message: + "React Hook useSpecialEffect has a missing dependency: 'props.foo'. Either include it or remove the dependency array.", + suggestions: [ + { + desc: 'Update the dependencies array to be: [props.foo]', + output: normalizeIndent` + function MyComponent(props) { + useSpecialEffect(() => { + console.log(props.foo); + }, [props.foo]); + } + `, + }, + ], + }, + ], + }, { code: normalizeIndent` function MyComponent(props) { @@ -7687,6 +7736,9 @@ if (__EXPERIMENTAL__) { useEffect(() => { onStuff(); }, []); + React.useEffect(() => { + onStuff(); + }, []); } `, }, @@ -7703,6 +7755,9 @@ if (__EXPERIMENTAL__) { useEffect(() => { onStuff(); }, [onStuff]); + React.useEffect(() => { + onStuff(); + }, [onStuff]); } `, errors: [ @@ -7721,6 +7776,32 @@ if (__EXPERIMENTAL__) { useEffect(() => { onStuff(); }, []); + React.useEffect(() => { + onStuff(); + }, [onStuff]); + } + `, + }, + ], + }, + { + message: + 'Functions returned from `useEffectEvent` must not be included in the dependency array. ' + + 'Remove `onStuff` from the list.', + suggestions: [ + { + desc: 'Remove the dependency `onStuff`', + output: normalizeIndent` + function MyComponent({ theme }) { + const onStuff = useEffectEvent(() => { + showNotification(theme); + }); + useEffect(() => { + onStuff(); + }, [onStuff]); + React.useEffect(() => { + onStuff(); + }, []); } `, }, @@ -7746,6 +7827,34 @@ const testsFlow = { }, ], invalid: [ + { + code: normalizeIndent` + hook useExample(a) { + useEffect(() => { + console.log(a); + }, []); + } + `, + errors: [ + { + message: + "React Hook useEffect has a missing dependency: 'a'. " + + 'Either include it or remove the dependency array.', + suggestions: [ + { + desc: 'Update the dependencies array to be: [a]', + output: normalizeIndent` + hook useExample(a) { + useEffect(() => { + console.log(a); + }, [a]); + } + `, + }, + ], + }, + ], + }, { code: normalizeIndent` function Foo() { @@ -7793,6 +7902,24 @@ const testsTypescript = { } `, }, + { + code: normalizeIndent` + function MyComponent() { + const [state, setState] = React.useState(0); + + useSpecialEffect(() => { + const someNumber: typeof state = 2; + setState(prevState => prevState + someNumber); + }) + } + `, + options: [ + { + additionalHooks: 'useSpecialEffect', + experimental_autoDependenciesHooks: ['useSpecialEffect'], + }, + ], + }, { code: normalizeIndent` function App() { @@ -8148,6 +8275,48 @@ const testsTypescript = { function MyComponent() { const [state, setState] = React.useState(0); + useSpecialEffect(() => { + const someNumber: typeof state = 2; + setState(prevState => prevState + someNumber + state); + }, []) + } + `, + options: [ + { + additionalHooks: 'useSpecialEffect', + experimental_autoDependenciesHooks: ['useSpecialEffect'], + }, + ], + errors: [ + { + message: + "React Hook useSpecialEffect has a missing dependency: 'state'. " + + 'Either include it or remove the dependency array. ' + + `You can also do a functional update 'setState(s => ...)' ` + + `if you only need 'state' in the 'setState' call.`, + suggestions: [ + { + desc: 'Update the dependencies array to be: [state]', + output: normalizeIndent` + function MyComponent() { + const [state, setState] = React.useState(0); + + useSpecialEffect(() => { + const someNumber: typeof state = 2; + setState(prevState => prevState + someNumber + state); + }, [state]) + } + `, + }, + ], + }, + ], + }, + { + code: normalizeIndent` + function MyComponent() { + const [state, setState] = React.useState(0); + useMemo(() => { const someNumber: typeof state = 2; console.log(someNumber); @@ -8208,6 +8377,23 @@ const testsTypescript = { }, ], }, + { + code: normalizeIndent` + function MyComponent(props) { + useEffect(() => { + console.log(props.foo); + }); + } + `, + options: [{requireExplicitEffectDeps: true}], + errors: [ + { + message: + 'React Hook useEffect always requires dependencies. Please add a dependency array or an explicit `undefined`', + suggestions: undefined, + }, + ], + }, ], }; @@ -8311,7 +8497,9 @@ describe('rules-of-hooks/exhaustive-deps', () => { }, }; - const testsBabelEslint = { + const testsBabelEslint = tests; + + const testsHermesParser = { valid: [...testsFlow.valid, ...tests.valid], invalid: [...testsFlow.invalid, ...tests.invalid], }; @@ -8336,6 +8524,33 @@ describe('rules-of-hooks/exhaustive-deps', () => { testsBabelEslint ); + new ESLintTesterV7({ + parser: require.resolve('hermes-eslint'), + parserOptions: { + sourceType: 'module', + enableExperimentalComponentSyntax: true, + }, + }).run( + 'eslint: v7, parser: hermes-eslint', + ReactHooksESLintRule, + testsHermesParser + ); + + new ESLintTesterV9({ + languageOptions: { + ...languageOptionsV9, + parser: require('hermes-eslint'), + parserOptions: { + sourceType: 'module', + enableExperimentalComponentSyntax: true, + }, + }, + }).run( + 'eslint: v9, parser: hermes-eslint', + ReactHooksESLintRule, + testsHermesParser + ); + const testsTypescriptEslintParser = { valid: [...testsTypescript.valid, ...tests.valid], invalid: [...testsTypescript.invalid, ...tests.invalid], diff --git a/packages/eslint-plugin-react-hooks/__tests__/ESLintRulesOfHooks-test.js b/packages/eslint-plugin-react-hooks/__tests__/ESLintRulesOfHooks-test.js index 22d2427de69d3..8d8040bb43cd1 100644 --- a/packages/eslint-plugin-react-hooks/__tests__/ESLintRulesOfHooks-test.js +++ b/packages/eslint-plugin-react-hooks/__tests__/ESLintRulesOfHooks-test.js @@ -12,7 +12,8 @@ const ESLintTesterV7 = require('eslint-v7').RuleTester; const ESLintTesterV9 = require('eslint-v9').RuleTester; const ReactHooksESLintPlugin = require('eslint-plugin-react-hooks'); -const ReactHooksESLintRule = ReactHooksESLintPlugin.rules['rules-of-hooks']; +const ReactHooksESLintRule = + ReactHooksESLintPlugin.default.rules['rules-of-hooks']; /** * A string template tag that removes padding from the left side of multi-line strings @@ -1324,6 +1325,34 @@ const allTests = { `, errors: [asyncComponentHookError('use')], }, + { + code: normalizeIndent` + function App({p1, p2}) { + try { + use(p1); + } catch (error) { + console.error(error); + } + use(p2); + return
    App
    ; + } + `, + errors: [tryCatchUseError('use')], + }, + { + code: normalizeIndent` + function App({p1, p2}) { + try { + doSomething(); + } catch { + use(p1); + } + use(p2); + return
    App
    ; + } + `, + errors: [tryCatchUseError('use')], + }, ], }; @@ -1340,33 +1369,9 @@ if (__EXPERIMENTAL__) { useEffect(() => { onClick(); }); - } - `, - }, - { - code: normalizeIndent` - // Valid because functions created with useEffectEvent can be called in closures. - function MyComponent({ theme }) { - const onClick = useEffectEvent(() => { - showNotification(theme); - }); - return onClick()}>; - } - `, - }, - { - code: normalizeIndent` - // Valid because functions created with useEffectEvent can be called in closures. - function MyComponent({ theme }) { - const onClick = useEffectEvent(() => { - showNotification(theme); + React.useEffect(() => { + onClick(); }); - const onClick2 = () => { onClick() }; - const onClick3 = useCallback(() => onClick(), []); - return <> - - - ; } `, }, @@ -1380,47 +1385,47 @@ if (__EXPERIMENTAL__) { }); const onClick2 = useEffectEvent(() => { debounce(onClick); + debounce(() => onClick()); + debounce(() => { onClick() }); + deboucne(() => debounce(onClick)); }); useEffect(() => { - let id = setInterval(onClick, 100); + let id = setInterval(() => onClick(), 100); return () => clearInterval(onClick); }, []); - return onClick2()} /> + React.useEffect(() => { + let id = setInterval(() => onClick(), 100); + return () => clearInterval(onClick); + }, []); + return null; } `, }, - { - code: normalizeIndent` - const MyComponent = ({theme}) => { - const onClick = useEffectEvent(() => { - showNotification(theme); - }); - return onClick()}>; - }; - `, - }, { code: normalizeIndent` function MyComponent({ theme }) { - const notificationService = useNotifications(); - const showNotification = useEffectEvent((text) => { - notificationService.notify(theme, text); + useEffect(() => { + onClick(); }); - const onClick = useEffectEvent((text) => { - showNotification(text); + const onClick = useEffectEvent(() => { + showNotification(theme); }); - return onClick(text)} /> } `, }, { code: normalizeIndent` function MyComponent({ theme }) { + // Can receive arguments + const onEvent = useEffectEvent((text) => { + console.log(text); + }); + useEffect(() => { - onClick(); + onEvent('Hello world'); }); - const onClick = useEffectEvent(() => { - showNotification(theme); + React.useEffect(() => { + onEvent('Hello world'); }); } `, @@ -1437,7 +1442,7 @@ if (__EXPERIMENTAL__) { return ; } `, - errors: [useEffectEventError('onClick')], + errors: [useEffectEventError('onClick', false)], }, { code: normalizeIndent` @@ -1456,8 +1461,23 @@ if (__EXPERIMENTAL__) { }); return onClick()} /> } + + // The useEffectEvent function shares an identifier name with the above + function MyLastComponent({theme}) { + const onClick = useEffectEvent(() => { + showNotification(theme) + }); + useEffect(() => { + onClick(); // No error here, errors on all other uses + onClick; + }) + return + } `, - errors: [{...useEffectEventError('onClick'), line: 7}], + errors: [ + {...useEffectEventError('onClick', false), line: 7}, + {...useEffectEventError('onClick', true), line: 15}, + ], }, { code: normalizeIndent` @@ -1468,7 +1488,7 @@ if (__EXPERIMENTAL__) { return ; } `, - errors: [useEffectEventError('onClick')], + errors: [useEffectEventError('onClick', false)], }, { code: normalizeIndent` @@ -1481,7 +1501,7 @@ if (__EXPERIMENTAL__) { return } `, - errors: [{...useEffectEventError('onClick'), line: 7}], + errors: [{...useEffectEventError('onClick', false), line: 7}], }, { code: normalizeIndent` @@ -1497,7 +1517,27 @@ if (__EXPERIMENTAL__) { return } `, - errors: [useEffectEventError('onClick')], + errors: [useEffectEventError('onClick', false)], + }, + { + code: normalizeIndent` + // Invalid because functions created with useEffectEvent cannot be called in arbitrary closures. + function MyComponent({ theme }) { + const onClick = useEffectEvent(() => { + showNotification(theme); + }); + const onClick2 = () => { onClick() }; + const onClick3 = useCallback(() => onClick(), []); + return <> + + + ; + } + `, + errors: [ + useEffectEventError('onClick', true), + useEffectEventError('onClick', true), + ], }, ]; } @@ -1559,11 +1599,11 @@ function classError(hook) { }; } -function useEffectEventError(fn) { +function useEffectEventError(fn, called) { return { message: `\`${fn}\` is a function created with React Hook "useEffectEvent", and can only be called from ` + - 'the same component. They cannot be assigned to variables or passed down.', + `the same component.${called ? '' : ' They cannot be assigned to variables or passed down.'}`, }; } @@ -1573,6 +1613,12 @@ function asyncComponentHookError(fn) { }; } +function tryCatchUseError(fn) { + return { + message: `React Hook "${fn}" cannot be called in a try/catch block.`, + }; +} + // For easier local testing if (!process.env.CI) { let only = []; diff --git a/packages/eslint-plugin-react-hooks/__tests__/ReactCompilerRule-test.ts b/packages/eslint-plugin-react-hooks/__tests__/ReactCompilerRule-test.ts deleted file mode 100644 index 30762e5819535..0000000000000 --- a/packages/eslint-plugin-react-hooks/__tests__/ReactCompilerRule-test.ts +++ /dev/null @@ -1,262 +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 {ErrorSeverity} from 'babel-plugin-react-compiler'; -import {RuleTester as ESLintTester} from 'eslint'; -import ReactCompilerRule from '../src/rules/ReactCompiler'; - -const ESLintTesterV8 = require('eslint-v8').RuleTester; - -/** - * A string template tag that removes padding from the left side of multi-line strings - * @param {Array} strings array of code strings (only one expected) - */ -function normalizeIndent(strings: TemplateStringsArray): string { - const codeLines = strings[0]?.split('\n') ?? []; - const leftPadding = codeLines[1]?.match(/\s+/)![0] ?? ''; - return codeLines.map(line => line.slice(leftPadding.length)).join('\n'); -} - -type CompilerTestCases = { - valid: ESLintTester.ValidTestCase[]; - invalid: ESLintTester.InvalidTestCase[]; -}; - -const tests: CompilerTestCases = { - valid: [ - { - name: 'Basic example', - code: normalizeIndent` - function foo(x, y) { - if (x) { - return foo(false, y); - } - return [y * 10]; - } - `, - }, - { - name: 'Violation with Flow suppression', - code: ` - // Valid since error already suppressed with flow. - function useHookWithHook() { - if (cond) { - // $FlowFixMe[react-rule-hook] - useConditionalHook(); - } - } - `, - }, - { - name: 'Basic example with component syntax', - code: normalizeIndent` - export default component HelloWorld( - text: string = 'Hello!', - onClick: () => void, - ) { - return
    {text}
    ; - } - `, - }, - { - name: 'Unsupported syntax', - code: normalizeIndent` - function foo(x) { - var y = 1; - return y * x; - } - `, - }, - { - // OK because invariants are only meant for the compiler team's consumption - name: '[Invariant] Defined after use', - code: normalizeIndent` - function Component(props) { - let y = function () { - m(x); - }; - - let x = { a }; - m(x); - return y; - } - `, - }, - { - name: "Classes don't throw", - code: normalizeIndent` - class Foo { - #bar() {} - } - `, - }, - ], - invalid: [ - { - name: 'Reportable levels can be configured', - options: [{reportableLevels: new Set([ErrorSeverity.Todo])}], - code: normalizeIndent` - function Foo(x) { - var y = 1; - return
    {y * x}
    ; - }`, - errors: [ - { - message: - '(BuildHIR::lowerStatement) Handle var kinds in VariableDeclaration', - }, - ], - }, - { - name: '[InvalidReact] ESlint suppression', - // Indentation is intentionally weird so it doesn't add extra whitespace - code: normalizeIndent` - function Component(props) { - // eslint-disable-next-line react-hooks/rules-of-hooks - return
    {props.foo}
    ; - }`, - errors: [ - { - message: - 'React Compiler has skipped optimizing this component because one or more React ESLint rules were disabled. React Compiler only works when your components follow all the rules of React, disabling them may result in unexpected or incorrect behavior', - suggestions: [ - { - output: normalizeIndent` - function Component(props) { - - return
    {props.foo}
    ; - }`, - }, - ], - }, - { - message: - "Definition for rule 'react-hooks/rules-of-hooks' was not found.", - }, - ], - }, - { - 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)', - }, - ], - }, - { - name: "'use no forget' does not disable eslint rule", - code: normalizeIndent` - let count = 0; - function Component() { - 'use no forget'; - count = count + 1; - return
    Hello world {count}
    - } - `, - errors: [ - { - message: - 'Unexpected reassignment of a variable which was defined outside of the component. Components and hooks should be pure and side-effect free, but variable reassignment is a form of side-effect. If this variable is used in rendering, use useState instead. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#side-effects-must-run-outside-of-render)', - }, - ], - }, - { - name: "Unused 'use no forget' directive is reported when no errors are present on components", - code: normalizeIndent` - function Component() { - 'use no forget'; - return
    Hello world
    - } - `, - errors: [ - { - message: "Unused 'use no forget' directive", - suggestions: [ - { - output: - // yuck - '\nfunction Component() {\n \n return
    Hello world
    \n}\n', - }, - ], - }, - ], - }, - { - name: "Unused 'use no forget' directive is reported when no errors are present on non-components or hooks", - code: normalizeIndent` - function notacomponent() { - 'use no forget'; - return 1 + 1; - } - `, - errors: [ - { - message: "Unused 'use no forget' directive", - suggestions: [ - { - output: - // yuck - '\nfunction notacomponent() {\n \n return 1 + 1;\n}\n', - }, - ], - }, - ], - }, - ], -}; - -const eslintTester = new ESLintTesterV8({ - parser: require.resolve('hermes-eslint'), - parserOptions: { - ecmaVersion: 2015, - sourceType: 'module', - enableExperimentalComponentSyntax: true, - }, -}); -eslintTester.run('react-compiler - eslint: v8', ReactCompilerRule, tests); diff --git a/packages/eslint-plugin-react-hooks/__tests__/ReactCompilerRuleTypescript-test.ts b/packages/eslint-plugin-react-hooks/__tests__/ReactCompilerRuleTypescript-test.ts index 2efe6c7a38419..d9385bdba4335 100644 --- a/packages/eslint-plugin-react-hooks/__tests__/ReactCompilerRuleTypescript-test.ts +++ b/packages/eslint-plugin-react-hooks/__tests__/ReactCompilerRuleTypescript-test.ts @@ -6,7 +6,7 @@ */ import {RuleTester} from 'eslint'; -import ReactCompilerRule from '../src/rules/ReactCompiler'; +import {allRules} from '../src/shared/ReactCompiler'; const ESLintTesterV8 = require('eslint-v8').RuleTester; @@ -63,8 +63,7 @@ const tests: CompilerTestCases = { `, errors: [ { - message: - "Mutating a value returned from 'useState()', which should not be mutated. Use the setter function to update instead", + message: /Modifying a value returned from 'useState\(\)'/, line: 7, }, ], @@ -75,4 +74,4 @@ const tests: CompilerTestCases = { const eslintTester = new ESLintTesterV8({ parser: require.resolve('@typescript-eslint/parser-v5'), }); -eslintTester.run('react-compiler - eslint: v8', ReactCompilerRule, tests); +eslintTester.run('react-compiler', allRules['immutability'], tests); diff --git a/packages/eslint-plugin-react-hooks/package.json b/packages/eslint-plugin-react-hooks/package.json index 25215d71e530f..8f7cfc361d1ff 100644 --- a/packages/eslint-plugin-react-hooks/package.json +++ b/packages/eslint-plugin-react-hooks/package.json @@ -41,7 +41,7 @@ "dependencies": { "@babel/core": "^7.24.4", "@babel/parser": "^7.24.4", - "@babel/plugin-transform-private-methods": "^7.24.4", + "@babel/plugin-proposal-private-methods": "^7.18.6", "hermes-parser": "^0.25.1", "zod": "^3.22.4", "zod-validation-error": "^3.0.3" diff --git a/packages/eslint-plugin-react-hooks/src/index.ts b/packages/eslint-plugin-react-hooks/src/index.ts index 26a2e2b2c4276..462c3104a70a2 100644 --- a/packages/eslint-plugin-react-hooks/src/index.ts +++ b/packages/eslint-plugin-react-hooks/src/index.ts @@ -4,63 +4,64 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ -import type {ESLint, Linter, Rule} from 'eslint'; +import type {Linter, Rule} from 'eslint'; import ExhaustiveDeps from './rules/ExhaustiveDeps'; -import ReactCompiler from './rules/ReactCompiler'; +import {allRules, recommendedRules} from './shared/ReactCompiler'; import RulesOfHooks from './rules/RulesOfHooks'; // All rules const rules = { 'exhaustive-deps': ExhaustiveDeps, - 'react-compiler': ReactCompiler, 'rules-of-hooks': RulesOfHooks, + ...allRules, } satisfies Record; // Config rules -const configRules = { +const ruleConfigs = { 'react-hooks/rules-of-hooks': 'error', 'react-hooks/exhaustive-deps': 'warn', + ...Object.fromEntries( + Object.keys(recommendedRules).map(name => ['react-hooks/' + name, 'error']), + ), } satisfies Linter.RulesRecord; -// Flat config -const recommendedConfig = { - name: 'react-hooks/recommended', - plugins: { - get 'react-hooks'(): ESLint.Plugin { - return plugin; - }, +const plugin = { + meta: { + name: 'eslint-plugin-react-hooks', }, - rules: configRules, + configs: {}, + rules, }; -// Plugin object -const plugin = { - // TODO: Make this more dynamic to populate version from package.json. - // This can be done by injecting at build time, since importing the package.json isn't an option in Meta - meta: {name: 'eslint-plugin-react-hooks'}, - rules, - configs: { - /** Legacy recommended config, to be used with rc-based configurations */ - 'recommended-legacy': { - plugins: ['react-hooks'], - rules: configRules, +Object.assign(plugin.configs, { + 'recommended-legacy': { + plugins: ['react-hooks'], + rules: ruleConfigs, + }, + + 'flat/recommended': [ + { + plugins: { + 'react-hooks': plugin, + }, + rules: ruleConfigs, }, + ], - /** - * Recommended config, to be used with flat configs. - */ - recommended: recommendedConfig, + 'recommended-latest': [ + { + plugins: { + 'react-hooks': plugin, + }, + rules: ruleConfigs, + }, + ], - /** @deprecated please use `recommended`; will be removed in v7 */ - 'recommended-latest': recommendedConfig, + recommended: { + plugins: ['react-hooks'], + rules: ruleConfigs, }, -} satisfies ESLint.Plugin; - -const configs = plugin.configs; -const meta = plugin.meta; -export {configs, meta, rules}; +}); -// TODO: If the plugin is ever updated to be pure ESM and drops support for rc-based configs, then it should be exporting the plugin as default -// instead of individual named exports. -// export default plugin; +export default plugin; diff --git a/packages/eslint-plugin-react-hooks/src/rules/ExhaustiveDeps.ts b/packages/eslint-plugin-react-hooks/src/rules/ExhaustiveDeps.ts index 1b0059757278f..d59a1ff79202c 100644 --- a/packages/eslint-plugin-react-hooks/src/rules/ExhaustiveDeps.ts +++ b/packages/eslint-plugin-react-hooks/src/rules/ExhaustiveDeps.ts @@ -61,28 +61,45 @@ const rule = { enableDangerousAutofixThisMayCauseInfiniteLoops: { type: 'boolean', }, + experimental_autoDependenciesHooks: { + type: 'array', + items: { + type: 'string', + }, + }, + requireExplicitEffectDeps: { + type: 'boolean', + } }, }, ], }, create(context: Rule.RuleContext) { + const rawOptions = context.options && context.options[0]; + // Parse the `additionalHooks` regex. const additionalHooks = - context.options && - context.options[0] && - context.options[0].additionalHooks - ? new RegExp(context.options[0].additionalHooks) + rawOptions && rawOptions.additionalHooks + ? new RegExp(rawOptions.additionalHooks) : undefined; const enableDangerousAutofixThisMayCauseInfiniteLoops: boolean = - (context.options && - context.options[0] && - context.options[0].enableDangerousAutofixThisMayCauseInfiniteLoops) || + (rawOptions && + rawOptions.enableDangerousAutofixThisMayCauseInfiniteLoops) || false; + const experimental_autoDependenciesHooks: ReadonlyArray = + rawOptions && Array.isArray(rawOptions.experimental_autoDependenciesHooks) + ? rawOptions.experimental_autoDependenciesHooks + : []; + + const requireExplicitEffectDeps: boolean = rawOptions && rawOptions.requireExplicitEffectDeps || false; + const options = { additionalHooks, + experimental_autoDependenciesHooks, enableDangerousAutofixThisMayCauseInfiniteLoops, + requireExplicitEffectDeps, }; function reportProblem(problem: Rule.ReportDescriptor) { @@ -162,6 +179,7 @@ const rule = { reactiveHook: Node, reactiveHookName: string, isEffect: boolean, + isAutoDepsHook: boolean, ): void { if (isEffect && node.async) { reportProblem({ @@ -203,7 +221,13 @@ const rule = { let currentScope = scope.upper; while (currentScope) { pureScopes.add(currentScope); - if (currentScope.type === 'function') { + if ( + currentScope.type === 'function' || + // @ts-expect-error incorrect TS types + currentScope.type === 'hook' || + // @ts-expect-error incorrect TS types + currentScope.type === 'component' + ) { break; } currentScope = currentScope.upper; @@ -643,6 +667,9 @@ const rule = { } if (!declaredDependenciesNode) { + if (isAutoDepsHook) { + return; + } // Check if there are any top-level setState() calls. // Those tend to lead to infinite loops. let setStateInsideEffectWithoutDeps: string | null = null; @@ -705,6 +732,13 @@ const rule = { } return; } + if ( + isAutoDepsHook && + declaredDependenciesNode.type === 'Literal' && + declaredDependenciesNode.value === null + ) { + return; + } const declaredDependencies: Array = []; const externalDependencies = new Set(); @@ -1312,10 +1346,28 @@ const rule = { return; } + if (!maybeNode && isEffect && options.requireExplicitEffectDeps) { + reportProblem({ + node: reactiveHook, + message: + `React Hook ${reactiveHookName} always requires dependencies. ` + + `Please add a dependency array or an explicit \`undefined\`` + }); + } + + const isAutoDepsHook = + options.experimental_autoDependenciesHooks.includes(reactiveHookName); + // Check the declared dependencies for this reactive hook. If there is no // second argument then the reactive callback will re-run on every render. // So no need to check for dependency inclusion. - if (!declaredDependenciesNode && !isEffect) { + if ( + (!declaredDependenciesNode || + (isAutoDepsHook && + declaredDependenciesNode.type === 'Literal' && + declaredDependenciesNode.value === null)) && + !isEffect + ) { // These are only used for optimization. if ( reactiveHookName === 'useMemo' || @@ -1349,11 +1401,17 @@ const rule = { reactiveHook, reactiveHookName, isEffect, + isAutoDepsHook, ); return; // Handled case 'Identifier': - if (!declaredDependenciesNode) { - // No deps, no problems. + if ( + !declaredDependenciesNode || + (isAutoDepsHook && + declaredDependenciesNode.type === 'Literal' && + declaredDependenciesNode.value === null) + ) { + // Always runs, no problems. return; // Handled } // The function passed as a callback is not written inline. @@ -1402,6 +1460,7 @@ const rule = { reactiveHook, reactiveHookName, isEffect, + isAutoDepsHook, ); return; // Handled case 'VariableDeclarator': @@ -1421,6 +1480,7 @@ const rule = { reactiveHook, reactiveHookName, isEffect, + isAutoDepsHook, ); return; // Handled } diff --git a/packages/eslint-plugin-react-hooks/src/rules/ReactCompiler.ts b/packages/eslint-plugin-react-hooks/src/rules/ReactCompiler.ts deleted file mode 100644 index 67d5745a1c7ea..0000000000000 --- a/packages/eslint-plugin-react-hooks/src/rules/ReactCompiler.ts +++ /dev/null @@ -1,353 +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. - */ -/* eslint-disable no-for-of-loops/no-for-of-loops */ - -import {transformFromAstSync} from '@babel/core'; -// @ts-expect-error: no types available -import PluginProposalPrivateMethods from '@babel/plugin-transform-private-methods'; -import type {SourceLocation as BabelSourceLocation} from '@babel/types'; -import BabelPluginReactCompiler, { - type CompilerErrorDetailOptions, - CompilerSuggestionOperation, - ErrorSeverity, - parsePluginOptions, - validateEnvironmentConfig, - OPT_OUT_DIRECTIVES, - type Logger, - type LoggerEvent, - type PluginOptions, -} from 'babel-plugin-react-compiler'; -import type {Rule} from 'eslint'; -import {Statement} from 'estree'; -import * as HermesParser from 'hermes-parser'; - -type CompilerErrorDetailWithLoc = Omit & { - loc: BabelSourceLocation; -}; - -function assertExhaustive(_: never, errorMsg: string): never { - throw new Error(errorMsg); -} - -const DEFAULT_REPORTABLE_LEVELS = new Set([ - ErrorSeverity.InvalidReact, - ErrorSeverity.InvalidJS, -]); -let reportableLevels = DEFAULT_REPORTABLE_LEVELS; - -function isReportableDiagnostic( - detail: CompilerErrorDetailOptions, -): detail is CompilerErrorDetailWithLoc { - return ( - reportableLevels.has(detail.severity) && - detail.loc != null && - typeof detail.loc !== 'symbol' - ); -} - -function makeSuggestions( - detail: CompilerErrorDetailOptions, -): Array { - const 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, - panicThreshold: 'none', - // Don't emit errors on Flow suppressions--Flow already gave a signal - flowSuppressions: false, - environment: validateEnvironmentConfig({ - validateRefAccessDuringRender: false, - }), -}; - -const rule: Rule.RuleModule = { - meta: { - type: 'problem', - docs: { - description: 'Surfaces diagnostics from React Forget', - recommended: true, - }, - fixable: 'code', - hasSuggestions: true, - // validation is done at runtime with zod - schema: [{type: 'object', additionalProperties: true}], - }, - create(context: Rule.RuleContext) { - // Compat with older versions of eslint - const sourceCode = context.sourceCode ?? context.getSourceCode(); - const filename = context.filename ?? context.getFilename(); - const userOpts = context.options[0] ?? {}; - if ( - userOpts.reportableLevels != null && - userOpts.reportableLevels instanceof Set - ) { - reportableLevels = userOpts.reportableLevels; - } 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; - } - - let shouldReportUnusedOptOutDirective = true; - const options: PluginOptions = parsePluginOptions({ - ...COMPILER_OPTIONS, - ...userOpts, - environment: { - ...COMPILER_OPTIONS.environment, - ...userOpts.environment, - }, - }); - const userLogger: Logger | null = options.logger; - options.logger = { - logEvent: (eventFilename, event): void => { - userLogger?.logEvent(eventFilename, event); - if (event.kind === 'CompileError') { - shouldReportUnusedOptOutDirective = false; - 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})` - : ''; - /** - * Report bailouts with a smaller span (just the first line). - * Compiler bailout lints only serve to flag that a react function - * has not been optimized by the compiler for codebases which depend - * on compiler memo heavily for perf. These lints are also often not - * actionable. - */ - let endLoc; - if (event.fnLoc.end.line === event.fnLoc.start.line) { - endLoc = event.fnLoc.end; - } else { - endLoc = { - line: event.fnLoc.start.line, - // Babel loc line numbers are 1-indexed - column: - sourceCode.text.split(/\r?\n|\r|\n/g)[ - event.fnLoc.start.line - 1 - ]?.length ?? 0, - }; - } - const firstLineLoc = { - start: event.fnLoc.start, - end: endLoc, - }; - context.report({ - message: `[ReactCompilerBailout] ${detail.reason}${locStr}`, - loc: firstLineLoc, - suggest, - }); - } - - if (!isReportableDiagnostic(detail)) { - return; - } - if ( - hasFlowSuppression(detail.loc, 'react-rule-hook') || - hasFlowSuppression(detail.loc, 'react-rule-unsafe-ref') - ) { - // 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( - options.environment ?? {}, - ); - } catch (err: unknown) { - options.logger?.logEvent('', err as LoggerEvent); - } - - function hasFlowSuppression( - nodeLoc: BabelSourceLocation, - suppression: string, - ): boolean { - const comments = sourceCode.getAllComments(); - const flowSuppressionRegex = new RegExp( - '\\$FlowFixMe\\[' + suppression + '\\]', - ); - for (const commentNode of comments) { - if ( - flowSuppressionRegex.test(commentNode.value) && - commentNode.loc!.end.line === nodeLoc.start.line - 1 - ) { - return true; - } - } - return false; - } - - let babelAST; - if (filename.endsWith('.tsx') || filename.endsWith('.ts')) { - try { - const {parse: babelParse} = require('@babel/parser'); - babelAST = babelParse(sourceCode.text, { - filename, - sourceType: 'unambiguous', - plugins: ['typescript', 'jsx'], - }); - } catch { - /* empty */ - } - } else { - try { - babelAST = HermesParser.parse(sourceCode.text, { - babel: true, - enableExperimentalComponentSyntax: true, - sourceFilename: filename, - sourceType: 'module', - }); - } catch { - /* empty */ - } - } - - if (babelAST != null) { - try { - transformFromAstSync(babelAST, sourceCode.text, { - filename, - highlightCode: false, - retainLines: true, - plugins: [ - [PluginProposalPrivateMethods, {loose: true}], - [BabelPluginReactCompiler, options], - ], - sourceType: 'module', - configFile: false, - babelrc: false, - }); - } catch (err) { - /* errors handled by injected logger */ - } - } - - function reportUnusedOptOutDirective(stmt: Statement) { - if ( - stmt.type === 'ExpressionStatement' && - stmt.expression.type === 'Literal' && - typeof stmt.expression.value === 'string' && - OPT_OUT_DIRECTIVES.has(stmt.expression.value) && - stmt.loc != null - ) { - context.report({ - message: `Unused '${stmt.expression.value}' directive`, - loc: stmt.loc, - suggest: [ - { - desc: 'Remove the directive', - fix(fixer) { - return fixer.remove(stmt); - }, - }, - ], - }); - } - } - if (shouldReportUnusedOptOutDirective) { - return { - FunctionDeclaration(fnDecl) { - for (const stmt of fnDecl.body.body) { - reportUnusedOptOutDirective(stmt); - } - }, - ArrowFunctionExpression(fnExpr) { - if (fnExpr.body.type === 'BlockStatement') { - for (const stmt of fnExpr.body.body) { - reportUnusedOptOutDirective(stmt); - } - } - }, - FunctionExpression(fnExpr) { - for (const stmt of fnExpr.body.body) { - reportUnusedOptOutDirective(stmt); - } - }, - }; - } else { - return {}; - } - }, -}; - -export default rule; diff --git a/packages/eslint-plugin-react-hooks/src/rules/RulesOfHooks.ts b/packages/eslint-plugin-react-hooks/src/rules/RulesOfHooks.ts index ac7d0f3a06cfe..0721a75e00642 100644 --- a/packages/eslint-plugin-react-hooks/src/rules/RulesOfHooks.ts +++ b/packages/eslint-plugin-react-hooks/src/rules/RulesOfHooks.ts @@ -7,7 +7,16 @@ /* eslint-disable no-for-of-loops/no-for-of-loops */ import type {Rule, Scope} from 'eslint'; -import type {CallExpression, DoWhileStatement, Node} from 'estree'; +import type { + CallExpression, + CatchClause, + DoWhileStatement, + Expression, + Identifier, + Node, + Super, + TryStatement, +} from 'estree'; // @ts-expect-error untyped module import CodePathAnalyzer from '../code-path-analysis/code-path-analyzer'; @@ -111,6 +120,36 @@ function isInsideDoWhileLoop(node: Node | undefined): node is DoWhileStatement { return false; } +function isInsideTryCatch( + node: Node | undefined, +): node is TryStatement | CatchClause { + while (node) { + if (node.type === 'TryStatement' || node.type === 'CatchClause') { + return true; + } + node = node.parent; + } + return false; +} + +function getNodeWithoutReactNamespace( + node: Expression | Super, +): Expression | Identifier | Super { + if ( + node.type === 'MemberExpression' && + node.object.type === 'Identifier' && + node.object.name === 'React' && + node.property.type === 'Identifier' && + !node.computed + ) { + return node.property; + } + return node; +} + +function isUseEffectIdentifier(node: Node): boolean { + return node.type === 'Identifier' && node.name === 'useEffect'; +} function isUseEffectEventIdentifier(node: Node): boolean { if (__EXPERIMENTAL__) { return node.type === 'Identifier' && node.name === 'useEffectEvent'; @@ -532,6 +571,16 @@ const rule = { continue; } + // Report an error if use() is called inside try/catch. + if (isUseIdentifier(hook) && isInsideTryCatch(hook)) { + context.report({ + node: hook, + message: `React Hook "${getSourceCode().getText( + hook, + )}" cannot be called in a try/catch block.`, + }); + } + // Report an error if a hook may be called more then once. // `use(...)` can be called in loops. if ( @@ -541,7 +590,9 @@ const rule = { context.report({ node: hook, message: - `React Hook "${getSourceCode().getText(hook)}" may be executed ` + + `React Hook "${getSourceCode().getText( + hook, + )}" may be executed ` + 'more than once. Possibly because it is called in a loop. ' + 'React Hooks must be called in the exact same order in ' + 'every component render.', @@ -596,7 +647,9 @@ const rule = { ) { // Custom message for hooks inside a class const message = - `React Hook "${getSourceCode().getText(hook)}" cannot be called ` + + `React Hook "${getSourceCode().getText( + hook, + )}" cannot be called ` + 'in a class component. React Hooks must be called in a ' + 'React function component or a custom React Hook function.'; context.report({node: hook, message}); @@ -613,7 +666,9 @@ const rule = { } else if (codePathNode.type === 'Program') { // These are dangerous if you have inline requires enabled. const message = - `React Hook "${getSourceCode().getText(hook)}" cannot be called ` + + `React Hook "${getSourceCode().getText( + hook, + )}" cannot be called ` + 'at the top level. React Hooks must be called in a ' + 'React function component or a custom React Hook function.'; context.report({node: hook, message}); @@ -626,7 +681,9 @@ const rule = { // `use(...)` can be called in callbacks. if (isSomewhereInsideComponentOrHook && !isUseIdentifier(hook)) { const message = - `React Hook "${getSourceCode().getText(hook)}" cannot be called ` + + `React Hook "${getSourceCode().getText( + hook, + )}" cannot be called ` + 'inside a callback. React Hooks must be called in a ' + 'React function component or a custom React Hook function.'; context.report({node: hook, message}); @@ -666,10 +723,11 @@ const rule = { // useEffectEvent: useEffectEvent functions can be passed by reference within useEffect as well as in // another useEffectEvent + // Check all `useEffect` and `React.useEffect`, `useEffectEvent`, and `React.useEffectEvent` + const nodeWithoutNamespace = getNodeWithoutReactNamespace(node.callee); if ( - node.callee.type === 'Identifier' && - (node.callee.name === 'useEffect' || - isUseEffectEventIdentifier(node.callee)) && + (isUseEffectIdentifier(nodeWithoutNamespace) || + isUseEffectEventIdentifier(nodeWithoutNamespace)) && node.arguments.length > 0 ) { // Denote that we have traversed into a useEffect call, and stash the CallExpr for @@ -681,18 +739,18 @@ const rule = { Identifier(node) { // This identifier resolves to a useEffectEvent function, but isn't being referenced in an // effect or another event function. It isn't being called either. - if ( - lastEffect == null && - useEffectEventFunctions.has(node) && - node.parent.type !== 'CallExpression' - ) { + if (lastEffect == null && useEffectEventFunctions.has(node)) { + const message = + `\`${getSourceCode().getText( + node, + )}\` is a function created with React Hook "useEffectEvent", and can only be called from ` + + 'the same component.' + + (node.parent.type === 'CallExpression' + ? '' + : ' They cannot be assigned to variables or passed down.'); context.report({ node, - message: - `\`${getSourceCode().getText( - node, - )}\` is a function created with React Hook "useEffectEvent", and can only be called from ` + - 'the same component. They cannot be assigned to variables or passed down.', + message, }); } }, diff --git a/packages/eslint-plugin-react-hooks/src/shared/ReactCompiler.ts b/packages/eslint-plugin-react-hooks/src/shared/ReactCompiler.ts new file mode 100644 index 0000000000000..f006b79781755 --- /dev/null +++ b/packages/eslint-plugin-react-hooks/src/shared/ReactCompiler.ts @@ -0,0 +1,216 @@ +/** + * 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. + */ +/* eslint-disable no-for-of-loops/no-for-of-loops */ + +import type {SourceLocation as BabelSourceLocation} from '@babel/types'; +import { + type CompilerDiagnosticOptions, + type CompilerErrorDetailOptions, + CompilerSuggestionOperation, + LintRules, + type LintRule, +} from 'babel-plugin-react-compiler'; +import type {Rule} from 'eslint'; +import runReactCompiler, {RunCacheEntry} from './RunReactCompiler'; + +function assertExhaustive(_: never, errorMsg: string): never { + throw new Error(errorMsg); +} + +function makeSuggestions( + detail: CompilerErrorDetailOptions | CompilerDiagnosticOptions, +): Array { + const 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; +} + +function getReactCompilerResult(context: Rule.RuleContext): RunCacheEntry { + // Compat with older versions of eslint + const sourceCode = context.sourceCode ?? context.getSourceCode(); + const filename = context.filename ?? context.getFilename(); + const userOpts = context.options[0] ?? {}; + + const results = runReactCompiler({ + sourceCode, + filename, + userOpts, + }); + + return results; +} + +function hasFlowSuppression( + program: RunCacheEntry, + nodeLoc: BabelSourceLocation, + suppressions: Array, +): boolean { + for (const commentNode of program.flowSuppressions) { + if ( + suppressions.includes(commentNode.code) && + commentNode.line === nodeLoc.start.line - 1 + ) { + return true; + } + } + return false; +} + +function makeRule(rule: LintRule): Rule.RuleModule { + const create = (context: Rule.RuleContext): Rule.RuleListener => { + const result = getReactCompilerResult(context); + + for (const event of result.events) { + if (event.kind === 'CompileError') { + const detail = event.detail; + if (detail.category === rule.category) { + const loc = detail.primaryLocation(); + if (loc == null || typeof loc === 'symbol') { + continue; + } + if ( + hasFlowSuppression(result, loc, [ + 'react-rule-hook', + 'react-rule-unsafe-ref', + ]) + ) { + // If Flow already caught this error, we don't need to report it again. + continue; + } + /* + * TODO: if multiple rules report the same linter category, + * we should deduplicate them with a "reported" set + */ + context.report({ + message: detail.printErrorMessage(result.sourceCode, { + eslint: true, + }), + loc, + suggest: makeSuggestions(detail.options), + }); + } + } + } + return {}; + }; + + return { + meta: { + type: 'problem', + docs: { + description: rule.description, + recommended: rule.recommended, + }, + fixable: 'code', + hasSuggestions: true, + // validation is done at runtime with zod + schema: [{type: 'object', additionalProperties: true}], + }, + create, + }; +} + +export const NoUnusedDirectivesRule: Rule.RuleModule = { + meta: { + type: 'suggestion', + docs: { + recommended: true, + }, + fixable: 'code', + hasSuggestions: true, + // validation is done at runtime with zod + schema: [{type: 'object', additionalProperties: true}], + }, + create(context: Rule.RuleContext): Rule.RuleListener { + const results = getReactCompilerResult(context); + + for (const directive of results.unusedOptOutDirectives) { + context.report({ + message: `Unused '${directive.directive}' directive`, + loc: directive.loc, + suggest: [ + { + desc: 'Remove the directive', + fix(fixer): Rule.Fix { + return fixer.removeRange(directive.range); + }, + }, + ], + }); + } + return {}; + }, +}; + +type RulesObject = {[name: string]: Rule.RuleModule}; + +export const allRules: RulesObject = LintRules.reduce( + (acc, rule) => { + acc[rule.name] = makeRule(rule); + return acc; + }, + { + 'no-unused-directives': NoUnusedDirectivesRule, + } as RulesObject, +); + +export const recommendedRules: RulesObject = LintRules.filter( + rule => rule.recommended, +).reduce( + (acc, rule) => { + acc[rule.name] = makeRule(rule); + return acc; + }, + { + 'no-unused-directives': NoUnusedDirectivesRule, + } as RulesObject, +); diff --git a/packages/eslint-plugin-react-hooks/src/shared/RunReactCompiler.ts b/packages/eslint-plugin-react-hooks/src/shared/RunReactCompiler.ts new file mode 100644 index 0000000000000..72e28b1f488ee --- /dev/null +++ b/packages/eslint-plugin-react-hooks/src/shared/RunReactCompiler.ts @@ -0,0 +1,281 @@ +/** + * 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. + */ +/* eslint-disable no-for-of-loops/no-for-of-loops */ + +import {transformFromAstSync, traverse} from '@babel/core'; +import {parse as babelParse} from '@babel/parser'; +import {Directive, File} from '@babel/types'; +// @ts-expect-error: no types available +import PluginProposalPrivateMethods from '@babel/plugin-proposal-private-methods'; +import BabelPluginReactCompiler, { + parsePluginOptions, + validateEnvironmentConfig, + OPT_OUT_DIRECTIVES, + type PluginOptions, + Logger, + LoggerEvent, +} from 'babel-plugin-react-compiler'; +import type {SourceCode} from 'eslint'; +import {SourceLocation} from 'estree'; +import * as HermesParser from 'hermes-parser'; +import {isDeepStrictEqual} from 'util'; +import type {ParseResult} from '@babel/parser'; + +const COMPILER_OPTIONS: Partial = { + noEmit: true, + panicThreshold: 'none', + // Don't emit errors on Flow suppressions--Flow already gave a signal + flowSuppressions: false, + environment: validateEnvironmentConfig({ + validateRefAccessDuringRender: true, + validateNoSetStateInRender: true, + validateNoSetStateInEffects: true, + validateNoJSXInTryStatements: true, + validateNoImpureFunctionsInRender: true, + validateStaticComponents: true, + validateNoFreezingKnownMutableFunctions: true, + validateNoVoidUseMemo: true, + // TODO: remove, this should be in the type system + validateNoCapitalizedCalls: [], + validateHooksUsage: true, + validateNoDerivedComputationsInEffects: true, + }), +}; + +export type UnusedOptOutDirective = { + loc: SourceLocation; + range: [number, number]; + directive: string; +}; +export type RunCacheEntry = { + sourceCode: string; + filename: string; + userOpts: PluginOptions; + flowSuppressions: Array<{line: number; code: string}>; + unusedOptOutDirectives: Array; + events: Array; +}; + +type RunParams = { + sourceCode: SourceCode; + filename: string; + userOpts: PluginOptions; +}; +const FLOW_SUPPRESSION_REGEX = /\$FlowFixMe\[([^\]]*)\]/g; + +function getFlowSuppressions( + sourceCode: SourceCode, +): Array<{line: number; code: string}> { + const comments = sourceCode.getAllComments(); + const results: Array<{line: number; code: string}> = []; + + for (const commentNode of comments) { + const matches = commentNode.value.matchAll(FLOW_SUPPRESSION_REGEX); + for (const match of matches) { + if (match.index != null && commentNode.loc != null) { + const code = match[1]; + results.push({ + line: commentNode.loc!.end.line, + code, + }); + } + } + } + return results; +} + +function filterUnusedOptOutDirectives( + directives: ReadonlyArray, +): Array { + const results: Array = []; + for (const directive of directives) { + if ( + OPT_OUT_DIRECTIVES.has(directive.value.value) && + directive.loc != null + ) { + results.push({ + loc: directive.loc, + directive: directive.value.value, + range: [directive.start!, directive.end!], + }); + } + } + return results; +} + +function runReactCompilerImpl({ + sourceCode, + filename, + userOpts, +}: RunParams): RunCacheEntry { + // Compat with older versions of eslint + const options: PluginOptions = parsePluginOptions({ + ...COMPILER_OPTIONS, + ...userOpts, + environment: { + ...COMPILER_OPTIONS.environment, + ...userOpts.environment, + }, + }); + const results: RunCacheEntry = { + sourceCode: sourceCode.text, + filename, + userOpts, + flowSuppressions: [], + unusedOptOutDirectives: [], + events: [], + }; + const userLogger: Logger | null = options.logger; + options.logger = { + logEvent: (eventFilename, event): void => { + userLogger?.logEvent(eventFilename, event); + results.events.push(event); + }, + }; + + try { + options.environment = validateEnvironmentConfig(options.environment ?? {}); + } catch (err: unknown) { + options.logger?.logEvent(filename, err as LoggerEvent); + } + + let babelAST: ParseResult | null = null; + if (filename.endsWith('.tsx') || filename.endsWith('.ts')) { + try { + babelAST = babelParse(sourceCode.text, { + sourceFilename: filename, + sourceType: 'unambiguous', + plugins: ['typescript', 'jsx'], + }); + } catch { + /* empty */ + } + } else { + try { + babelAST = HermesParser.parse(sourceCode.text, { + babel: true, + enableExperimentalComponentSyntax: true, + sourceFilename: filename, + sourceType: 'module', + }); + } catch { + /* empty */ + } + } + + if (babelAST != null) { + results.flowSuppressions = getFlowSuppressions(sourceCode); + try { + transformFromAstSync(babelAST, sourceCode.text, { + filename, + highlightCode: false, + retainLines: true, + plugins: [ + [PluginProposalPrivateMethods, {loose: true}], + [BabelPluginReactCompiler, options], + ], + sourceType: 'module', + configFile: false, + babelrc: false, + }); + + if (results.events.filter(e => e.kind === 'CompileError').length === 0) { + traverse(babelAST, { + FunctionDeclaration(path) { + results.unusedOptOutDirectives.push( + ...filterUnusedOptOutDirectives(path.node.body.directives), + ); + }, + ArrowFunctionExpression(path) { + if (path.node.body.type === 'BlockStatement') { + results.unusedOptOutDirectives.push( + ...filterUnusedOptOutDirectives(path.node.body.directives), + ); + } + }, + FunctionExpression(path) { + results.unusedOptOutDirectives.push( + ...filterUnusedOptOutDirectives(path.node.body.directives), + ); + }, + }); + } + } catch (err) { + /* errors handled by injected logger */ + } + } + + return results; +} + +const SENTINEL = Symbol(); + +// Array backed LRU cache -- should be small < 10 elements +class LRUCache { + // newest at headIdx, then headIdx + 1, ..., tailIdx + #values: Array<[K, T | Error] | [typeof SENTINEL, void]>; + #headIdx: number = 0; + + constructor(size: number) { + this.#values = new Array(size).fill(SENTINEL); + } + + // gets a value and sets it as "recently used" + get(key: K): T | null { + const idx = this.#values.findIndex(entry => entry[0] === key); + // If found, move to front + if (idx === this.#headIdx) { + return this.#values[this.#headIdx][1] as T; + } else if (idx < 0) { + return null; + } + + const entry: [K, T] = this.#values[idx] as [K, T]; + + const len = this.#values.length; + for (let i = 0; i < Math.min(idx, len - 1); i++) { + this.#values[(this.#headIdx + i + 1) % len] = + this.#values[(this.#headIdx + i) % len]; + } + this.#values[this.#headIdx] = entry; + return entry[1]; + } + push(key: K, value: T): void { + this.#headIdx = + (this.#headIdx - 1 + this.#values.length) % this.#values.length; + this.#values[this.#headIdx] = [key, value]; + } +} +const cache = new LRUCache(10); + +export default function runReactCompiler({ + sourceCode, + filename, + userOpts, +}: RunParams): RunCacheEntry { + const entry = cache.get(filename); + if ( + entry != null && + entry.sourceCode === sourceCode.text && + isDeepStrictEqual(entry.userOpts, userOpts) + ) { + return entry; + } + + const runEntry = runReactCompilerImpl({ + sourceCode, + filename, + userOpts, + }); + // If we have a cache entry, we can update it + if (entry != null) { + Object.assign(entry, runEntry); + } else { + cache.push(filename, runEntry); + } + return {...runEntry}; +} diff --git a/packages/internal-test-utils/__tests__/ReactInternalTestUtils-test.js b/packages/internal-test-utils/__tests__/ReactInternalTestUtils-test.js index 3c10125186832..b24741477866b 100644 --- a/packages/internal-test-utils/__tests__/ReactInternalTestUtils-test.js +++ b/packages/internal-test-utils/__tests__/ReactInternalTestUtils-test.js @@ -2169,6 +2169,29 @@ describe('ReactInternalTestUtils console assertions', () => { + Bye in div (at **)" `); }); + + // @gate __DEV__ + it('fails if last received error containing "undefined" is not included', () => { + const message = expectToThrowFailure(() => { + console.error('Hi'); + console.error( + "TypeError: Cannot read properties of undefined (reading 'stack')\n" + + ' in Foo (at **)' + ); + assertConsoleErrorDev([['Hi', {withoutStack: true}]]); + }); + expect(message).toMatchInlineSnapshot(` + "assertConsoleErrorDev(expected) + + Unexpected error(s) recorded. + + - Expected errors + + Received errors + + Hi + + TypeError: Cannot read properties of undefined (reading 'stack') in Foo (at **)" + `); + }); // @gate __DEV__ it('fails if only error does not contain a stack', () => { const message = expectToThrowFailure(() => { diff --git a/packages/internal-test-utils/consoleMock.js b/packages/internal-test-utils/consoleMock.js index 9bb797bac395b..743519590e37e 100644 --- a/packages/internal-test-utils/consoleMock.js +++ b/packages/internal-test-utils/consoleMock.js @@ -156,7 +156,8 @@ function normalizeCodeLocInfo(str) { // at Component (/path/filename.js:123:45) // React format: // in Component (at filename.js:123) - return str.replace(/\n +(?:at|in) ([\S]+)[^\n]*/g, function (m, name) { + return str.replace(/\n +(?:at|in) ([^(\[\n]+)[^\n]*/g, function (m, name) { + name = name.trim(); 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. @@ -354,7 +355,7 @@ export function createLogAssertion( let argIndex = 0; // console.* could have been called with a non-string e.g. `console.error(new Error())` // eslint-disable-next-line react-internal/safe-string-coercion - String(format).replace(/%s|%c/g, () => argIndex++); + String(format).replace(/%s|%c|%o/g, () => argIndex++); if (argIndex !== args.length) { if (format.includes('%c%s')) { // We intentionally use mismatching formatting when printing badging because we don't know @@ -381,8 +382,9 @@ export function createLogAssertion( // Main logic to check if log is expected, with the component stack. if ( - normalizedMessage === expectedMessage || - normalizedMessage.includes(expectedMessage) + typeof expectedMessage === 'string' && + (normalizedMessage === expectedMessage || + normalizedMessage.includes(expectedMessage)) ) { if (isLikelyAComponentStack(normalizedMessage)) { if (expectedWithoutStack === true) { diff --git a/packages/internal-test-utils/enqueueTask.js b/packages/internal-test-utils/enqueueTask.js index 9ddcf1d5cab53..1edb75a7b1806 100644 --- a/packages/internal-test-utils/enqueueTask.js +++ b/packages/internal-test-utils/enqueueTask.js @@ -11,6 +11,7 @@ const {MessageChannel} = require('node:worker_threads'); export default function enqueueTask(task: () => void): void { const channel = new MessageChannel(); + // $FlowFixMe[prop-missing] channel.port1.onmessage = () => { channel.port1.close(); task(); diff --git a/packages/internal-test-utils/internalAct.js b/packages/internal-test-utils/internalAct.js index 752725eeb842f..1a420bcf203b4 100644 --- a/packages/internal-test-utils/internalAct.js +++ b/packages/internal-test-utils/internalAct.js @@ -138,6 +138,7 @@ export async function act(scope: () => Thenable): Thenable { // those will also fire now, too, which is not ideal. (The public // version of `act` doesn't do this.) For this reason, we should try // to avoid using timers in our internal tests. + j.runAllTicks(); j.runOnlyPendingTimers(); // If a committing a fallback triggers another update, it might not // get scheduled until a microtask. So wait one more time. @@ -194,6 +195,39 @@ export async function act(scope: () => Thenable): Thenable { } } +async function waitForTasksAndTimers(error: Error) { + do { + // Wait until end of current task/microtask. + await waitForMicrotasks(); + + // $FlowFixMe[cannot-resolve-name]: Flow doesn't know about global Jest object + if (jest.isEnvironmentTornDown()) { + error.message = + 'The Jest environment was torn down before `act` completed. This ' + + 'probably means you forgot to `await` an `act` call.'; + throw error; + } + + // $FlowFixMe[cannot-resolve-name]: Flow doesn't know about global Jest object + const j = jest; + if (j.getTimerCount() > 0) { + // There's a pending timer. Flush it now. We only do this in order to + // force Suspense fallbacks to display; the fact that it's a timer + // is an implementation detail. If there are other timers scheduled, + // those will also fire now, too, which is not ideal. (The public + // version of `act` doesn't do this.) For this reason, we should try + // to avoid using timers in our internal tests. + j.runAllTicks(); + j.runOnlyPendingTimers(); + // If a committing a fallback triggers another update, it might not + // get scheduled until a microtask. So wait one more time. + await waitForMicrotasks(); + } else { + break; + } + } while (true); +} + export async function serverAct(scope: () => Thenable): Thenable { // We require every `act` call to assert console logs // with one of the assertion helpers. Fails if not empty. @@ -233,37 +267,17 @@ export async function serverAct(scope: () => Thenable): Thenable { } try { - const result = await scope(); - - do { - // Wait until end of current task/microtask. - await waitForMicrotasks(); - - // $FlowFixMe[cannot-resolve-name]: Flow doesn't know about global Jest object - if (jest.isEnvironmentTornDown()) { - error.message = - 'The Jest environment was torn down before `act` completed. This ' + - 'probably means you forgot to `await` an `act` call.'; - throw error; - } - - // $FlowFixMe[cannot-resolve-name]: Flow doesn't know about global Jest object - const j = jest; - if (j.getTimerCount() > 0) { - // There's a pending timer. Flush it now. We only do this in order to - // force Suspense fallbacks to display; the fact that it's a timer - // is an implementation detail. If there are other timers scheduled, - // those will also fire now, too, which is not ideal. (The public - // version of `act` doesn't do this.) For this reason, we should try - // to avoid using timers in our internal tests. - j.runOnlyPendingTimers(); - // If a committing a fallback triggers another update, it might not - // get scheduled until a microtask. So wait one more time. - await waitForMicrotasks(); - } else { - break; - } - } while (true); + const promise = scope(); + // $FlowFixMe[prop-missing] + if (promise && typeof promise.catch === 'function') { + // $FlowFixMe[incompatible-use] + promise.catch(() => {}); // Handle below + } + // See if we need to do some work to unblock the promise first. + await waitForTasksAndTimers(error); + const result = await promise; + // Then wait to flush the result. + await waitForTasksAndTimers(error); if (thrownErrors.length > 0) { // Rethrow any errors logged by the global error handling. diff --git a/packages/react-client/src/ReactClientConsoleConfigBrowser.js b/packages/react-client/src/ReactClientConsoleConfigBrowser.js index 355edc9c08197..f67e4afa0c464 100644 --- a/packages/react-client/src/ReactClientConsoleConfigBrowser.js +++ b/packages/react-client/src/ReactClientConsoleConfigBrowser.js @@ -7,7 +7,8 @@ * @flow */ -const badgeFormat = '%c%s%c '; +// Keep in sync with ReactServerConsoleConfig +const badgeFormat = '%c%s%c'; // Same badge styling as DevTools. const badgeStyle = // We use a fixed background if light-dark is not supported, otherwise @@ -48,7 +49,7 @@ export function bindToConsole( newArgs.splice( offset, 1, - badgeFormat + newArgs[offset], + badgeFormat + ' ' + newArgs[offset], badgeStyle, pad + badgeName + pad, resetStyle, diff --git a/packages/react-client/src/ReactClientConsoleConfigPlain.js b/packages/react-client/src/ReactClientConsoleConfigPlain.js index 6b41ad4effe98..ee4c87ca61331 100644 --- a/packages/react-client/src/ReactClientConsoleConfigPlain.js +++ b/packages/react-client/src/ReactClientConsoleConfigPlain.js @@ -7,7 +7,8 @@ * @flow */ -const badgeFormat = '[%s] '; +// Keep in sync with ReactServerConsoleConfig +const badgeFormat = '[%s]'; const pad = ' '; const bind = Function.prototype.bind; @@ -38,7 +39,7 @@ export function bindToConsole( newArgs.splice( offset, 1, - badgeFormat + newArgs[offset], + badgeFormat + ' ' + newArgs[offset], pad + badgeName + pad, ); } else { diff --git a/packages/react-client/src/ReactClientConsoleConfigServer.js b/packages/react-client/src/ReactClientConsoleConfigServer.js index efbcd2865d712..6e69ef12a3ce6 100644 --- a/packages/react-client/src/ReactClientConsoleConfigServer.js +++ b/packages/react-client/src/ReactClientConsoleConfigServer.js @@ -7,8 +7,9 @@ * @flow */ +// Keep in sync with ReactServerConsoleConfig // This flips color using ANSI, then sets a color styling, then resets. -const badgeFormat = '\x1b[0m\x1b[7m%c%s\x1b[0m%c '; +const badgeFormat = '\x1b[0m\x1b[7m%c%s\x1b[0m%c'; // Same badge styling as DevTools. const badgeStyle = // We use a fixed background if light-dark is not supported, otherwise @@ -49,7 +50,7 @@ export function bindToConsole( newArgs.splice( offset, 1, - badgeFormat + newArgs[offset], + badgeFormat + ' ' + newArgs[offset], badgeStyle, pad + badgeName + pad, resetStyle, diff --git a/packages/react-client/src/ReactFlightClient.js b/packages/react-client/src/ReactFlightClient.js index 7d6bbd5c1fbb0..fc59a91fb2fac 100644 --- a/packages/react-client/src/ReactFlightClient.js +++ b/packages/react-client/src/ReactFlightClient.js @@ -10,10 +10,10 @@ import type { Thenable, ReactDebugInfo, + ReactDebugInfoEntry, ReactComponentInfo, - ReactEnvironmentInfo, ReactAsyncInfo, - ReactTimeInfo, + ReactIOInfo, ReactStackTrace, ReactFunctionLocation, ReactErrorInfoDev, @@ -47,6 +47,7 @@ import { enablePostpone, enableProfilerTimer, enableComponentPerformanceTrack, + enableAsyncDebugInfo, } from 'shared/ReactFeatureFlags'; import { @@ -54,6 +55,7 @@ import { resolveServerReference, preloadModule, requireModule, + getModuleDebugInfo, dispatchHint, readPartialStringChunk, readFinalStringChunk, @@ -75,7 +77,13 @@ import { markAllTracksInOrder, logComponentRender, logDedupedComponentRender, + logComponentAborted, logComponentErrored, + logIOInfo, + logIOInfoErrored, + logComponentAwait, + logComponentAwaitAborted, + logComponentAwaitErrored, } from './ReactFlightPerformanceTrack'; import { @@ -92,6 +100,8 @@ import {getOwnerStackByComponentInfoInDev} from 'shared/ReactComponentInfoStack' import {injectInternals} from './ReactFlightClientDevToolsHook'; +import {OMITTED_PROP_ERROR} from 'shared/ReactFlightPropertyAccess'; + import ReactVersion from 'shared/ReactVersion'; import isArray from 'shared/isArray'; @@ -151,50 +161,51 @@ const RESOLVED_MODEL = 'resolved_model'; const RESOLVED_MODULE = 'resolved_module'; const INITIALIZED = 'fulfilled'; const ERRORED = 'rejected'; +const HALTED = 'halted'; // DEV-only. Means it never resolves even if connection closes. type PendingChunk = { status: 'pending', - value: null | Array<(T) => mixed>, - reason: null | Array<(mixed) => mixed>, - _response: Response, + value: null | Array mixed)>, + reason: null | Array mixed)>, _children: Array> | ProfilingResult, // Profiling-only - _debugInfo?: null | ReactDebugInfo, // DEV-only + _debugChunk: null | SomeChunk, // DEV-only + _debugInfo: null | ReactDebugInfo, // DEV-only then(resolve: (T) => mixed, reject?: (mixed) => mixed): void, }; type BlockedChunk = { status: 'blocked', - value: null | Array<(T) => mixed>, - reason: null | Array<(mixed) => mixed>, - _response: Response, + value: null | Array mixed)>, + reason: null | Array mixed)>, _children: Array> | ProfilingResult, // Profiling-only - _debugInfo?: null | ReactDebugInfo, // DEV-only + _debugChunk: null, // DEV-only + _debugInfo: null | ReactDebugInfo, // DEV-only then(resolve: (T) => mixed, reject?: (mixed) => mixed): void, }; type ResolvedModelChunk = { status: 'resolved_model', value: UninitializedModel, - reason: null, - _response: Response, + reason: Response, _children: Array> | ProfilingResult, // Profiling-only - _debugInfo?: null | ReactDebugInfo, // DEV-only + _debugChunk: null | SomeChunk, // DEV-only + _debugInfo: null | ReactDebugInfo, // DEV-only then(resolve: (T) => mixed, reject?: (mixed) => mixed): void, }; type ResolvedModuleChunk = { status: 'resolved_module', value: ClientReference, reason: null, - _response: Response, _children: Array> | ProfilingResult, // Profiling-only - _debugInfo?: null | ReactDebugInfo, // DEV-only + _debugChunk: null, // DEV-only + _debugInfo: null | ReactDebugInfo, // DEV-only then(resolve: (T) => mixed, reject?: (mixed) => mixed): void, }; type InitializedChunk = { status: 'fulfilled', value: T, reason: null | FlightStreamController, - _response: Response, _children: Array> | ProfilingResult, // Profiling-only - _debugInfo?: null | ReactDebugInfo, // DEV-only + _debugChunk: null, // DEV-only + _debugInfo: null | ReactDebugInfo, // DEV-only then(resolve: (T) => mixed, reject?: (mixed) => mixed): void, }; type InitializedStreamChunk< @@ -203,18 +214,27 @@ type InitializedStreamChunk< status: 'fulfilled', value: T, reason: FlightStreamController, - _response: Response, _children: Array> | ProfilingResult, // Profiling-only - _debugInfo?: null | ReactDebugInfo, // DEV-only + _debugChunk: null, // DEV-only + _debugInfo: null | ReactDebugInfo, // DEV-only then(resolve: (ReadableStream) => mixed, reject?: (mixed) => mixed): void, }; type ErroredChunk = { status: 'rejected', value: null, reason: mixed, - _response: Response, _children: Array> | ProfilingResult, // Profiling-only - _debugInfo?: null | ReactDebugInfo, // DEV-only + _debugChunk: null, // DEV-only + _debugInfo: null | ReactDebugInfo, // DEV-only + then(resolve: (T) => mixed, reject?: (mixed) => mixed): void, +}; +type HaltedChunk = { + status: 'halted', + value: null, + reason: null, + _children: Array> | ProfilingResult, // Profiling-only + _debugChunk: null, // DEV-only + _debugInfo: null | ReactDebugInfo, // DEV-only then(resolve: (T) => mixed, reject?: (mixed) => mixed): void, }; type SomeChunk = @@ -223,23 +243,19 @@ type SomeChunk = | ResolvedModelChunk | ResolvedModuleChunk | InitializedChunk - | ErroredChunk; + | ErroredChunk + | HaltedChunk; // $FlowFixMe[missing-this-annot] -function ReactPromise( - status: any, - value: any, - reason: any, - response: Response, -) { +function ReactPromise(status: any, value: any, reason: any) { this.status = status; this.value = value; this.reason = reason; - this._response = response; if (enableProfilerTimer && enableComponentPerformanceTrack) { this._children = []; } if (__DEV__) { + this._debugChunk = null; this._debugInfo = null; } } @@ -262,28 +278,56 @@ ReactPromise.prototype.then = function ( initializeModuleChunk(chunk); break; } + if (__DEV__ && enableAsyncDebugInfo) { + // Because only native Promises get picked up when we're awaiting we need to wrap + // this in a native Promise in DEV. This means that these callbacks are no longer sync + // but the lazy initialization is still sync and the .value can be inspected after, + // allowing it to be read synchronously anyway. + const resolveCallback = resolve; + const rejectCallback = reject; + const wrapperPromise: Promise = new Promise((res, rej) => { + resolve = value => { + // $FlowFixMe + wrapperPromise._debugInfo = this._debugInfo; + res(value); + }; + reject = reason => { + // $FlowFixMe + wrapperPromise._debugInfo = this._debugInfo; + rej(reason); + }; + }); + wrapperPromise.then(resolveCallback, rejectCallback); + } // The status might have changed after initialization. switch (chunk.status) { case INITIALIZED: - resolve(chunk.value); + if (typeof resolve === 'function') { + resolve(chunk.value); + } break; case PENDING: case BLOCKED: - if (resolve) { + if (typeof resolve === 'function') { if (chunk.value === null) { - chunk.value = ([]: Array<(T) => mixed>); + chunk.value = ([]: Array mixed)>); } chunk.value.push(resolve); } - if (reject) { + if (typeof reject === 'function') { if (chunk.reason === null) { - chunk.reason = ([]: Array<(mixed) => mixed>); + chunk.reason = ([]: Array< + InitializationReference | (mixed => mixed), + >); } chunk.reason.push(reject); } break; + case HALTED: { + break; + } default: - if (reject) { + if (typeof reject === 'function') { reject(chunk.reason); } break; @@ -295,7 +339,14 @@ export type FindSourceMapURLCallback = ( environmentName: string, ) => null | string; -export type Response = { +export type DebugChannelCallback = (message: string) => void; + +export type DebugChannel = { + hasReadable: boolean, + callback: DebugChannelCallback | null, +}; + +type Response = { _bundlerConfig: ServerConsumerModuleMap, _serverReferenceConfig: null | ServerManifest, _moduleLoading: ModuleLoading, @@ -305,23 +356,71 @@ export type Response = { _chunks: Map>, _fromJSON: (key: string, value: JSONValue) => any, _stringDecoder: StringDecoder, - _rowState: RowParserState, - _rowID: number, // parts of a row ID parsed so far - _rowTag: number, // 0 indicates that we're currently parsing the row ID - _rowLength: number, // remaining bytes in the row. 0 indicates that we're looking for a newline. - _buffer: Array, // chunks received so far as part of this row _closed: boolean, _closedReason: mixed, _tempRefs: void | TemporaryReferenceSet, // the set temporary references can be resolved from _timeOrigin: number, // Profiling-only + _pendingInitialRender: null | TimeoutID, // Profiling-only, + _pendingChunks: number, // DEV-only + _weakResponse: WeakResponse, // DEV-only _debugRootOwner?: null | ReactComponentInfo, // DEV-only _debugRootStack?: null | Error, // DEV-only _debugRootTask?: null | ConsoleTask, // DEV-only _debugFindSourceMapURL?: void | FindSourceMapURLCallback, // DEV-only + _debugChannel?: void | DebugChannel, // DEV-only + _blockedConsole?: null | SomeChunk, // DEV-only _replayConsole: boolean, // DEV-only _rootEnvironmentName: string, // DEV-only, the requested environment name. }; +// This indirection exists only to clean up DebugChannel when all Lazy References are GC:ed. +// Therefore we only use the indirection in DEV. +type WeakResponse = { + weak: WeakRef, + response: null | Response, // This is null when there are no pending chunks. +}; + +export type {WeakResponse as Response}; + +function hasGCedResponse(weakResponse: WeakResponse): boolean { + return __DEV__ && weakResponse.weak.deref() === undefined; +} + +function unwrapWeakResponse(weakResponse: WeakResponse): Response { + if (__DEV__) { + const response = weakResponse.weak.deref(); + if (response === undefined) { + // eslint-disable-next-line react-internal/prod-error-codes + throw new Error( + 'We did not expect to receive new data after GC:ing the response.', + ); + } + return response; + } else { + return (weakResponse: any); // In prod we just use the real Response directly. + } +} + +function getWeakResponse(response: Response): WeakResponse { + if (__DEV__) { + return response._weakResponse; + } else { + return (response: any); // In prod we just use the real Response directly. + } +} + +function closeDebugChannel(debugChannel: DebugChannel): void { + if (debugChannel.callback) { + debugChannel.callback(''); + } +} + +// If FinalizationRegistry doesn't exist, we cannot use the debugChannel. +const debugChannelRegistry = + __DEV__ && typeof FinalizationRegistry === 'function' + ? new FinalizationRegistry(closeDebugChannel) + : null; + function readChunk(chunk: SomeChunk): T { // If we have resolved content, we try to initialize it first which // might put us back into one of the other states. @@ -339,6 +438,7 @@ function readChunk(chunk: SomeChunk): T { return chunk.value; case PENDING: case BLOCKED: + case HALTED: // eslint-disable-next-line no-throw-literal throw ((chunk: any): Thenable); default: @@ -346,19 +446,48 @@ function readChunk(chunk: SomeChunk): T { } } -export function getRoot(response: Response): Thenable { +export function getRoot(weakResponse: WeakResponse): Thenable { + const response = unwrapWeakResponse(weakResponse); const chunk = getChunk(response, 0); return (chunk: any); } function createPendingChunk(response: Response): PendingChunk { + if (__DEV__) { + // Retain a strong reference to the Response while we wait for the result. + if (response._pendingChunks++ === 0) { + response._weakResponse.response = response; + if (response._pendingInitialRender !== null) { + clearTimeout(response._pendingInitialRender); + response._pendingInitialRender = null; + } + } + } // $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors - return new ReactPromise(PENDING, null, null, response); + return new ReactPromise(PENDING, null, null); +} + +function releasePendingChunk(response: Response, chunk: SomeChunk): void { + if (__DEV__ && chunk.status === PENDING) { + if (--response._pendingChunks === 0) { + // We're no longer waiting for any more chunks. We can release the strong reference + // to the response. We'll regain it if we ask for any more data later on. + response._weakResponse.response = null; + // Wait a short period to see if any more chunks get asked for. E.g. by a React render. + // These chunks might discover more pending chunks. + // If we don't ask for more then we assume that those chunks weren't blocking initial + // render and are excluded from the performance track. + response._pendingInitialRender = setTimeout( + flushInitialRenderPerformance.bind(null, response), + 100, + ); + } + } } function createBlockedChunk(response: Response): BlockedChunk { // $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors - return new ReactPromise(BLOCKED, null, null, response); + return new ReactPromise(BLOCKED, null, null); } function createErrorChunk( @@ -366,27 +495,100 @@ function createErrorChunk( error: mixed, ): ErroredChunk { // $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors - return new ReactPromise(ERRORED, null, error, response); + return new ReactPromise(ERRORED, null, error); } -function wakeChunk(listeners: Array<(T) => mixed>, value: T): void { +function wakeChunk( + listeners: Array mixed)>, + value: T, + chunk: SomeChunk, +): void { for (let i = 0; i < listeners.length; i++) { const listener = listeners[i]; - listener(value); + if (typeof listener === 'function') { + listener(value); + } else { + fulfillReference(listener, value, chunk); + } } } +function rejectChunk( + listeners: Array mixed)>, + error: mixed, +): void { + for (let i = 0; i < listeners.length; i++) { + const listener = listeners[i]; + if (typeof listener === 'function') { + listener(error); + } else { + rejectReference(listener, error); + } + } +} + +function resolveBlockedCycle( + resolvedChunk: SomeChunk, + reference: InitializationReference, +): null | InitializationHandler { + const referencedChunk = reference.handler.chunk; + if (referencedChunk === null) { + return null; + } + if (referencedChunk === resolvedChunk) { + // We found the cycle. We can resolve the blocked cycle now. + return reference.handler; + } + const resolveListeners = referencedChunk.value; + if (resolveListeners !== null) { + for (let i = 0; i < resolveListeners.length; i++) { + const listener = resolveListeners[i]; + if (typeof listener !== 'function') { + const foundHandler = resolveBlockedCycle(resolvedChunk, listener); + if (foundHandler !== null) { + return foundHandler; + } + } + } + } + return null; +} + function wakeChunkIfInitialized( chunk: SomeChunk, - resolveListeners: Array<(T) => mixed>, - rejectListeners: null | Array<(mixed) => mixed>, + resolveListeners: Array mixed)>, + rejectListeners: null | Array mixed)>, ): void { switch (chunk.status) { case INITIALIZED: - wakeChunk(resolveListeners, chunk.value); + wakeChunk(resolveListeners, chunk.value, chunk); break; - case PENDING: case BLOCKED: + // It is possible that we're blocked on our own chunk if it's a cycle. + // Before adding back the listeners to the chunk, let's check if it would + // result in a cycle. + for (let i = 0; i < resolveListeners.length; i++) { + const listener = resolveListeners[i]; + if (typeof listener !== 'function') { + const reference: InitializationReference = listener; + const cyclicHandler = resolveBlockedCycle(chunk, reference); + if (cyclicHandler !== null) { + // This reference points back to this chunk. We can resolve the cycle by + // using the value from that handler. + fulfillReference(reference, cyclicHandler.value, chunk); + resolveListeners.splice(i, 1); + i--; + if (rejectListeners !== null) { + const rejectionIdx = rejectListeners.indexOf(reference); + if (rejectionIdx !== -1) { + rejectListeners.splice(rejectionIdx, 1); + } + } + } + } + } + // Fallthrough + case PENDING: if (chunk.value) { for (let i = 0; i < resolveListeners.length; i++) { chunk.value.push(resolveListeners[i]); @@ -408,13 +610,17 @@ function wakeChunkIfInitialized( break; case ERRORED: if (rejectListeners) { - wakeChunk(rejectListeners, chunk.reason); + rejectChunk(rejectListeners, chunk.reason); } break; } } -function triggerErrorOnChunk(chunk: SomeChunk, error: mixed): void { +function triggerErrorOnChunk( + response: Response, + chunk: SomeChunk, + error: mixed, +): void { if (chunk.status !== PENDING && chunk.status !== BLOCKED) { // If we get more data to an already resolved ID, we assume that it's // a stream chunk since any other row shouldn't have more than one entry. @@ -424,12 +630,46 @@ function triggerErrorOnChunk(chunk: SomeChunk, error: mixed): void { controller.error(error); return; } + releasePendingChunk(response, chunk); const listeners = chunk.reason; + + if (__DEV__ && chunk.status === PENDING) { + // Lazily initialize any debug info and block the initializing chunk on any unresolved entries. + if (chunk._debugChunk != null) { + const prevHandler = initializingHandler; + const prevChunk = initializingChunk; + initializingHandler = null; + const cyclicChunk: BlockedChunk = (chunk: any); + cyclicChunk.status = BLOCKED; + cyclicChunk.value = null; + cyclicChunk.reason = null; + if ((enableProfilerTimer && enableComponentPerformanceTrack) || __DEV__) { + initializingChunk = cyclicChunk; + } + try { + initializeDebugChunk(response, chunk); + chunk._debugChunk = null; + if (initializingHandler !== null) { + if (initializingHandler.errored) { + // Ignore error parsing debug info, we'll report the original error instead. + } else if (initializingHandler.deps > 0) { + // TODO: Block the resolution of the error until all the debug info has loaded. + // We currently don't have a way to throw an error after all dependencies have + // loaded because we currently treat errors as immediately cancelling the handler. + } + } + } finally { + initializingHandler = prevHandler; + initializingChunk = prevChunk; + } + } + } + const erroredChunk: ErroredChunk = (chunk: any); erroredChunk.status = ERRORED; erroredChunk.reason = error; if (listeners !== null) { - wakeChunk(listeners, error); + rejectChunk(listeners, error); } } @@ -438,7 +678,7 @@ function createResolvedModelChunk( value: UninitializedModel, ): ResolvedModelChunk { // $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors - return new ReactPromise(RESOLVED_MODEL, value, null, response); + return new ReactPromise(RESOLVED_MODEL, value, response); } function createResolvedModuleChunk( @@ -446,7 +686,7 @@ function createResolvedModuleChunk( value: ClientReference, ): ResolvedModuleChunk { // $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors - return new ReactPromise(RESOLVED_MODULE, value, null, response); + return new ReactPromise(RESOLVED_MODULE, value, null); } function createInitializedTextChunk( @@ -454,7 +694,7 @@ function createInitializedTextChunk( value: string, ): InitializedChunk { // $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors - return new ReactPromise(INITIALIZED, value, null, response); + return new ReactPromise(INITIALIZED, value, null); } function createInitializedBufferChunk( @@ -462,7 +702,7 @@ function createInitializedBufferChunk( value: $ArrayBufferView | ArrayBuffer, ): InitializedChunk { // $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors - return new ReactPromise(INITIALIZED, value, null, response); + return new ReactPromise(INITIALIZED, value, null); } function createInitializedIteratorResultChunk( @@ -471,12 +711,7 @@ function createInitializedIteratorResultChunk( done: boolean, ): InitializedChunk> { // $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors - return new ReactPromise( - INITIALIZED, - {done: done, value: value}, - null, - response, - ); + return new ReactPromise(INITIALIZED, {done: done, value: value}, null); } function createInitializedStreamChunk< @@ -489,7 +724,7 @@ function createInitializedStreamChunk< // We use the reason field to stash the controller since we already have that // field. It's a bit of a hack but efficient. // $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors - return new ReactPromise(INITIALIZED, value, controller, response); + return new ReactPromise(INITIALIZED, value, controller); } function createResolvedIteratorResultChunk( @@ -501,10 +736,11 @@ function createResolvedIteratorResultChunk( const iteratorResultJSON = (done ? '{"done":true,"value":' : '{"done":false,"value":') + value + '}'; // $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors - return new ReactPromise(RESOLVED_MODEL, iteratorResultJSON, null, response); + return new ReactPromise(RESOLVED_MODEL, iteratorResultJSON, response); } function resolveIteratorResultChunk( + response: Response, chunk: SomeChunk>, value: UninitializedModel, done: boolean, @@ -512,10 +748,11 @@ function resolveIteratorResultChunk( // To reuse code as much code as possible we add the wrapper element as part of the JSON. const iteratorResultJSON = (done ? '{"done":true,"value":' : '{"done":false,"value":') + value + '}'; - resolveModelChunk(chunk, iteratorResultJSON); + resolveModelChunk(response, chunk, iteratorResultJSON); } function resolveModelChunk( + response: Response, chunk: SomeChunk, value: UninitializedModel, ): void { @@ -527,11 +764,13 @@ function resolveModelChunk( controller.enqueueModel(value); return; } + releasePendingChunk(response, chunk); const resolveListeners = chunk.value; const rejectListeners = chunk.reason; const resolvedChunk: ResolvedModelChunk = (chunk: any); resolvedChunk.status = RESOLVED_MODEL; resolvedChunk.value = value; + resolvedChunk.reason = response; if (resolveListeners !== null) { // This is unfortunate that we're reading this eagerly if // we already have listeners attached since they might no @@ -543,6 +782,7 @@ function resolveModelChunk( } function resolveModuleChunk( + response: Response, chunk: SomeChunk, value: ClientReference, ): void { @@ -550,33 +790,132 @@ function resolveModuleChunk( // We already resolved. We didn't expect to see this. return; } + releasePendingChunk(response, chunk); const resolveListeners = chunk.value; const rejectListeners = chunk.reason; const resolvedChunk: ResolvedModuleChunk = (chunk: any); resolvedChunk.status = RESOLVED_MODULE; resolvedChunk.value = value; + if (__DEV__) { + const debugInfo = getModuleDebugInfo(value); + if (debugInfo !== null && resolvedChunk._debugInfo != null) { + // Add to the live set if it was already initialized. + // $FlowFixMe[method-unbinding] + resolvedChunk._debugInfo.push.apply(resolvedChunk._debugInfo, debugInfo); + } else { + resolvedChunk._debugInfo = debugInfo; + } + } if (resolveListeners !== null) { initializeModuleChunk(resolvedChunk); wakeChunkIfInitialized(chunk, resolveListeners, rejectListeners); } } +type InitializationReference = { + response: Response, // TODO: Remove Response from here and pass it through instead. + handler: InitializationHandler, + parentObject: Object, + key: string, + map: ( + response: Response, + model: any, + parentObject: Object, + key: string, + ) => any, + path: Array, +}; type InitializationHandler = { parent: null | InitializationHandler, chunk: null | BlockedChunk, value: any, + reason: any, deps: number, errored: boolean, }; let initializingHandler: null | InitializationHandler = null; let initializingChunk: null | BlockedChunk = null; +function initializeDebugChunk( + response: Response, + chunk: ResolvedModelChunk | PendingChunk, +): void { + const debugChunk = chunk._debugChunk; + if (debugChunk !== null) { + const debugInfo = chunk._debugInfo || (chunk._debugInfo = []); + try { + if (debugChunk.status === RESOLVED_MODEL) { + // Find the index of this debug info by walking the linked list. + let idx = debugInfo.length; + let c = debugChunk._debugChunk; + while (c !== null) { + if (c.status !== INITIALIZED) { + idx++; + } + c = c._debugChunk; + } + // Initializing the model for the first time. + initializeModelChunk(debugChunk); + const initializedChunk = ((debugChunk: any): SomeChunk); + switch (initializedChunk.status) { + case INITIALIZED: { + debugInfo[idx] = initializeDebugInfo( + response, + initializedChunk.value, + ); + break; + } + case BLOCKED: + case PENDING: { + waitForReference( + initializedChunk, + debugInfo, + '' + idx, + response, + initializeDebugInfo, + [''], // path + ); + break; + } + default: + throw initializedChunk.reason; + } + } else { + switch (debugChunk.status) { + case INITIALIZED: { + // Already done. + break; + } + case BLOCKED: + case PENDING: { + // Signal to the caller that we need to wait. + waitForReference( + debugChunk, + {}, // noop, since we'll have already added an entry to debug info + 'debug', // noop, but we need it to not be empty string since that indicates the root object + response, + initializeDebugInfo, + [''], // path + ); + break; + } + default: + throw debugChunk.reason; + } + } + } catch (error) { + triggerErrorOnChunk(response, chunk, error); + } + } +} + function initializeModelChunk(chunk: ResolvedModelChunk): void { const prevHandler = initializingHandler; const prevChunk = initializingChunk; initializingHandler = null; const resolvedModel = chunk.value; + const response = chunk.reason; // We go to the BLOCKED state until we've fully resolved this. // We do this before parsing in case we try to initialize the same chunk @@ -586,12 +925,18 @@ function initializeModelChunk(chunk: ResolvedModelChunk): void { cyclicChunk.value = null; cyclicChunk.reason = null; - if (enableProfilerTimer && enableComponentPerformanceTrack) { + if ((enableProfilerTimer && enableComponentPerformanceTrack) || __DEV__) { initializingChunk = cyclicChunk; } + if (__DEV__) { + // Lazily initialize any debug info and block the initializing chunk on any unresolved entries. + initializeDebugChunk(response, chunk); + chunk._debugChunk = null; + } + try { - const value: T = parseModel(chunk._response, resolvedModel); + const value: T = parseModel(response, resolvedModel); // Invoke any listeners added while resolving this model. I.e. cyclic // references. This may or may not fully resolve the model depending on // if they were blocked. @@ -599,11 +944,11 @@ function initializeModelChunk(chunk: ResolvedModelChunk): void { if (resolveListeners !== null) { cyclicChunk.value = null; cyclicChunk.reason = null; - wakeChunk(resolveListeners, value); + wakeChunk(resolveListeners, value, cyclicChunk); } if (initializingHandler !== null) { if (initializingHandler.errored) { - throw initializingHandler.value; + throw initializingHandler.reason; } if (initializingHandler.deps > 0) { // We discovered new dependencies on modules that are not yet resolved. @@ -622,7 +967,7 @@ function initializeModelChunk(chunk: ResolvedModelChunk): void { erroredChunk.reason = error; } finally { initializingHandler = prevHandler; - if (enableProfilerTimer && enableComponentPerformanceTrack) { + if ((enableProfilerTimer && enableComponentPerformanceTrack) || __DEV__) { initializingChunk = prevChunk; } } @@ -643,7 +988,15 @@ function initializeModuleChunk(chunk: ResolvedModuleChunk): void { // Report that any missing chunks in the model is now going to throw this // error upon read. Also notify any pending promises. -export function reportGlobalError(response: Response, error: Error): void { +export function reportGlobalError( + weakResponse: WeakResponse, + error: Error, +): void { + if (hasGCedResponse(weakResponse)) { + // Ignore close signal if we are not awaiting any more pending chunks. + return; + } + const response = unwrapWeakResponse(weakResponse); response._closed = true; response._closedReason = error; response._chunks.forEach(chunk => { @@ -651,18 +1004,22 @@ export function reportGlobalError(response: Response, error: Error): void { // trigger an error but if it wasn't then we need to // because we won't be getting any new data to resolve it. if (chunk.status === PENDING) { - triggerErrorOnChunk(chunk, error); + triggerErrorOnChunk(response, chunk, error); } }); - if (enableProfilerTimer && enableComponentPerformanceTrack) { - markAllTracksInOrder(); - flushComponentPerformance( - response, - getChunk(response, 0), - 0, - -Infinity, - -Infinity, - ); + if (__DEV__) { + const debugChannel = response._debugChannel; + if (debugChannel !== undefined) { + // If we don't have any more ways of reading data, we don't have to send + // any more neither. So we close the writable side. + closeDebugChannel(debugChannel); + response._debugChannel = undefined; + // Make sure the debug channel is not closed a second time when the + // Response gets GC:ed. + if (debugChannelRegistry !== null) { + debugChannelRegistry.unregister(response); + } + } } } @@ -672,6 +1029,14 @@ function nullRefGetter() { } } +function getIOInfoTaskName(ioInfo: ReactIOInfo): string { + return ioInfo.name || 'unknown'; +} + +function getAsyncInfoTaskName(asyncInfo: ReactAsyncInfo): string { + return 'await ' + getIOInfoTaskName(asyncInfo.awaited); +} + function getServerComponentTaskName(componentInfo: ReactComponentInfo): string { return '<' + (componentInfo.name || '...') + '>'; } @@ -709,14 +1074,105 @@ function getTaskName(type: mixed): string { } } +function initializeElement( + response: Response, + element: any, + lazyType: null | LazyComponent< + React$Element, + SomeChunk>, + >, +): void { + if (!__DEV__) { + return; + } + const stack = element._debugStack; + const owner = element._owner; + if (owner === null) { + element._owner = response._debugRootOwner; + } + let env = response._rootEnvironmentName; + if (owner !== null && owner.env != null) { + // Interestingly we don't actually have the environment name of where + // this JSX was created if it doesn't have an owner but if it does + // it must be the same environment as the owner. We could send it separately + // but it seems a bit unnecessary for this edge case. + env = owner.env; + } + let normalizedStackTrace: null | Error = null; + if (owner === null && response._debugRootStack != null) { + // We override the stack if we override the owner since the stack where the root JSX + // was created on the server isn't very useful but where the request was made is. + normalizedStackTrace = response._debugRootStack; + } else if (stack !== null) { + // We create a fake stack and then create an Error object inside of it. + // This means that the stack trace is now normalized into the native format + // of the browser and the stack frames will have been registered with + // source mapping information. + // This can unfortunately happen within a user space callstack which will + // remain on the stack. + normalizedStackTrace = createFakeJSXCallStackInDEV(response, stack, env); + } + element._debugStack = normalizedStackTrace; + let task: null | ConsoleTask = null; + if (supportsCreateTask && stack !== null) { + const createTaskFn = (console: any).createTask.bind( + console, + getTaskName(element.type), + ); + const callStack = buildFakeCallStack( + response, + stack, + env, + false, + createTaskFn, + ); + // This owner should ideally have already been initialized to avoid getting + // user stack frames on the stack. + const ownerTask = + owner === null ? null : initializeFakeTask(response, owner); + if (ownerTask === null) { + const rootTask = response._debugRootTask; + if (rootTask != null) { + task = rootTask.run(callStack); + } else { + task = callStack(); + } + } else { + task = ownerTask.run(callStack); + } + } + element._debugTask = task; + + // This owner should ideally have already been initialized to avoid getting + // user stack frames on the stack. + if (owner !== null) { + initializeFakeStack(response, owner); + } + + // In case the JSX runtime has validated the lazy type as a static child, we + // need to transfer this information to the element. + if ( + lazyType && + lazyType._store && + lazyType._store.validated && + !element._store.validated + ) { + element._store.validated = lazyType._store.validated; + } + + // TODO: We should be freezing the element but currently, we might write into + // _debugInfo later. We could move it into _store which remains mutable. + Object.freeze(element.props); +} + function createElement( response: Response, type: mixed, key: mixed, props: mixed, - owner: null | ReactComponentInfo, // DEV-only - stack: null | ReactStackTrace, // DEV-only - validated: number, // DEV-only + owner: ?ReactComponentInfo, // DEV-only + stack: ?ReactStackTrace, // DEV-only + validated: 0 | 1 | 2, // DEV-only ): | React$Element | LazyComponent, SomeChunk>> { @@ -728,7 +1184,7 @@ function createElement( type, key, props, - _owner: __DEV__ && owner === null ? response._debugRootOwner : owner, + _owner: owner === undefined ? null : owner, }: any); Object.defineProperty(element, 'ref', { enumerable: false, @@ -766,69 +1222,18 @@ function createElement( writable: true, value: null, }); - let env = response._rootEnvironmentName; - if (owner !== null && owner.env != null) { - // Interestingly we don't actually have the environment name of where - // this JSX was created if it doesn't have an owner but if it does - // it must be the same environment as the owner. We could send it separately - // but it seems a bit unnecessary for this edge case. - env = owner.env; - } - let normalizedStackTrace: null | Error = null; - if (owner === null && response._debugRootStack != null) { - // We override the stack if we override the owner since the stack where the root JSX - // was created on the server isn't very useful but where the request was made is. - normalizedStackTrace = response._debugRootStack; - } else if (stack !== null) { - // We create a fake stack and then create an Error object inside of it. - // This means that the stack trace is now normalized into the native format - // of the browser and the stack frames will have been registered with - // source mapping information. - // This can unfortunately happen within a user space callstack which will - // remain on the stack. - normalizedStackTrace = createFakeJSXCallStackInDEV(response, stack, env); - } Object.defineProperty(element, '_debugStack', { configurable: false, enumerable: false, writable: true, - value: normalizedStackTrace, + value: stack === undefined ? null : stack, }); - - let task: null | ConsoleTask = null; - if (supportsCreateTask && stack !== null) { - const createTaskFn = (console: any).createTask.bind( - console, - getTaskName(type), - ); - const callStack = buildFakeCallStack(response, stack, env, createTaskFn); - // This owner should ideally have already been initialized to avoid getting - // user stack frames on the stack. - const ownerTask = - owner === null ? null : initializeFakeTask(response, owner, env); - if (ownerTask === null) { - const rootTask = response._debugRootTask; - if (rootTask != null) { - task = rootTask.run(callStack); - } else { - task = callStack(); - } - } else { - task = ownerTask.run(callStack); - } - } Object.defineProperty(element, '_debugTask', { configurable: false, enumerable: false, writable: true, - value: task, + value: null, }); - - // This owner should ideally have already been initialized to avoid getting - // user stack frames on the stack. - if (owner !== null) { - initializeFakeStack(response, owner); - } } if (initializingHandler !== null) { @@ -841,9 +1246,10 @@ function createElement( // into a Lazy so that we can still render up until that Lazy is rendered. const erroredChunk: ErroredChunk> = createErrorChunk( response, - handler.value, + handler.reason, ); if (__DEV__) { + initializeElement(response, element, null); // Conceptually the error happened inside this Element but right before // it was rendered. We don't have a client side component to render but // we can add some DebugInfo to explain that this was conceptually a @@ -862,7 +1268,7 @@ function createElement( } erroredChunk._debugInfo = [erroredComponent]; } - return createLazyChunkWrapper(erroredChunk); + return createLazyChunkWrapper(erroredChunk, validated); } if (handler.deps > 0) { // We have blocked references inside this Element but we can turn this into @@ -871,16 +1277,17 @@ function createElement( createBlockedChunk(response); handler.value = element; handler.chunk = blockedChunk; + const lazyType = createLazyChunkWrapper(blockedChunk, validated); if (__DEV__) { - const freeze = Object.freeze.bind(Object, element.props); - blockedChunk.then(freeze, freeze); + // After we have initialized any blocked references, initialize stack etc. + const init = initializeElement.bind(null, response, element, lazyType); + blockedChunk.then(init, init); } - return createLazyChunkWrapper(blockedChunk); + return lazyType; } - } else if (__DEV__) { - // TODO: We should be freezing the element but currently, we might write into - // _debugInfo later. We could move it into _store which remains mutable. - Object.freeze(element.props); + } + if (__DEV__) { + initializeElement(response, element, null); } return element; @@ -888,6 +1295,7 @@ function createElement( function createLazyChunkWrapper( chunk: SomeChunk, + validated: 0 | 1 | 2, // DEV-only ): LazyComponent> { const lazyType: LazyComponent> = { $$typeof: REACT_LAZY_TYPE, @@ -897,8 +1305,10 @@ function createLazyChunkWrapper( if (__DEV__) { // Ensure we have a live array to track future debug info. const chunkDebugInfo: ReactDebugInfo = - chunk._debugInfo || (chunk._debugInfo = []); + chunk._debugInfo || (chunk._debugInfo = ([]: ReactDebugInfo)); lazyType._debugInfo = chunkDebugInfo; + // Initialize a store for key validation by the JSX runtime. + lazyType._store = {validated: validated}; } return lazyType; } @@ -919,14 +1329,222 @@ function getChunk(response: Response, id: number): SomeChunk { return chunk; } +function fulfillReference( + reference: InitializationReference, + value: any, + fulfilledChunk: SomeChunk, +): void { + const {response, handler, parentObject, key, map, path} = reference; + + for (let i = 1; i < path.length; i++) { + while (value.$$typeof === REACT_LAZY_TYPE) { + // We never expect to see a Lazy node on this path because we encode those as + // separate models. This must mean that we have inserted an extra lazy node + // e.g. to replace a blocked element. We must instead look for it inside. + const referencedChunk: SomeChunk = value._payload; + if (referencedChunk === handler.chunk) { + // This is a reference to the thing we're currently blocking. We can peak + // inside of it to get the value. + value = handler.value; + continue; + } else { + switch (referencedChunk.status) { + case RESOLVED_MODEL: + initializeModelChunk(referencedChunk); + break; + case RESOLVED_MODULE: + initializeModuleChunk(referencedChunk); + break; + } + switch (referencedChunk.status) { + case INITIALIZED: { + value = referencedChunk.value; + continue; + } + case BLOCKED: { + // It is possible that we're blocked on our own chunk if it's a cycle. + // Before adding the listener to the inner chunk, let's check if it would + // result in a cycle. + const cyclicHandler = resolveBlockedCycle( + referencedChunk, + reference, + ); + if (cyclicHandler !== null) { + // This reference points back to this chunk. We can resolve the cycle by + // using the value from that handler. + value = cyclicHandler.value; + continue; + } + // Fallthrough + } + case PENDING: { + // If we're not yet initialized we need to skip what we've already drilled + // through and then wait for the next value to become available. + path.splice(0, i - 1); + // Add "listener" to our new chunk dependency. + if (referencedChunk.value === null) { + referencedChunk.value = [reference]; + } else { + referencedChunk.value.push(reference); + } + if (referencedChunk.reason === null) { + referencedChunk.reason = [reference]; + } else { + referencedChunk.reason.push(reference); + } + return; + } + case HALTED: { + // Do nothing. We couldn't fulfill. + // TODO: Mark downstreams as halted too. + return; + } + default: { + rejectReference(reference, referencedChunk.reason); + return; + } + } + } + } + value = value[path[i]]; + } + const mappedValue = map(response, value, parentObject, key); + parentObject[key] = mappedValue; + + transferReferencedDebugInfo(handler.chunk, fulfilledChunk, mappedValue); + + // If this is the root object for a model reference, where `handler.value` + // is a stale `null`, the resolved value can be used directly. + if (key === '' && handler.value === null) { + handler.value = mappedValue; + } + + // If the parent object is an unparsed React element tuple, we also need to + // update the props and owner of the parsed element object (i.e. + // handler.value). + if ( + parentObject[0] === REACT_ELEMENT_TYPE && + typeof handler.value === 'object' && + handler.value !== null && + handler.value.$$typeof === REACT_ELEMENT_TYPE + ) { + const element: any = handler.value; + switch (key) { + case '3': + element.props = mappedValue; + break; + case '4': + if (__DEV__) { + element._owner = mappedValue; + } + break; + case '5': + if (__DEV__) { + element._debugStack = mappedValue; + } + break; + } + } + + handler.deps--; + + if (handler.deps === 0) { + const chunk = handler.chunk; + if (chunk === null || chunk.status !== BLOCKED) { + return; + } + const resolveListeners = chunk.value; + const initializedChunk: InitializedChunk = (chunk: any); + initializedChunk.status = INITIALIZED; + initializedChunk.value = handler.value; + initializedChunk.reason = handler.reason; // Used by streaming chunks + if (resolveListeners !== null) { + wakeChunk(resolveListeners, handler.value, initializedChunk); + } + } +} + +function rejectReference( + reference: InitializationReference, + error: mixed, +): void { + const {handler, response} = reference; + + if (handler.errored) { + // We've already errored. We could instead build up an AggregateError + // but if there are multiple errors we just take the first one like + // Promise.all. + return; + } + const blockedValue = handler.value; + handler.errored = true; + handler.value = null; + handler.reason = error; + const chunk = handler.chunk; + if (chunk === null || chunk.status !== BLOCKED) { + return; + } + + if (__DEV__) { + if ( + typeof blockedValue === 'object' && + blockedValue !== null && + blockedValue.$$typeof === REACT_ELEMENT_TYPE + ) { + const element = blockedValue; + // Conceptually the error happened inside this Element but right before + // it was rendered. We don't have a client side component to render but + // we can add some DebugInfo to explain that this was conceptually a + // Server side error that errored inside this element. That way any stack + // traces will point to the nearest JSX that errored - e.g. during + // serialization. + const erroredComponent: ReactComponentInfo = { + name: getComponentNameFromType(element.type) || '', + owner: element._owner, + }; + // $FlowFixMe[cannot-write] + erroredComponent.debugStack = element._debugStack; + if (supportsCreateTask) { + // $FlowFixMe[cannot-write] + erroredComponent.debugTask = element._debugTask; + } + const chunkDebugInfo: ReactDebugInfo = + chunk._debugInfo || (chunk._debugInfo = []); + chunkDebugInfo.push(erroredComponent); + } + } + + triggerErrorOnChunk(response, chunk, error); +} + function waitForReference( - referencedChunk: SomeChunk, + referencedChunk: PendingChunk | BlockedChunk, parentObject: Object, key: string, response: Response, map: (response: Response, model: any, parentObject: Object, key: string) => T, path: Array, ): T { + if ( + __DEV__ && + (response._debugChannel === undefined || + !response._debugChannel.hasReadable) + ) { + if ( + referencedChunk.status === PENDING && + parentObject[0] === REACT_ELEMENT_TYPE && + (key === '4' || key === '5') + ) { + // If the parent object is an unparsed React element tuple, and this is a reference + // to the owner or debug stack. Then we expect the chunk to have been emitted earlier + // in the stream. It might be blocked on other things but chunk should no longer be pending. + // If it's still pending that suggests that it was referencing an object in the debug + // channel, but no debug channel was wired up so it's missing. In this case we can just + // drop the debug info instead of halting the whole stream. + return (null: any); + } + } + let handler: InitializationHandler; if (initializingHandler) { handler = initializingHandler; @@ -936,133 +1554,33 @@ function waitForReference( parent: null, chunk: null, value: null, + reason: null, deps: 1, errored: false, }; } - function fulfill(value: any): void { - for (let i = 1; i < path.length; i++) { - while (value.$$typeof === REACT_LAZY_TYPE) { - // We never expect to see a Lazy node on this path because we encode those as - // separate models. This must mean that we have inserted an extra lazy node - // e.g. to replace a blocked element. We must instead look for it inside. - const chunk: SomeChunk = value._payload; - if (chunk === handler.chunk) { - // This is a reference to the thing we're currently blocking. We can peak - // inside of it to get the value. - value = handler.value; - continue; - } else if (chunk.status === INITIALIZED) { - value = chunk.value; - continue; - } else { - // If we're not yet initialized we need to skip what we've already drilled - // through and then wait for the next value to become available. - path.splice(0, i - 1); - chunk.then(fulfill, reject); - return; - } - } - value = value[path[i]]; - } - const mappedValue = map(response, value, parentObject, key); - parentObject[key] = mappedValue; - - // If this is the root object for a model reference, where `handler.value` - // is a stale `null`, the resolved value can be used directly. - if (key === '' && handler.value === null) { - handler.value = mappedValue; - } - - // If the parent object is an unparsed React element tuple, we also need to - // update the props and owner of the parsed element object (i.e. - // handler.value). - if ( - parentObject[0] === REACT_ELEMENT_TYPE && - typeof handler.value === 'object' && - handler.value !== null && - handler.value.$$typeof === REACT_ELEMENT_TYPE - ) { - const element: any = handler.value; - switch (key) { - case '3': - element.props = mappedValue; - break; - case '4': - if (__DEV__) { - element._owner = mappedValue; - } - break; - } - } - - handler.deps--; + const reference: InitializationReference = { + response, + handler, + parentObject, + key, + map, + path, + }; - if (handler.deps === 0) { - const chunk = handler.chunk; - if (chunk === null || chunk.status !== BLOCKED) { - return; - } - const resolveListeners = chunk.value; - const initializedChunk: InitializedChunk = (chunk: any); - initializedChunk.status = INITIALIZED; - initializedChunk.value = handler.value; - if (resolveListeners !== null) { - wakeChunk(resolveListeners, handler.value); - } - } + // Add "listener". + if (referencedChunk.value === null) { + referencedChunk.value = [reference]; + } else { + referencedChunk.value.push(reference); } - - function reject(error: mixed): void { - if (handler.errored) { - // We've already errored. We could instead build up an AggregateError - // but if there are multiple errors we just take the first one like - // Promise.all. - return; - } - const blockedValue = handler.value; - handler.errored = true; - handler.value = error; - const chunk = handler.chunk; - if (chunk === null || chunk.status !== BLOCKED) { - return; - } - - if (__DEV__) { - if ( - typeof blockedValue === 'object' && - blockedValue !== null && - blockedValue.$$typeof === REACT_ELEMENT_TYPE - ) { - const element = blockedValue; - // Conceptually the error happened inside this Element but right before - // it was rendered. We don't have a client side component to render but - // we can add some DebugInfo to explain that this was conceptually a - // Server side error that errored inside this element. That way any stack - // traces will point to the nearest JSX that errored - e.g. during - // serialization. - const erroredComponent: ReactComponentInfo = { - name: getComponentNameFromType(element.type) || '', - owner: element._owner, - }; - // $FlowFixMe[cannot-write] - erroredComponent.debugStack = element._debugStack; - if (supportsCreateTask) { - // $FlowFixMe[cannot-write] - erroredComponent.debugTask = element._debugTask; - } - const chunkDebugInfo: ReactDebugInfo = - chunk._debugInfo || (chunk._debugInfo = []); - chunkDebugInfo.push(erroredComponent); - } - } - - triggerErrorOnChunk(chunk, error); + if (referencedChunk.reason === null) { + referencedChunk.reason = [reference]; + } else { + referencedChunk.reason.push(reference); } - referencedChunk.then(fulfill, reject); - // Return a place holder value for now. return (null: any); } @@ -1123,6 +1641,7 @@ function loadServerReference, T>( parent: null, chunk: null, value: null, + reason: null, deps: 1, errored: false, }; @@ -1187,7 +1706,7 @@ function loadServerReference, T>( initializedChunk.status = INITIALIZED; initializedChunk.value = handler.value; if (resolveListeners !== null) { - wakeChunk(resolveListeners, handler.value); + wakeChunk(resolveListeners, handler.value, initializedChunk); } } } @@ -1201,7 +1720,8 @@ function loadServerReference, T>( } const blockedValue = handler.value; handler.errored = true; - handler.value = error; + handler.value = null; + handler.reason = error; const chunk = handler.chunk; if (chunk === null || chunk.status !== BLOCKED) { return; @@ -1236,7 +1756,7 @@ function loadServerReference, T>( } } - triggerErrorOnChunk(chunk, error); + triggerErrorOnChunk(response, chunk, error); } promise.then(fulfill, reject); @@ -1245,6 +1765,49 @@ function loadServerReference, T>( return (null: any); } +function transferReferencedDebugInfo( + parentChunk: null | SomeChunk, + referencedChunk: SomeChunk, + referencedValue: mixed, +): void { + if (__DEV__ && referencedChunk._debugInfo) { + const referencedDebugInfo = referencedChunk._debugInfo; + // If we have a direct reference to an object that was rendered by a synchronous + // server component, it might have some debug info about how it was rendered. + // We forward this to the underlying object. This might be a React Element or + // an Array fragment. + // If this was a string / number return value we lose the debug info. We choose + // that tradeoff to allow sync server components to return plain values and not + // use them as React Nodes necessarily. We could otherwise wrap them in a Lazy. + if ( + typeof referencedValue === 'object' && + referencedValue !== null && + (isArray(referencedValue) || + typeof referencedValue[ASYNC_ITERATOR] === 'function' || + referencedValue.$$typeof === REACT_ELEMENT_TYPE) && + !referencedValue._debugInfo + ) { + // We should maybe use a unique symbol for arrays but this is a React owned array. + // $FlowFixMe[prop-missing]: This should be added to elements. + Object.defineProperty((referencedValue: any), '_debugInfo', { + configurable: false, + enumerable: false, + writable: true, + value: referencedDebugInfo, + }); + } + // We also add it to the initializing chunk since the resolution of that promise is + // also blocked by these. By adding it to both we can track it even if the array/element + // is extracted, or if the root is rendered as is. + if (parentChunk !== null) { + const parentDebugInfo = + parentChunk._debugInfo || (parentChunk._debugInfo = []); + // $FlowFixMe[method-unbinding] + parentDebugInfo.push.apply(parentDebugInfo, referencedDebugInfo); + } + } +} + function getOutlinedModel( response: Response, reference: string, @@ -1275,63 +1838,110 @@ function getOutlinedModel( for (let i = 1; i < path.length; i++) { while (value.$$typeof === REACT_LAZY_TYPE) { const referencedChunk: SomeChunk = value._payload; - if (referencedChunk.status === INITIALIZED) { - value = referencedChunk.value; - } else { - return waitForReference( - referencedChunk, - parentObject, - key, - response, - map, - path.slice(i - 1), - ); + switch (referencedChunk.status) { + case RESOLVED_MODEL: + initializeModelChunk(referencedChunk); + break; + case RESOLVED_MODULE: + initializeModuleChunk(referencedChunk); + break; + } + switch (referencedChunk.status) { + case INITIALIZED: { + value = referencedChunk.value; + break; + } + case BLOCKED: + case PENDING: { + return waitForReference( + referencedChunk, + parentObject, + key, + response, + map, + path.slice(i - 1), + ); + } + case HALTED: { + // Add a dependency that will never resolve. + // TODO: Mark downstreams as halted too. + let handler: InitializationHandler; + if (initializingHandler) { + handler = initializingHandler; + handler.deps++; + } else { + handler = initializingHandler = { + parent: null, + chunk: null, + value: null, + reason: null, + deps: 1, + errored: false, + }; + } + return (null: any); + } + default: { + // This is an error. Instead of erroring directly, we're going to encode this on + // an initialization handler so that we can catch it at the nearest Element. + if (initializingHandler) { + initializingHandler.errored = true; + initializingHandler.value = null; + initializingHandler.reason = referencedChunk.reason; + } else { + initializingHandler = { + parent: null, + chunk: null, + value: null, + reason: referencedChunk.reason, + deps: 0, + errored: true, + }; + } + return (null: any); + } } } value = value[path[i]]; } const chunkValue = map(response, value, parentObject, key); - if (__DEV__ && chunk._debugInfo) { - // If we have a direct reference to an object that was rendered by a synchronous - // server component, it might have some debug info about how it was rendered. - // We forward this to the underlying object. This might be a React Element or - // an Array fragment. - // If this was a string / number return value we lose the debug info. We choose - // that tradeoff to allow sync server components to return plain values and not - // use them as React Nodes necessarily. We could otherwise wrap them in a Lazy. - if ( - typeof chunkValue === 'object' && - chunkValue !== null && - (isArray(chunkValue) || - typeof chunkValue[ASYNC_ITERATOR] === 'function' || - chunkValue.$$typeof === REACT_ELEMENT_TYPE) && - !chunkValue._debugInfo - ) { - // We should maybe use a unique symbol for arrays but this is a React owned array. - // $FlowFixMe[prop-missing]: This should be added to elements. - Object.defineProperty((chunkValue: any), '_debugInfo', { - configurable: false, - enumerable: false, - writable: true, - value: chunk._debugInfo, - }); - } - } + transferReferencedDebugInfo(initializingChunk, chunk, chunkValue); return chunkValue; case PENDING: case BLOCKED: return waitForReference(chunk, parentObject, key, response, map, path); + case HALTED: { + // Add a dependency that will never resolve. + // TODO: Mark downstreams as halted too. + let handler: InitializationHandler; + if (initializingHandler) { + handler = initializingHandler; + handler.deps++; + } else { + handler = initializingHandler = { + parent: null, + chunk: null, + value: null, + reason: null, + deps: 1, + errored: false, + }; + } + return (null: any); + } default: // This is an error. Instead of erroring directly, we're going to encode this on // an initialization handler so that we can catch it at the nearest Element. if (initializingHandler) { initializingHandler.errored = true; - initializingHandler.value = chunk.reason; + initializingHandler.value = null; + initializingHandler.reason = chunk.reason; } else { initializingHandler = { parent: null, chunk: null, - value: chunk.reason, + value: null, + reason: chunk.reason, deps: 0, errored: true, }; @@ -1367,6 +1977,51 @@ function createFormData( return formData; } +function applyConstructor( + response: Response, + model: Function, + parentObject: Object, + key: string, +): void { + Object.setPrototypeOf(parentObject, model.prototype); + // Delete the property. It was just a placeholder. + return undefined; +} + +function defineLazyGetter( + response: Response, + chunk: SomeChunk, + parentObject: Object, + key: string, +): any { + // We don't immediately initialize it even if it's resolved. + // Instead, we wait for the getter to get accessed. + Object.defineProperty(parentObject, key, { + get: function () { + if (chunk.status === RESOLVED_MODEL) { + // If it was now resolved, then we initialize it. This may then discover + // a new set of lazy references that are then asked for eagerly in case + // we get that deep. + initializeModelChunk(chunk); + } + switch (chunk.status) { + case INITIALIZED: { + return chunk.value; + } + case ERRORED: + throw chunk.reason; + } + // Otherwise, we didn't have enough time to load the object before it was + // accessed or the connection closed. So we just log that it was omitted. + // TODO: We should ideally throw here to indicate a difference. + return OMITTED_PROP_ERROR; + }, + enumerable: true, + configurable: false, + }); + return null; +} + function extractIterator(response: Response, model: Array): Iterator { // $FlowFixMe[incompatible-use]: This uses raw Symbols because we're extracting from a native array. return model[Symbol.iterator](); @@ -1376,6 +2031,44 @@ function createModel(response: Response, model: any): any { return model; } +const mightHaveStaticConstructor = /\bclass\b.*\bstatic\b/; + +function getInferredFunctionApproximate(code: string): () => void { + let slicedCode; + if (code.startsWith('Object.defineProperty(')) { + slicedCode = code.slice('Object.defineProperty('.length); + } else if (code.startsWith('(')) { + slicedCode = code.slice(1); + } else { + slicedCode = code; + } + if (slicedCode.startsWith('async function')) { + const idx = slicedCode.indexOf('(', 14); + if (idx !== -1) { + const name = slicedCode.slice(14, idx).trim(); + // eslint-disable-next-line no-eval + return (0, eval)('({' + JSON.stringify(name) + ':async function(){}})')[ + name + ]; + } + } else if (slicedCode.startsWith('function')) { + const idx = slicedCode.indexOf('(', 8); + if (idx !== -1) { + const name = slicedCode.slice(8, idx).trim(); + // eslint-disable-next-line no-eval + return (0, eval)('({' + JSON.stringify(name) + ':function(){}})')[name]; + } + } else if (slicedCode.startsWith('class')) { + const idx = slicedCode.indexOf('{', 5); + if (idx !== -1) { + const name = slicedCode.slice(5, idx).trim(); + // eslint-disable-next-line no-eval + return (0, eval)('({' + JSON.stringify(name) + ':class{}})')[name]; + } + } + return function () {}; +} + function parseModelString( response: Response, parentObject: Object, @@ -1394,6 +2087,7 @@ function parseModelString( parent: initializingHandler, chunk: null, value: null, + reason: null, deps: 0, errored: false, }; @@ -1419,14 +2113,10 @@ function parseModelString( } // We create a React.lazy wrapper around any lazy values. // When passed into React, we'll know how to suspend on this. - return createLazyChunkWrapper(chunk); + return createLazyChunkWrapper(chunk, 0); } case '@': { // Promise - if (value.length === 2) { - // Infinite promise that never resolves. - return new Promise(() => {}); - } const id = parseInt(value.slice(2), 16); const chunk = getChunk(response, id); if (enableProfilerTimer && enableComponentPerformanceTrack) { @@ -1547,34 +2237,102 @@ function parseModelString( // BigInt return BigInt(value.slice(2)); } + case 'P': { + if (__DEV__) { + // In DEV mode we allow debug objects to specify themselves as instances of + // another constructor. + const ref = value.slice(2); + return getOutlinedModel( + response, + ref, + parentObject, + key, + applyConstructor, + ); + } + //Fallthrough + } case 'E': { if (__DEV__) { // In DEV mode we allow indirect eval to produce functions for logging. // This should not compile to eval() because then it has local scope access. + const code = value.slice(2); try { - // eslint-disable-next-line no-eval - return (0, eval)(value.slice(2)); + // If this might be a class constructor with a static initializer or + // static constructor then don't eval it. It might cause unexpected + // side-effects. Instead, fallback to parsing out the function type + // and name. + if (!mightHaveStaticConstructor.test(code)) { + // eslint-disable-next-line no-eval + return (0, eval)(code); + } } catch (x) { - // We currently use this to express functions so we fail parsing it, - // let's just return a blank function as a place holder. - return function () {}; + // Fallthrough to fallback case. } + // We currently use this to express functions so we fail parsing it, + // let's just return a blank function as a place holder. + let fn; + try { + fn = getInferredFunctionApproximate(code); + if (code.startsWith('Object.defineProperty(')) { + const DESCRIPTOR = ',"name",{value:"'; + const idx = code.lastIndexOf(DESCRIPTOR); + if (idx !== -1) { + const name = JSON.parse( + code.slice(idx + DESCRIPTOR.length - 1, code.length - 2), + ); + // $FlowFixMe[cannot-write] + Object.defineProperty(fn, 'name', {value: name}); + } + } + } catch (_) { + fn = function () {}; + } + return fn; } // Fallthrough } case 'Y': { if (__DEV__) { + if (value.length > 2) { + const debugChannelCallback = + response._debugChannel && response._debugChannel.callback; + if (debugChannelCallback) { + if (value[2] === '@') { + // This is a deferred Promise. + const ref = value.slice(3); // We assume this doesn't have a path just id. + const id = parseInt(ref, 16); + if (!response._chunks.has(id)) { + // We haven't seen this id before. Query the server to start sending it. + debugChannelCallback('P:' + ref); + } + // Start waiting. This now creates a pending chunk if it doesn't already exist. + // This is the actual Promise we're waiting for. + return getChunk(response, id); + } + const ref = value.slice(2); // We assume this doesn't have a path just id. + const id = parseInt(ref, 16); + if (!response._chunks.has(id)) { + // We haven't seen this id before. Query the server to start sending it. + debugChannelCallback('Q:' + ref); + } + // Start waiting. This now creates a pending chunk if it doesn't already exist. + const chunk = getChunk(response, id); + if (chunk.status === INITIALIZED) { + // We already loaded this before. We can just use the real value. + return chunk.value; + } + return defineLazyGetter(response, chunk, parentObject, key); + } + } + // 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 () { // TODO: We should ideally throw here to indicate a difference. - return ( - '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.' - ); + return OMITTED_PROP_ERROR; }, enumerable: true, configurable: false, @@ -1631,9 +2389,10 @@ function ResponseInstance( encodeFormAction: void | EncodeFormActionCallback, nonce: void | string, temporaryReferences: void | TemporaryReferenceSet, - findSourceMapURL: void | FindSourceMapURLCallback, - replayConsole: boolean, - environmentName: void | string, + findSourceMapURL: void | FindSourceMapURLCallback, // DEV-only + replayConsole: boolean, // DEV-only + environmentName: void | string, // DEV-only + debugChannel: void | DebugChannel, // DEV-only ) { const chunks: Map> = new Map(); this._bundlerConfig = bundlerConfig; @@ -1645,18 +2404,19 @@ function ResponseInstance( this._chunks = chunks; this._stringDecoder = createStringDecoder(); this._fromJSON = (null: any); - this._rowState = 0; - this._rowID = 0; - this._rowTag = 0; - this._rowLength = 0; - this._buffer = []; this._closed = false; this._closedReason = null; this._tempRefs = temporaryReferences; if (enableProfilerTimer && enableComponentPerformanceTrack) { this._timeOrigin = 0; + this._pendingInitialRender = null; } if (__DEV__) { + this._pendingChunks = 0; + this._weakResponse = { + weak: new WeakRef(this), + response: this, + }; // TODO: The Flight Client can be used in a Client Environment too and we should really support // getting the owner there as well, but currently the owner of ReactComponentInfo is typed as only // supporting other ReactComponentInfo as owners (and not Fiber or Fizz's ComponentStackNode). @@ -1688,9 +2448,32 @@ function ResponseInstance( ); } this._debugFindSourceMapURL = findSourceMapURL; + this._debugChannel = debugChannel; + this._blockedConsole = null; this._replayConsole = replayConsole; this._rootEnvironmentName = rootEnv; + if (debugChannel) { + if (debugChannelRegistry === null) { + // We can't safely clean things up later, so we immediately close the + // debug channel. + closeDebugChannel(debugChannel); + this._debugChannel = undefined; + } else { + // When a Response gets GC:ed because nobody is referring to any of the + // objects that lazily load from the Response anymore, then we can close + // the debug channel. + debugChannelRegistry.register(this, debugChannel, this); + } + } + } + if (enableProfilerTimer && enableComponentPerformanceTrack) { + // Since we don't know when recording of profiles will start and stop, we have to + // mark the order over and over again. + if (replayConsole) { + markAllTracksInOrder(); + } } + // Don't inline this call because it causes closure to outline the call above. this._fromJSON = createFromJSONCallback(this); } @@ -1703,25 +2486,64 @@ export function createResponse( encodeFormAction: void | EncodeFormActionCallback, nonce: void | string, temporaryReferences: void | TemporaryReferenceSet, - findSourceMapURL: void | FindSourceMapURLCallback, - replayConsole: boolean, - environmentName: void | string, -): Response { - // $FlowFixMe[invalid-constructor]: the shapes are exact here but Flow doesn't like constructors - return new ResponseInstance( - bundlerConfig, - serverReferenceConfig, - moduleLoading, - callServer, - encodeFormAction, - nonce, - temporaryReferences, - findSourceMapURL, - replayConsole, - environmentName, + findSourceMapURL: void | FindSourceMapURLCallback, // DEV-only + replayConsole: boolean, // DEV-only + environmentName: void | string, // DEV-only + debugChannel: void | DebugChannel, // DEV-only +): WeakResponse { + return getWeakResponse( + // $FlowFixMe[invalid-constructor]: the shapes are exact here but Flow doesn't like constructors + new ResponseInstance( + bundlerConfig, + serverReferenceConfig, + moduleLoading, + callServer, + encodeFormAction, + nonce, + temporaryReferences, + findSourceMapURL, + replayConsole, + environmentName, + debugChannel, + ), ); } +export type StreamState = { + _rowState: RowParserState, + _rowID: number, // parts of a row ID parsed so far + _rowTag: number, // 0 indicates that we're currently parsing the row ID + _rowLength: number, // remaining bytes in the row. 0 indicates that we're looking for a newline. + _buffer: Array, // chunks received so far as part of this row +}; + +export function createStreamState(): StreamState { + return { + _rowState: 0, + _rowID: 0, + _rowTag: 0, + _rowLength: 0, + _buffer: [], + }; +} + +function resolveDebugHalt(response: Response, id: number): void { + const chunks = response._chunks; + let chunk = chunks.get(id); + if (!chunk) { + chunks.set(id, (chunk = createPendingChunk(response))); + } else { + } + if (chunk.status !== PENDING && chunk.status !== BLOCKED) { + return; + } + releasePendingChunk(response, chunk); + const haltedChunk: HaltedChunk = (chunk: any); + haltedChunk.status = HALTED; + haltedChunk.value = null; + haltedChunk.reason = null; +} + function resolveModel( response: Response, id: number, @@ -1732,7 +2554,7 @@ function resolveModel( if (!chunk) { chunks.set(id, createResolvedModelChunk(response, model)); } else { - resolveModelChunk(chunk, model); + resolveModelChunk(response, chunk, model); } } @@ -1747,6 +2569,9 @@ function resolveText(response: Response, id: number, text: string): void { controller.enqueueValue(text); return; } + if (chunk) { + releasePendingChunk(response, chunk); + } chunks.set(id, createInitializedTextChunk(response, text)); } @@ -1765,6 +2590,9 @@ function resolveBuffer( controller.enqueueValue(buffer); return; } + if (chunk) { + releasePendingChunk(response, chunk); + } chunks.set(id, createInitializedBufferChunk(response, buffer)); } @@ -1802,14 +2630,15 @@ function resolveModule( blockedChunk = createBlockedChunk(response); chunks.set(id, blockedChunk); } else { + releasePendingChunk(response, chunk); // This can't actually happen because we don't have any forward // references to modules. blockedChunk = (chunk: any); blockedChunk.status = BLOCKED; } promise.then( - () => resolveModuleChunk(blockedChunk, clientReference), - error => triggerErrorOnChunk(blockedChunk, error), + () => resolveModuleChunk(response, blockedChunk, clientReference), + error => triggerErrorOnChunk(response, blockedChunk, error), ); } else { if (!chunk) { @@ -1817,7 +2646,7 @@ function resolveModule( } else { // This can't actually happen because we don't have any forward // references to modules. - resolveModuleChunk(chunk, clientReference); + resolveModuleChunk(response, chunk, clientReference); } } } @@ -1838,13 +2667,50 @@ function resolveStream>( // We already resolved. We didn't expect to see this. return; } + releasePendingChunk(response, chunk); + const resolveListeners = chunk.value; + + if (__DEV__) { + // Lazily initialize any debug info and block the initializing chunk on any unresolved entries. + if (chunk._debugChunk != null) { + const prevHandler = initializingHandler; + const prevChunk = initializingChunk; + initializingHandler = null; + const cyclicChunk: BlockedChunk = (chunk: any); + cyclicChunk.status = BLOCKED; + cyclicChunk.value = null; + cyclicChunk.reason = null; + if ((enableProfilerTimer && enableComponentPerformanceTrack) || __DEV__) { + initializingChunk = cyclicChunk; + } + try { + initializeDebugChunk(response, chunk); + chunk._debugChunk = null; + if (initializingHandler !== null) { + if (initializingHandler.errored) { + // Ignore error parsing debug info, we'll report the original error instead. + } else if (initializingHandler.deps > 0) { + // Leave blocked until we can resolve all the debug info. + initializingHandler.value = stream; + initializingHandler.reason = controller; + initializingHandler.chunk = cyclicChunk; + return; + } + } + } finally { + initializingHandler = prevHandler; + initializingChunk = prevChunk; + } + } + } + const resolvedChunk: InitializedStreamChunk = (chunk: any); resolvedChunk.status = INITIALIZED; resolvedChunk.value = stream; resolvedChunk.reason = controller; if (resolveListeners !== null) { - wakeChunk(resolveListeners, chunk.value); + wakeChunk(resolveListeners, chunk.value, chunk); } } @@ -1906,7 +2772,7 @@ function startReadableStream( // to synchronous emitting. previousBlockedChunk = null; } - resolveModelChunk(chunk, json); + resolveModelChunk(response, chunk, json); }); } }, @@ -1994,7 +2860,12 @@ function startAsyncIterable( false, ); } else { - resolveIteratorResultChunk(buffer[nextWriteIndex], value, false); + resolveIteratorResultChunk( + response, + buffer[nextWriteIndex], + value, + false, + ); } nextWriteIndex++; }, @@ -2007,12 +2878,18 @@ function startAsyncIterable( true, ); } else { - resolveIteratorResultChunk(buffer[nextWriteIndex], value, true); + resolveIteratorResultChunk( + response, + buffer[nextWriteIndex], + value, + true, + ); } nextWriteIndex++; while (nextWriteIndex < buffer.length) { // In generators, any extra reads from the iterator have the value undefined. resolveIteratorResultChunk( + response, buffer[nextWriteIndex++], '"$undefined"', true, @@ -2026,36 +2903,37 @@ function startAsyncIterable( createPendingChunk>(response); } while (nextWriteIndex < buffer.length) { - triggerErrorOnChunk(buffer[nextWriteIndex++], error); + triggerErrorOnChunk(response, buffer[nextWriteIndex++], error); } }, }; - const iterable: $AsyncIterable = { - [ASYNC_ITERATOR](): $AsyncIterator { - let nextReadIndex = 0; - return createIterator(arg => { - if (arg !== undefined) { - throw new Error( - 'Values cannot be passed to next() of AsyncIterables passed to Client Components.', + + const iterable: $AsyncIterable = ({}: any); + // $FlowFixMe[cannot-write] + iterable[ASYNC_ITERATOR] = (): $AsyncIterator => { + let nextReadIndex = 0; + return createIterator(arg => { + if (arg !== undefined) { + throw new Error( + 'Values cannot be passed to next() of AsyncIterables passed to Client Components.', + ); + } + if (nextReadIndex === buffer.length) { + if (closed) { + // $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors + return new ReactPromise( + INITIALIZED, + {done: true, value: undefined}, + null, ); } - if (nextReadIndex === buffer.length) { - if (closed) { - // $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors - return new ReactPromise( - INITIALIZED, - {done: true, value: undefined}, - null, - response, - ); - } - buffer[nextReadIndex] = - createPendingChunk>(response); - } - return buffer[nextReadIndex++]; - }); - }, + buffer[nextReadIndex] = + createPendingChunk>(response); + } + return buffer[nextReadIndex++]; + }); }; + // TODO: If it's a single shot iterator we can optimize memory by cleaning up the buffer after // reading through the end, but currently we favor code size over this optimization. resolveStream( @@ -2122,6 +3000,7 @@ function resolveErrorDev( response, stack, env, + false, // $FlowFixMe[incompatible-use] Error.bind( null, @@ -2161,7 +3040,7 @@ function resolvePostponeProd(response: Response, id: number): void { if (!chunk) { chunks.set(id, createErrorChunk(response, postponeInstance)); } else { - triggerErrorOnChunk(chunk, postponeInstance); + triggerErrorOnChunk(response, chunk, postponeInstance); } } @@ -2184,6 +3063,7 @@ function resolvePostponeDev( response, stack, env, + false, // $FlowFixMe[incompatible-use] Error.bind(null, reason || ''), ); @@ -2199,7 +3079,30 @@ function resolvePostponeDev( if (!chunk) { chunks.set(id, createErrorChunk(response, postponeInstance)); } else { - triggerErrorOnChunk(chunk, postponeInstance); + triggerErrorOnChunk(response, chunk, postponeInstance); + } +} + +function resolveErrorModel( + response: Response, + id: number, + row: UninitializedModel, +): void { + const chunks = response._chunks; + const chunk = chunks.get(id); + const errorInfo = JSON.parse(row); + let error; + if (__DEV__) { + error = resolveErrorDev(response, errorInfo); + } else { + error = resolveErrorProd(response); + } + (error: any).digest = errorInfo.digest; + const errorWithDigest: ErrorWithDigest = (error: any); + if (!chunk) { + chunks.set(id, createErrorChunk(response, errorWithDigest)); + } else { + triggerErrorOnChunk(response, chunk, errorWithDigest); } } @@ -2352,7 +3255,7 @@ function createFakeFunction( } if (sourceMap) { - // We use the prefix rsc://React/ to separate these from other files listed in + // We use the prefix about://React/ to separate these from other files listed in // the Chrome DevTools. We need a "host name" and not just a protocol because // otherwise the group name becomes the root folder. Ideally we don't want to // show these at all but there's two reasons to assign a fake URL. @@ -2360,7 +3263,7 @@ function createFakeFunction( // 2) If source maps are disabled or fails, you should at least be able to tell // which file it was. code += - '\n//# sourceURL=rsc://React/' + + '\n//# sourceURL=about://React/' + encodeURIComponent(environmentName) + '/' + encodeURI(filename) + @@ -2392,12 +3295,17 @@ function buildFakeCallStack( response: Response, stack: ReactStackTrace, environmentName: string, + useEnclosingLine: boolean, innerCall: () => T, ): () => T { let callStack = innerCall; for (let i = 0; i < stack.length; i++) { const frame = stack[i]; - const frameKey = frame.join('-') + '-' + environmentName; + const frameKey = + frame.join('-') + + '-' + + environmentName + + (useEnclosingLine ? '-e' : '-n'); let fn = fakeFunctionCache.get(frameKey); if (fn === undefined) { const [name, filename, line, col, enclosingLine, enclosingCol] = frame; @@ -2411,8 +3319,8 @@ function buildFakeCallStack( sourceMap, line, col, - enclosingLine, - enclosingCol, + useEnclosingLine ? line : enclosingLine, + useEnclosingLine ? col : enclosingCol, environmentName, ); // TODO: This cache should technically live on the response since the _debugFindSourceMapURL @@ -2447,55 +3355,61 @@ function getRootTask( function initializeFakeTask( response: Response, - debugInfo: ReactComponentInfo | ReactAsyncInfo, - childEnvironmentName: string, + debugInfo: ReactComponentInfo | ReactAsyncInfo | ReactIOInfo, ): null | ConsoleTask { if (!supportsCreateTask) { return null; } - const componentInfo: ReactComponentInfo = (debugInfo: any); // Refined if (debugInfo.stack == null) { // If this is an error, we should've really already initialized the task. // If it's null, we can't initialize a task. return null; } + const cachedEntry = debugInfo.debugTask; + if (cachedEntry !== undefined) { + return cachedEntry; + } + + // Workaround for a bug where Chrome Performance tracking uses the enclosing line/column + // instead of the callsite. For ReactAsyncInfo/ReactIOInfo, the only thing we're going + // to use the fake task for is the Performance tracking so we encode the enclosing line/ + // column at the callsite to get a better line number. We could do this for Components too + // but we're going to use those for other things too like console logs and it's not worth + // duplicating. If this bug is every fixed in Chrome, this should be set to false. + const useEnclosingLine = debugInfo.key === undefined; + const stack = debugInfo.stack; const env: string = - componentInfo.env == null + debugInfo.env == null ? response._rootEnvironmentName : debugInfo.env; + const ownerEnv: string = + debugInfo.owner == null || debugInfo.owner.env == null ? response._rootEnvironmentName - : componentInfo.env; - if (env !== childEnvironmentName) { + : debugInfo.owner.env; + const ownerTask = + debugInfo.owner == null + ? null + : initializeFakeTask(response, debugInfo.owner); + const taskName = // This is the boundary between two environments so we'll annotate the task name. - // That is unusual so we don't cache it. - const ownerTask = - componentInfo.owner == null - ? null - : initializeFakeTask(response, componentInfo.owner, env); - return buildFakeTask( - response, - ownerTask, - stack, - '"use ' + childEnvironmentName.toLowerCase() + '"', - env, - ); - } else { - const cachedEntry = componentInfo.debugTask; - if (cachedEntry !== undefined) { - return cachedEntry; - } - const ownerTask = - componentInfo.owner == null - ? null - : initializeFakeTask(response, componentInfo.owner, env); - // $FlowFixMe[cannot-write]: We consider this part of initialization. - return (componentInfo.debugTask = buildFakeTask( - response, - ownerTask, - stack, - getServerComponentTaskName(componentInfo), - env, - )); - } + // We assume that the stack frame of the entry into the new environment was done + // from the old environment. So we use the owner's environment as the current. + env !== ownerEnv + ? '"use ' + env.toLowerCase() + '"' + : // Some unfortunate pattern matching to refine the type. + debugInfo.key !== undefined + ? getServerComponentTaskName(((debugInfo: any): ReactComponentInfo)) + : debugInfo.name !== undefined + ? getIOInfoTaskName(((debugInfo: any): ReactIOInfo)) + : getAsyncInfoTaskName(((debugInfo: any): ReactAsyncInfo)); + // $FlowFixMe[cannot-write]: We consider this part of initialization. + return (debugInfo.debugTask = buildFakeTask( + response, + ownerTask, + stack, + taskName, + ownerEnv, + useEnclosingLine, + )); } function buildFakeTask( @@ -2504,9 +3418,16 @@ function buildFakeTask( stack: ReactStackTrace, taskName: string, env: string, + useEnclosingLine: boolean, ): ConsoleTask { const createTaskFn = (console: any).createTask.bind(console, taskName); - const callStack = buildFakeCallStack(response, stack, env, createTaskFn); + const callStack = buildFakeCallStack( + response, + stack, + env, + useEnclosingLine, + createTaskFn, + ); if (ownerTask === null) { const rootTask = getRootTask(response, env); if (rootTask != null) { @@ -2520,7 +3441,7 @@ function buildFakeTask( } const createFakeJSXCallStack = { - 'react-stack-bottom-frame': function ( + react_stack_bottom_frame: function ( response: Response, stack: ReactStackTrace, environmentName: string, @@ -2529,6 +3450,7 @@ const createFakeJSXCallStack = { response, stack, environmentName, + false, fakeJSXCallSite, ); return callStackForError(); @@ -2541,7 +3463,7 @@ const createFakeJSXCallStackInDEV: ( environmentName: string, ) => Error = __DEV__ ? // We use this technique to trick minifiers to preserve the function name. - (createFakeJSXCallStack['react-stack-bottom-frame'].bind( + (createFakeJSXCallStack.react_stack_bottom_frame.bind( createFakeJSXCallStack, ): any) : (null: any); @@ -2555,7 +3477,7 @@ function fakeJSXCallSite() { function initializeFakeStack( response: Response, - debugInfo: ReactComponentInfo | ReactAsyncInfo, + debugInfo: ReactComponentInfo | ReactAsyncInfo | ReactIOInfo, ): void { const cachedEntry = debugInfo.debugStack; if (cachedEntry !== undefined) { @@ -2567,48 +3489,53 @@ function initializeFakeStack( // $FlowFixMe[cannot-write] debugInfo.debugStack = createFakeJSXCallStackInDEV(response, stack, env); } - if (debugInfo.owner != null) { + const owner = debugInfo.owner; + if (owner != null) { // Initialize any owners not yet initialized. - initializeFakeStack(response, debugInfo.owner); + initializeFakeStack(response, owner); + if (owner.debugLocation === undefined && debugInfo.debugStack != null) { + // If we are the child of this owner, then the owner should be the bottom frame + // our stack. We can use it as the implied location of the owner. + owner.debugLocation = debugInfo.debugStack; + } } } -function resolveDebugInfo( +function initializeDebugInfo( response: Response, - id: number, - debugInfo: - | ReactComponentInfo - | ReactEnvironmentInfo - | ReactAsyncInfo - | ReactTimeInfo, -): void { + debugInfo: ReactDebugInfoEntry, +): ReactDebugInfoEntry { if (!__DEV__) { // These errors should never make it into a build so we don't need to encode them in codes.json // eslint-disable-next-line react-internal/prod-error-codes throw new Error( - 'resolveDebugInfo should never be called in production mode. This is a bug in React.', + 'initializeDebugInfo should never be called in production mode. This is a bug in React.', ); } - // We eagerly initialize the fake task because this resolving happens outside any - // render phase so we're not inside a user space stack at this point. If we waited - // to initialize it when we need it, we might be inside user code. - const env = - debugInfo.env === undefined ? response._rootEnvironmentName : debugInfo.env; if (debugInfo.stack !== undefined) { const componentInfoOrAsyncInfo: ReactComponentInfo | ReactAsyncInfo = // $FlowFixMe[incompatible-type] debugInfo; - initializeFakeTask(response, componentInfoOrAsyncInfo, env); + // We eagerly initialize the fake task because this resolving happens outside any + // render phase so we're not inside a user space stack at this point. If we waited + // to initialize it when we need it, we might be inside user code. + initializeFakeTask(response, componentInfoOrAsyncInfo); } - if (debugInfo.owner === null && response._debugRootOwner != null) { - // $FlowFixMe[prop-missing] By narrowing `owner` to `null`, we narrowed `debugInfo` to `ReactComponentInfo` - const componentInfo: ReactComponentInfo = debugInfo; + if (debugInfo.owner == null && response._debugRootOwner != null) { + const componentInfoOrAsyncInfo: ReactComponentInfo | ReactAsyncInfo = + // $FlowFixMe: By narrowing `owner` to `null`, we narrowed `debugInfo` to `ReactComponentInfo` + debugInfo; // $FlowFixMe[cannot-write] - componentInfo.owner = response._debugRootOwner; + componentInfoOrAsyncInfo.owner = response._debugRootOwner; + // We clear the parsed stack frames to indicate that it needs to be re-parsed from debugStack. + // $FlowFixMe[cannot-write] + componentInfoOrAsyncInfo.stack = null; // We override the stack if we override the owner since the stack where the root JSX // was created on the server isn't very useful but where the request was made is. // $FlowFixMe[cannot-write] - componentInfo.debugStack = response._debugRootStack; + componentInfoOrAsyncInfo.debugStack = response._debugRootStack; + // $FlowFixMe[cannot-write] + componentInfoOrAsyncInfo.debugTask = response._debugRootTask; } else if (debugInfo.stack !== undefined) { const componentInfoOrAsyncInfo: ReactComponentInfo | ReactAsyncInfo = // $FlowFixMe[incompatible-type] @@ -2625,11 +3552,54 @@ function resolveDebugInfo( }; } } + return debugInfo; +} - const chunk = getChunk(response, id); - const chunkDebugInfo: ReactDebugInfo = - chunk._debugInfo || (chunk._debugInfo = []); - chunkDebugInfo.push(debugInfo); +function resolveDebugModel( + response: Response, + id: number, + json: UninitializedModel, +): void { + const parentChunk = getChunk(response, id); + if ( + parentChunk.status === INITIALIZED || + parentChunk.status === ERRORED || + parentChunk.status === HALTED || + parentChunk.status === BLOCKED + ) { + // We shouldn't really get debug info late. It's too late to add it after we resolved. + return; + } + if (parentChunk.status === RESOLVED_MODULE) { + // We don't expect to get debug info on modules. + return; + } + const previousChunk = parentChunk._debugChunk; + const debugChunk: ResolvedModelChunk = + createResolvedModelChunk(response, json); + debugChunk._debugChunk = previousChunk; // Linked list of the debug chunks + parentChunk._debugChunk = debugChunk; + initializeDebugChunk(response, parentChunk); + if ( + __DEV__ && + ((debugChunk: any): SomeChunk).status === BLOCKED && + (response._debugChannel === undefined || + !response._debugChannel.hasReadable) + ) { + if (json[0] === '"' && json[1] === '$') { + const path = json.slice(2, json.length - 1).split(':'); + const outlinedId = parseInt(path[0], 16); + const chunk = getChunk(response, outlinedId); + if (chunk.status === PENDING) { + // We expect the debug chunk to have been emitted earlier in the stream. It might be + // blocked on other things but chunk should no longer be pending. + // If it's still pending that suggests that it was referencing an object in the debug + // channel, but no debug channel was wired up so it's missing. In this case we can just + // drop the debug info instead of halting the whole stream. + parentChunk._debugChunk = null; + } + } + } } let currentOwnerInDEV: null | ReactComponentInfo = null; @@ -2645,14 +3615,16 @@ function getCurrentStackInDEV(): string { } const replayConsoleWithCallStack = { - 'react-stack-bottom-frame': function ( + react_stack_bottom_frame: function ( response: Response, - methodName: string, - stackTrace: ReactStackTrace, - owner: null | ReactComponentInfo, - env: string, - args: Array, + payload: ConsoleEntry, ): void { + const methodName = payload[0]; + const stackTrace = payload[1]; + const owner = payload[2]; + const env = payload[3]; + const args = payload.slice(4); + // There really shouldn't be anything else on the stack atm. const prevStack = ReactSharedInternals.getCurrentStack; ReactSharedInternals.getCurrentStack = getCurrentStackInDEV; @@ -2664,10 +3636,11 @@ const replayConsoleWithCallStack = { response, stackTrace, env, + false, bindToConsole(methodName, args, env), ); if (owner != null) { - const task = initializeFakeTask(response, owner, env); + const task = initializeFakeTask(response, owner); initializeFakeStack(response, owner); if (task !== null) { task.run(callStack); @@ -2689,21 +3662,25 @@ const replayConsoleWithCallStack = { const replayConsoleWithCallStackInDEV: ( response: Response, - methodName: string, - stackTrace: ReactStackTrace, - owner: null | ReactComponentInfo, - env: string, - args: Array, + payload: ConsoleEntry, ) => void = __DEV__ ? // We use this technique to trick minifiers to preserve the function name. - (replayConsoleWithCallStack['react-stack-bottom-frame'].bind( + (replayConsoleWithCallStack.react_stack_bottom_frame.bind( replayConsoleWithCallStack, ): any) : (null: any); +type ConsoleEntry = [ + string, + ReactStackTrace, + null | ReactComponentInfo, + string, + mixed, +]; + function resolveConsoleEntry( response: Response, - value: UninitializedModel, + json: UninitializedModel, ): void { if (!__DEV__) { // These errors should never make it into a build so we don't need to encode them in codes.json @@ -2717,27 +3694,116 @@ function resolveConsoleEntry( return; } - const payload: [ - string, - ReactStackTrace, - null | ReactComponentInfo, - string, - mixed, - ] = parseModel(response, value); - const methodName = payload[0]; - const stackTrace = payload[1]; - const owner = payload[2]; - const env = payload[3]; - const args = payload.slice(4); - - replayConsoleWithCallStackInDEV( - response, - methodName, - stackTrace, - owner, - env, - args, - ); + const blockedChunk = response._blockedConsole; + if (blockedChunk == null) { + // If we're not blocked on any other chunks, we can try to eagerly initialize + // this as a fast-path to avoid awaiting them. + const chunk: ResolvedModelChunk = createResolvedModelChunk( + response, + json, + ); + initializeModelChunk(chunk); + const initializedChunk: SomeChunk = chunk; + if (initializedChunk.status === INITIALIZED) { + replayConsoleWithCallStackInDEV(response, initializedChunk.value); + } else { + chunk.then( + v => replayConsoleWithCallStackInDEV(response, v), + e => { + // Ignore console errors for now. Unnecessary noise. + }, + ); + response._blockedConsole = chunk; + } + } else { + // We're still waiting on a previous chunk so we can't enqueue quite yet. + const chunk: SomeChunk = createPendingChunk(response); + chunk.then( + v => replayConsoleWithCallStackInDEV(response, v), + e => { + // Ignore console errors for now. Unnecessary noise. + }, + ); + response._blockedConsole = chunk; + const unblock = () => { + if (response._blockedConsole === chunk) { + // We were still the last chunk so we can now clear the queue and return + // to synchronous emitting. + response._blockedConsole = null; + } + resolveModelChunk(response, chunk, json); + }; + blockedChunk.then(unblock, unblock); + } +} + +function initializeIOInfo(response: Response, ioInfo: ReactIOInfo): void { + if (ioInfo.stack !== undefined) { + initializeFakeTask(response, ioInfo); + initializeFakeStack(response, ioInfo); + } + // Adjust the time to the current environment's time space. + // $FlowFixMe[cannot-write] + ioInfo.start += response._timeOrigin; + // $FlowFixMe[cannot-write] + ioInfo.end += response._timeOrigin; + + if (enableComponentPerformanceTrack && response._replayConsole) { + const env = response._rootEnvironmentName; + const promise = ioInfo.value; + if (promise) { + const thenable: Thenable = (promise: any); + switch (thenable.status) { + case INITIALIZED: + logIOInfo(ioInfo, env, thenable.value); + break; + case ERRORED: + logIOInfoErrored(ioInfo, env, thenable.reason); + break; + default: + // If we haven't resolved the Promise yet, wait to log until have so we can include + // its data in the log. + promise.then( + logIOInfo.bind(null, ioInfo, env), + logIOInfoErrored.bind(null, ioInfo, env), + ); + break; + } + } else { + logIOInfo(ioInfo, env, undefined); + } + } +} + +function resolveIOInfo( + response: Response, + id: number, + model: UninitializedModel, +): void { + const chunks = response._chunks; + let chunk = chunks.get(id); + if (!chunk) { + chunk = createResolvedModelChunk(response, model); + chunks.set(id, chunk); + initializeModelChunk(chunk); + } else { + resolveModelChunk(response, chunk, model); + if (chunk.status === RESOLVED_MODEL) { + initializeModelChunk(chunk); + } + } + if (chunk.status === INITIALIZED) { + initializeIOInfo(response, chunk.value); + } else { + chunk.then( + v => { + initializeIOInfo(response, v); + }, + e => { + // Ignore debug info errors for now. Unnecessary noise. + }, + ); + } } function mergeBuffer( @@ -2792,6 +3858,46 @@ function resolveTypedArray( resolveBuffer(response, id, view); } +function logComponentInfo( + response: Response, + root: SomeChunk, + componentInfo: ReactComponentInfo, + trackIdx: number, + startTime: number, + componentEndTime: number, + childrenEndTime: number, + isLastComponent: boolean, +): void { + // $FlowFixMe: Refined. + if ( + isLastComponent && + root.status === ERRORED && + root.reason !== response._closedReason + ) { + // If this is the last component to render before this chunk rejected, then conceptually + // this component errored. If this was a cancellation then it wasn't this component that + // errored. + logComponentErrored( + componentInfo, + trackIdx, + startTime, + componentEndTime, + childrenEndTime, + response._rootEnvironmentName, + root.reason, + ); + } else { + logComponentRender( + componentInfo, + trackIdx, + startTime, + componentEndTime, + childrenEndTime, + response._rootEnvironmentName, + ); + } +} + function flushComponentPerformance( response: Response, root: SomeChunk, @@ -2828,6 +3934,7 @@ function flushComponentPerformance( trackIdx, parentEndTime, previousEndTime, + response._rootEnvironmentName, ); } // Since we didn't bump the track this time, we just return the same track. @@ -2835,32 +3942,25 @@ function flushComponentPerformance( return previousResult; } const children = root._children; - if (root.status === RESOLVED_MODEL) { - // If the model is not initialized by now, do that now so we can find its - // children. This part is a little sketchy since it significantly changes - // the performance characteristics of the app by profiling. - initializeModelChunk(root); - } // First find the start time of the first component to know if it was running // in parallel with the previous. - const debugInfo = root._debugInfo; + const debugInfo = __DEV__ && root._debugInfo; if (debugInfo) { - for (let i = 1; i < debugInfo.length; i++) { + let startTime = 0; + for (let i = 0; i < debugInfo.length; i++) { const info = debugInfo[i]; + if (typeof info.time === 'number') { + startTime = info.time; + } if (typeof info.name === 'string') { - // $FlowFixMe: Refined. - const startTimeInfo = debugInfo[i - 1]; - if (typeof startTimeInfo.time === 'number') { - const startTime = startTimeInfo.time; - if (startTime < trackTime) { - // The start time of this component is before the end time of the previous - // component on this track so we need to bump the next one to a parallel track. - trackIdx++; - } - trackTime = startTime; - break; + if (startTime < trackTime) { + // The start time of this component is before the end time of the previous + // component on this track so we need to bump the next one to a parallel track. + trackIdx++; } + trackTime = startTime; + break; } } for (let i = debugInfo.length - 1; i >= 0; i--) { @@ -2868,6 +3968,7 @@ function flushComponentPerformance( if (typeof info.time === 'number') { if (info.time > parentEndTime) { parentEndTime = info.time; + break; // We assume the highest number is at the end. } } } @@ -2895,67 +3996,180 @@ function flushComponentPerformance( } childTrackIdx = childResult.track; const childEndTime = childResult.endTime; - childTrackTime = childEndTime; + if (childEndTime > childTrackTime) { + childTrackTime = childEndTime; + } if (childEndTime > childrenEndTime) { childrenEndTime = childEndTime; } } if (debugInfo) { - let endTime = 0; + // Write debug info in reverse order (just like stack traces). + let componentEndTime = 0; let isLastComponent = true; + let endTime = -1; + let endTimeIdx = -1; for (let i = debugInfo.length - 1; i >= 0; i--) { const info = debugInfo[i]; - if (typeof info.time === 'number') { - endTime = info.time; - if (endTime > childrenEndTime) { - childrenEndTime = endTime; - } + if (typeof info.time !== 'number') { + continue; } - if (typeof info.name === 'string' && i > 0) { - // $FlowFixMe: Refined. - const componentInfo: ReactComponentInfo = info; - const startTimeInfo = debugInfo[i - 1]; - if (typeof startTimeInfo.time === 'number') { - const startTime = startTimeInfo.time; - if ( - isLastComponent && - root.status === ERRORED && - root.reason !== response._closedReason - ) { - // If this is the last component to render before this chunk rejected, then conceptually - // this component errored. If this was a cancellation then it wasn't this component that - // errored. - logComponentErrored( + if (componentEndTime === 0) { + // Last timestamp is the end of the last component. + componentEndTime = info.time; + } + const time = info.time; + if (endTimeIdx > -1) { + // Now that we know the start and end time, we can emit the entries between. + for (let j = endTimeIdx - 1; j > i; j--) { + const candidateInfo = debugInfo[j]; + if (typeof candidateInfo.name === 'string') { + if (componentEndTime > childrenEndTime) { + childrenEndTime = componentEndTime; + } + // $FlowFixMe: Refined. + const componentInfo: ReactComponentInfo = candidateInfo; + logComponentInfo( + response, + root, componentInfo, trackIdx, - startTime, - endTime, + time, + componentEndTime, childrenEndTime, - response._rootEnvironmentName, - root.reason, + isLastComponent, ); - } else { - logComponentRender( + componentEndTime = time; // The end time of previous component is the start time of the next. + // Track the root most component of the result for deduping logging. + result.component = componentInfo; + isLastComponent = false; + } else if ( + candidateInfo.awaited && + // Skip awaits on client resources since they didn't block the server component. + candidateInfo.awaited.env != null + ) { + if (endTime > childrenEndTime) { + childrenEndTime = endTime; + } + // $FlowFixMe: Refined. + const asyncInfo: ReactAsyncInfo = candidateInfo; + const env = response._rootEnvironmentName; + const promise = asyncInfo.awaited.value; + if (promise) { + const thenable: Thenable = (promise: any); + switch (thenable.status) { + case INITIALIZED: + logComponentAwait( + asyncInfo, + trackIdx, + time, + endTime, + env, + thenable.value, + ); + break; + case ERRORED: + logComponentAwaitErrored( + asyncInfo, + trackIdx, + time, + endTime, + env, + thenable.reason, + ); + break; + default: + // We assume that we should have received the data by now since this is logged at the + // end of the response stream. This is more sensitive to ordering so we don't wait + // to log it. + logComponentAwait( + asyncInfo, + trackIdx, + time, + endTime, + env, + undefined, + ); + break; + } + } else { + logComponentAwait( + asyncInfo, + trackIdx, + time, + endTime, + env, + undefined, + ); + } + } + } + } else { + // Anything between the end and now was aborted if it has no end time. + // Either because the client stream was aborted reading it or the server stream aborted. + endTime = time; // If we don't find anything else the endTime is the start time. + for (let j = debugInfo.length - 1; j > i; j--) { + const candidateInfo = debugInfo[j]; + if (typeof candidateInfo.name === 'string') { + if (componentEndTime > childrenEndTime) { + childrenEndTime = componentEndTime; + } + // $FlowFixMe: Refined. + const componentInfo: ReactComponentInfo = candidateInfo; + const env = response._rootEnvironmentName; + logComponentAborted( componentInfo, trackIdx, - startTime, - endTime, + time, + componentEndTime, childrenEndTime, - response._rootEnvironmentName, + env, ); + componentEndTime = time; // The end time of previous component is the start time of the next. + // Track the root most component of the result for deduping logging. + result.component = componentInfo; + isLastComponent = false; + } else if ( + candidateInfo.awaited && + // Skip awaits on client resources since they didn't block the server component. + candidateInfo.awaited.env != null + ) { + // If we don't have an end time for an await, that means we aborted. + const asyncInfo: ReactAsyncInfo = candidateInfo; + const env = response._rootEnvironmentName; + if (asyncInfo.awaited.end > endTime) { + endTime = asyncInfo.awaited.end; // Take the end time of the I/O as the await end. + } + if (endTime > childrenEndTime) { + childrenEndTime = endTime; + } + logComponentAwaitAborted(asyncInfo, trackIdx, time, endTime, env); } - // Track the root most component of the result for deduping logging. - result.component = componentInfo; } - isLastComponent = false; } + endTime = time; // The end time of the next entry is this time. + endTimeIdx = i; } } result.endTime = childrenEndTime; return result; } +function flushInitialRenderPerformance(response: Response): void { + if ( + enableProfilerTimer && + enableComponentPerformanceTrack && + response._replayConsole + ) { + const rootChunk = getChunk(response, 0); + if (isArray(rootChunk._children)) { + markAllTracksInOrder(); + flushComponentPerformance(response, rootChunk, 0, -Infinity, -Infinity); + } + } +} + function processFullBinaryRow( response: Response, id: number, @@ -3036,22 +4250,7 @@ function processFullStringRow( return; } case 69 /* "E" */: { - const errorInfo = JSON.parse(row); - let error; - if (__DEV__) { - error = resolveErrorDev(response, errorInfo); - } else { - error = resolveErrorProd(response); - } - (error: any).digest = errorInfo.digest; - const errorWithDigest: ErrorWithDigest = (error: any); - const chunks = response._chunks; - const chunk = chunks.get(id); - if (!chunk) { - chunks.set(id, createErrorChunk(response, errorWithDigest)); - } else { - triggerErrorOnChunk(chunk, errorWithDigest); - } + resolveErrorModel(response, id, row); return; } case 84 /* "T" */: { @@ -3059,7 +4258,10 @@ function processFullStringRow( return; } case 78 /* "N" */: { - if (enableProfilerTimer && enableComponentPerformanceTrack) { + if ( + enableProfilerTimer && + (enableComponentPerformanceTrack || enableAsyncDebugInfo) + ) { // Track the time origin for future debug info. We track it relative // to the current environment's time space. const timeOrigin: number = +row; @@ -3073,30 +4275,14 @@ function processFullStringRow( } case 68 /* "D" */: { if (__DEV__) { - const chunk: ResolvedModelChunk< - | ReactComponentInfo - | ReactEnvironmentInfo - | ReactAsyncInfo - | ReactTimeInfo, - > = createResolvedModelChunk(response, row); - initializeModelChunk(chunk); - const initializedChunk: SomeChunk< - | ReactComponentInfo - | ReactEnvironmentInfo - | ReactAsyncInfo - | ReactTimeInfo, - > = chunk; - if (initializedChunk.status === INITIALIZED) { - resolveDebugInfo(response, id, initializedChunk.value); - } else { - // TODO: This is not going to resolve in the right order if there's more than one. - chunk.then( - v => resolveDebugInfo(response, id, v), - e => { - // Ignore debug info errors for now. Unnecessary noise. - }, - ); - } + resolveDebugModel(response, id, row); + return; + } + // Fallthrough to share the error with Console entries. + } + case 74 /* "J" */: { + if (enableProfilerTimer && enableAsyncDebugInfo) { + resolveIOInfo(response, id, row); return; } // Fallthrough to share the error with Console entries. @@ -3156,6 +4342,10 @@ function processFullStringRow( } // Fallthrough default: /* """ "{" "[" "t" "f" "n" "0" - "9" */ { + if (__DEV__ && row === '') { + resolveDebugHalt(response, id); + return; + } // We assume anything else is JSON. resolveModel(response, id, row); return; @@ -3164,15 +4354,21 @@ function processFullStringRow( } export function processBinaryChunk( - response: Response, + weakResponse: WeakResponse, + streamState: StreamState, chunk: Uint8Array, ): void { + if (hasGCedResponse(weakResponse)) { + // Ignore more chunks if we've already GC:ed all listeners. + return; + } + const response = unwrapWeakResponse(weakResponse); let i = 0; - let rowState = response._rowState; - let rowID = response._rowID; - let rowTag = response._rowTag; - let rowLength = response._rowLength; - const buffer = response._buffer; + let rowState = streamState._rowState; + let rowID = streamState._rowID; + let rowTag = streamState._rowTag; + let rowLength = streamState._rowLength; + const buffer = streamState._buffer; const chunkLength = chunk.length; while (i < chunkLength) { let lastIdx = -1; @@ -3277,13 +4473,22 @@ export function processBinaryChunk( break; } } - response._rowState = rowState; - response._rowID = rowID; - response._rowTag = rowTag; - response._rowLength = rowLength; + streamState._rowState = rowState; + streamState._rowID = rowID; + streamState._rowTag = rowTag; + streamState._rowLength = rowLength; } -export function processStringChunk(response: Response, chunk: string): void { +export function processStringChunk( + weakResponse: WeakResponse, + streamState: StreamState, + chunk: string, +): void { + if (hasGCedResponse(weakResponse)) { + // Ignore more chunks if we've already GC:ed all listeners. + return; + } + const response = unwrapWeakResponse(weakResponse); // 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 @@ -3293,11 +4498,11 @@ export function processStringChunk(response: Response, chunk: string): void { // 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; + let rowState = streamState._rowState; + let rowID = streamState._rowID; + let rowTag = streamState._rowTag; + let rowLength = streamState._rowLength; + const buffer = streamState._buffer; const chunkLength = chunk.length; while (i < chunkLength) { let lastIdx = -1; @@ -3421,10 +4626,10 @@ export function processStringChunk(response: Response, chunk: string): void { ); } } - response._rowState = rowState; - response._rowID = rowID; - response._rowTag = rowTag; - response._rowLength = rowLength; + streamState._rowState = rowState; + streamState._rowID = rowID; + streamState._rowTag = rowTag; + streamState._rowLength = rowLength; } function parseModel(response: Response, json: UninitializedModel): T { @@ -3445,12 +4650,12 @@ function createFromJSONCallback(response: Response) { }; } -export function close(response: Response): void { +export function close(weakResponse: WeakResponse): void { // In case there are any remaining unresolved chunks, they won't // be resolved now. So we need to issue an error to those. // Ideally we should be able to early bail out if we kept a // ref count of pending chunks. - reportGlobalError(response, new Error('Connection closed.')); + reportGlobalError(weakResponse, new Error('Connection closed.')); } function getCurrentOwnerInDEV(): null | ReactComponentInfo { diff --git a/packages/react-client/src/ReactFlightClientDevToolsHook.js b/packages/react-client/src/ReactFlightClientDevToolsHook.js index b8ca649d4de45..4f5a716eb32f4 100644 --- a/packages/react-client/src/ReactFlightClientDevToolsHook.js +++ b/packages/react-client/src/ReactFlightClientDevToolsHook.js @@ -30,7 +30,7 @@ export function injectInternals(internals: Object): boolean { } catch (err) { // Catch all errors because it is unsafe to throw during initialization. if (__DEV__) { - console.error('React instrumentation encountered an error: %s.', err); + console.error('React instrumentation encountered an error: %o.', err); } } if (hook.checkDCE) { diff --git a/packages/react-client/src/ReactFlightPerformanceTrack.js b/packages/react-client/src/ReactFlightPerformanceTrack.js index ed0f67a4f313e..81474767363fd 100644 --- a/packages/react-client/src/ReactFlightPerformanceTrack.js +++ b/packages/react-client/src/ReactFlightPerformanceTrack.js @@ -9,15 +9,30 @@ /* eslint-disable react-internal/no-production-logging */ -import type {ReactComponentInfo} from 'shared/ReactTypes'; +import type { + ReactComponentInfo, + ReactIOInfo, + ReactAsyncInfo, +} from 'shared/ReactTypes'; import {enableProfilerTimer} from 'shared/ReactFeatureFlags'; +import { + addValueToProperties, + addObjectToProperties, +} from 'shared/ReactPerformanceTrackProperties'; + +import {getIODescription} from 'shared/ReactIODescription'; + const supportsUserTiming = enableProfilerTimer && typeof console !== 'undefined' && - typeof console.timeStamp === 'function'; + typeof console.timeStamp === 'function' && + typeof performance !== 'undefined' && + // $FlowFixMe[method-unbinding] + typeof performance.measure === 'function'; +const IO_TRACK = 'Server Requests ⚛'; const COMPONENTS_TRACK = 'Server Components ⚛'; export function markAllTracksInOrder() { @@ -25,6 +40,14 @@ export function markAllTracksInOrder() { // Ensure we create the Server Component track groups earlier than the Client Scheduler // and Client Components. We can always add the 0 time slot even if it's in the past. // That's still considered for ordering. + console.timeStamp( + 'Server Requests Track', + 0.001, + 0.001, + IO_TRACK, + undefined, + 'primary-light', + ); console.timeStamp( 'Server Components Track', 0.001, @@ -80,21 +103,31 @@ export function logComponentRender( isPrimaryEnv || env === undefined ? name : name + ' [' + env + ']'; const debugTask = componentInfo.debugTask; if (__DEV__ && debugTask) { + const properties: Array<[string, string]> = []; + if (componentInfo.key != null) { + addValueToProperties('key', componentInfo.key, properties, 0, ''); + } + if (componentInfo.props != null) { + addObjectToProperties(componentInfo.props, properties, 0, ''); + } debugTask.run( // $FlowFixMe[method-unbinding] - console.timeStamp.bind( - console, - entryName, - startTime < 0 ? 0 : startTime, - childrenEndTime, - trackNames[trackIdx], - COMPONENTS_TRACK, - color, - ), + performance.measure.bind(performance, '\u200b' + entryName, { + start: startTime < 0 ? 0 : startTime, + end: childrenEndTime, + detail: { + devtools: { + color: color, + track: trackNames[trackIdx], + trackGroup: COMPONENTS_TRACK, + properties, + }, + }, + }), ); } else { console.timeStamp( - entryName, + '\u200b' + entryName, startTime < 0 ? 0 : startTime, childrenEndTime, trackNames[trackIdx], @@ -105,6 +138,59 @@ export function logComponentRender( } } +export function logComponentAborted( + componentInfo: ReactComponentInfo, + trackIdx: number, + startTime: number, + endTime: number, + childrenEndTime: number, + rootEnv: string, +): void { + if (supportsUserTiming) { + const env = componentInfo.env; + const name = componentInfo.name; + const isPrimaryEnv = env === rootEnv; + const entryName = + isPrimaryEnv || env === undefined ? name : name + ' [' + env + ']'; + if (__DEV__) { + const properties: Array<[string, string]> = [ + [ + 'Aborted', + 'The stream was aborted before this Component finished rendering.', + ], + ]; + if (componentInfo.key != null) { + addValueToProperties('key', componentInfo.key, properties, 0, ''); + } + if (componentInfo.props != null) { + addObjectToProperties(componentInfo.props, properties, 0, ''); + } + performance.measure('\u200b' + entryName, { + start: startTime < 0 ? 0 : startTime, + end: childrenEndTime, + detail: { + devtools: { + color: 'warning', + track: trackNames[trackIdx], + trackGroup: COMPONENTS_TRACK, + tooltipText: entryName + ' Aborted', + properties, + }, + }, + }); + } else { + console.timeStamp( + entryName, + startTime < 0 ? 0 : startTime, + childrenEndTime, + trackNames[trackIdx], + COMPONENTS_TRACK, + 'warning', + ); + } + } +} + export function logComponentErrored( componentInfo: ReactComponentInfo, trackIdx: number, @@ -120,12 +206,7 @@ export function logComponentErrored( const isPrimaryEnv = env === rootEnv; const entryName = isPrimaryEnv || env === undefined ? name : name + ' [' + env + ']'; - if ( - __DEV__ && - typeof performance !== 'undefined' && - // $FlowFixMe[method-unbinding] - typeof performance.measure === 'function' - ) { + if (__DEV__) { const message = typeof error === 'object' && error !== null && @@ -134,8 +215,14 @@ export function logComponentErrored( String(error.message) : // eslint-disable-next-line react-internal/safe-string-coercion String(error); - const properties = [['Error', message]]; - performance.measure(entryName, { + const properties: Array<[string, string]> = [['Error', message]]; + if (componentInfo.key != null) { + addValueToProperties('key', componentInfo.key, properties, 0, ''); + } + if (componentInfo.props != null) { + addObjectToProperties(componentInfo.props, properties, 0, ''); + } + performance.measure('\u200b' + entryName, { start: startTime < 0 ? 0 : startTime, end: childrenEndTime, detail: { @@ -166,9 +253,13 @@ export function logDedupedComponentRender( trackIdx: number, startTime: number, endTime: number, + rootEnv: string, ): void { if (supportsUserTiming && endTime >= 0 && trackIdx < 10) { + const env = componentInfo.env; const name = componentInfo.name; + const isPrimaryEnv = env === rootEnv; + const color = isPrimaryEnv ? 'primary-light' : 'secondary-light'; const entryName = name + ' [deduped]'; const debugTask = componentInfo.debugTask; if (__DEV__ && debugTask) { @@ -181,7 +272,7 @@ export function logDedupedComponentRender( endTime, trackNames[trackIdx], COMPONENTS_TRACK, - 'tertiary-light', + color, ), ); } else { @@ -191,7 +282,350 @@ export function logDedupedComponentRender( endTime, trackNames[trackIdx], COMPONENTS_TRACK, - 'tertiary-light', + color, + ); + } + } +} + +function getIOColor( + functionName: string, +): 'tertiary-light' | 'tertiary' | 'tertiary-dark' { + // Add some color variation to be able to distinguish various sources. + switch (functionName.charCodeAt(0) % 3) { + case 0: + return 'tertiary-light'; + case 1: + return 'tertiary'; + default: + return 'tertiary-dark'; + } +} + +function getIOLongName( + ioInfo: ReactIOInfo, + description: string, + env: void | string, + rootEnv: string, +): string { + const name = ioInfo.name; + const longName = description === '' ? name : name + ' (' + description + ')'; + const isPrimaryEnv = env === rootEnv; + return isPrimaryEnv || env === undefined + ? longName + : longName + ' [' + env + ']'; +} + +function getIOShortName( + ioInfo: ReactIOInfo, + description: string, + env: void | string, + rootEnv: string, +): string { + const name = ioInfo.name; + const isPrimaryEnv = env === rootEnv; + const envSuffix = isPrimaryEnv || env === undefined ? '' : ' [' + env + ']'; + let desc = ''; + const descMaxLength = 30 - name.length - envSuffix.length; + if (descMaxLength > 1) { + const l = description.length; + if (l > 0 && l <= descMaxLength) { + // We can fit the full description + desc = ' (' + description + ')'; + } else if ( + description.startsWith('http://') || + description.startsWith('https://') || + description.startsWith('/') + ) { + // Looks like a URL. Let's see if we can extract something shorter. + // We don't have to do a full parse so let's try something cheaper. + let queryIdx = description.indexOf('?'); + if (queryIdx === -1) { + queryIdx = description.length; + } + if (description.charCodeAt(queryIdx - 1) === 47 /* "/" */) { + // Ends with slash. Look before that. + queryIdx--; + } + const slashIdx = description.lastIndexOf('/', queryIdx - 1); + if (queryIdx - slashIdx < descMaxLength) { + // This may now be either the file name or the host. + // Include the slash to make it more obvious what we trimmed. + desc = ' (…' + description.slice(slashIdx, queryIdx) + ')'; + } else { + // cut out the middle to not exceed the max length + const start = description.slice(slashIdx, slashIdx + descMaxLength / 2); + const end = description.slice(queryIdx - descMaxLength / 2, queryIdx); + desc = ' (' + (slashIdx > 0 ? '…' : '') + start + '…' + end + ')'; + } + } + } + return name + desc + envSuffix; +} + +export function logComponentAwaitAborted( + asyncInfo: ReactAsyncInfo, + trackIdx: number, + startTime: number, + endTime: number, + rootEnv: string, +): void { + if (supportsUserTiming && endTime > 0) { + const entryName = + 'await ' + getIOShortName(asyncInfo.awaited, '', asyncInfo.env, rootEnv); + const debugTask = asyncInfo.debugTask || asyncInfo.awaited.debugTask; + if (__DEV__ && debugTask) { + const properties = [ + ['Aborted', 'The stream was aborted before this Promise resolved.'], + ]; + const tooltipText = + getIOLongName(asyncInfo.awaited, '', asyncInfo.env, rootEnv) + + ' Aborted'; + debugTask.run( + // $FlowFixMe[method-unbinding] + performance.measure.bind(performance, entryName, { + start: startTime < 0 ? 0 : startTime, + end: endTime, + detail: { + devtools: { + color: 'warning', + track: trackNames[trackIdx], + trackGroup: COMPONENTS_TRACK, + properties, + tooltipText, + }, + }, + }), + ); + } else { + console.timeStamp( + entryName, + startTime < 0 ? 0 : startTime, + endTime, + trackNames[trackIdx], + COMPONENTS_TRACK, + 'warning', + ); + } + } +} + +export function logComponentAwaitErrored( + asyncInfo: ReactAsyncInfo, + trackIdx: number, + startTime: number, + endTime: number, + rootEnv: string, + error: mixed, +): void { + if (supportsUserTiming && endTime > 0) { + const description = getIODescription(error); + const entryName = + 'await ' + + getIOShortName(asyncInfo.awaited, description, asyncInfo.env, rootEnv); + const debugTask = asyncInfo.debugTask || asyncInfo.awaited.debugTask; + if (__DEV__ && debugTask) { + const message = + typeof error === 'object' && + error !== null && + typeof error.message === 'string' + ? // eslint-disable-next-line react-internal/safe-string-coercion + String(error.message) + : // eslint-disable-next-line react-internal/safe-string-coercion + String(error); + const properties = [['Rejected', message]]; + const tooltipText = + getIOLongName(asyncInfo.awaited, description, asyncInfo.env, rootEnv) + + ' Rejected'; + debugTask.run( + // $FlowFixMe[method-unbinding] + performance.measure.bind(performance, entryName, { + start: startTime < 0 ? 0 : startTime, + end: endTime, + detail: { + devtools: { + color: 'error', + track: trackNames[trackIdx], + trackGroup: COMPONENTS_TRACK, + properties, + tooltipText, + }, + }, + }), + ); + } else { + console.timeStamp( + entryName, + startTime < 0 ? 0 : startTime, + endTime, + trackNames[trackIdx], + COMPONENTS_TRACK, + 'error', + ); + } + } +} + +export function logComponentAwait( + asyncInfo: ReactAsyncInfo, + trackIdx: number, + startTime: number, + endTime: number, + rootEnv: string, + value: mixed, +): void { + if (supportsUserTiming && endTime > 0) { + const description = getIODescription(value); + const name = getIOShortName( + asyncInfo.awaited, + description, + asyncInfo.env, + rootEnv, + ); + const entryName = 'await ' + name; + const color = getIOColor(name); + const debugTask = asyncInfo.debugTask || asyncInfo.awaited.debugTask; + if (__DEV__ && debugTask) { + const properties: Array<[string, string]> = []; + if (typeof value === 'object' && value !== null) { + addObjectToProperties(value, properties, 0, ''); + } else if (value !== undefined) { + addValueToProperties('awaited value', value, properties, 0, ''); + } + const tooltipText = getIOLongName( + asyncInfo.awaited, + description, + asyncInfo.env, + rootEnv, + ); + debugTask.run( + // $FlowFixMe[method-unbinding] + performance.measure.bind(performance, entryName, { + start: startTime < 0 ? 0 : startTime, + end: endTime, + detail: { + devtools: { + color: color, + track: trackNames[trackIdx], + trackGroup: COMPONENTS_TRACK, + properties, + tooltipText, + }, + }, + }), + ); + } else { + console.timeStamp( + entryName, + startTime < 0 ? 0 : startTime, + endTime, + trackNames[trackIdx], + COMPONENTS_TRACK, + color, + ); + } + } +} + +export function logIOInfoErrored( + ioInfo: ReactIOInfo, + rootEnv: string, + error: mixed, +): void { + const startTime = ioInfo.start; + const endTime = ioInfo.end; + if (supportsUserTiming && endTime >= 0) { + const description = getIODescription(error); + const entryName = getIOShortName(ioInfo, description, ioInfo.env, rootEnv); + const debugTask = ioInfo.debugTask; + if (__DEV__ && debugTask) { + const message = + typeof error === 'object' && + error !== null && + typeof error.message === 'string' + ? // eslint-disable-next-line react-internal/safe-string-coercion + String(error.message) + : // eslint-disable-next-line react-internal/safe-string-coercion + String(error); + const properties = [['rejected with', message]]; + const tooltipText = + getIOLongName(ioInfo, description, ioInfo.env, rootEnv) + ' Rejected'; + debugTask.run( + // $FlowFixMe[method-unbinding] + performance.measure.bind(performance, '\u200b' + entryName, { + start: startTime < 0 ? 0 : startTime, + end: endTime, + detail: { + devtools: { + color: 'error', + track: IO_TRACK, + properties, + tooltipText, + }, + }, + }), + ); + } else { + console.timeStamp( + entryName, + startTime < 0 ? 0 : startTime, + endTime, + IO_TRACK, + undefined, + 'error', + ); + } + } +} + +export function logIOInfo( + ioInfo: ReactIOInfo, + rootEnv: string, + value: mixed, +): void { + const startTime = ioInfo.start; + const endTime = ioInfo.end; + if (supportsUserTiming && endTime >= 0) { + const description = getIODescription(value); + const entryName = getIOShortName(ioInfo, description, ioInfo.env, rootEnv); + const color = getIOColor(entryName); + const debugTask = ioInfo.debugTask; + if (__DEV__ && debugTask) { + const properties: Array<[string, string]> = []; + if (typeof value === 'object' && value !== null) { + addObjectToProperties(value, properties, 0, ''); + } else if (value !== undefined) { + addValueToProperties('Resolved', value, properties, 0, ''); + } + const tooltipText = getIOLongName( + ioInfo, + description, + ioInfo.env, + rootEnv, + ); + debugTask.run( + // $FlowFixMe[method-unbinding] + performance.measure.bind(performance, '\u200b' + entryName, { + start: startTime < 0 ? 0 : startTime, + end: endTime, + detail: { + devtools: { + color: color, + track: IO_TRACK, + properties, + tooltipText, + }, + }, + }), + ); + } else { + console.timeStamp( + entryName, + startTime < 0 ? 0 : startTime, + endTime, + IO_TRACK, + undefined, + color, ); } } diff --git a/packages/react-client/src/ReactFlightReplyClient.js b/packages/react-client/src/ReactFlightReplyClient.js index 6a0a37b787d34..f75f54f4ead0f 100644 --- a/packages/react-client/src/ReactFlightReplyClient.js +++ b/packages/react-client/src/ReactFlightReplyClient.js @@ -18,13 +18,10 @@ import type { import type {LazyComponent} from 'react/src/ReactLazy'; import type {TemporaryReferenceSet} from './ReactFlightTemporaryReferences'; -import {enableRenderableContext} from 'shared/ReactFeatureFlags'; - import { REACT_ELEMENT_TYPE, REACT_LAZY_TYPE, REACT_CONTEXT_TYPE, - REACT_PROVIDER_TYPE, getIteratorFn, ASYNC_ITERATOR, } from 'shared/ReactSymbols'; @@ -699,10 +696,7 @@ export function processReply( return serializeTemporaryReferenceMarker(); } if (__DEV__) { - if ( - (value: any).$$typeof === - (enableRenderableContext ? REACT_CONTEXT_TYPE : REACT_PROVIDER_TYPE) - ) { + if ((value: any).$$typeof === REACT_CONTEXT_TYPE) { console.error( 'React Context Providers cannot be passed to Server Functions from the Client.%s', describeObjectForErrorMessage(parent, key), @@ -1101,7 +1095,7 @@ function createFakeServerFunction, T>( } if (sourceMap) { - // We use the prefix rsc://React/ to separate these from other files listed in + // We use the prefix about://React/ to separate these from other files listed in // the Chrome DevTools. We need a "host name" and not just a protocol because // otherwise the group name becomes the root folder. Ideally we don't want to // show these at all but there's two reasons to assign a fake URL. @@ -1109,10 +1103,10 @@ function createFakeServerFunction, T>( // 2) If source maps are disabled or fails, you should at least be able to tell // which file it was. code += - '\n//# sourceURL=rsc://React/' + + '\n//# sourceURL=about://React/' + encodeURIComponent(environmentName) + '/' + - filename + + encodeURI(filename) + '?s' + // We add an extra s here to distinguish from the fake stack frames fakeServerFunctionIdx++; code += '\n//# sourceMappingURL=' + sourceMap; @@ -1189,7 +1183,7 @@ function bind(this: Function): Function { const referenceClosure = knownServerReferences.get(this); if (!referenceClosure) { - // $FlowFixMe[prop-missing] + // $FlowFixMe[incompatible-call] return FunctionBind.apply(this, arguments); } diff --git a/packages/react-client/src/__tests__/ReactFlight-test.js b/packages/react-client/src/__tests__/ReactFlight-test.js index b954f32ecd674..01f37319bf574 100644 --- a/packages/react-client/src/__tests__/ReactFlight-test.js +++ b/packages/react-client/src/__tests__/ReactFlight-test.js @@ -69,7 +69,7 @@ function getErrorForJestMatcher(error) { function normalizeComponentInfo(debugInfo) { if (Array.isArray(debugInfo.stack)) { - const {debugTask, debugStack, ...copy} = debugInfo; + const {debugTask, debugStack, debugLocation, ...copy} = debugInfo; copy.stack = formatV8Stack(debugInfo.stack); if (debugInfo.owner) { copy.owner = normalizeComponentInfo(debugInfo.owner); @@ -92,21 +92,27 @@ function getDebugInfo(obj) { return debugInfo; } -const heldValues = []; -let finalizationCallback; +const finalizationRegistries = []; function FinalizationRegistryMock(callback) { - finalizationCallback = callback; + this._heldValues = []; + this._callback = callback; + finalizationRegistries.push(this); } FinalizationRegistryMock.prototype.register = function (target, heldValue) { - heldValues.push(heldValue); + this._heldValues.push(heldValue); }; global.FinalizationRegistry = FinalizationRegistryMock; function gc() { - for (let i = 0; i < heldValues.length; i++) { - finalizationCallback(heldValues[i]); + for (let i = 0; i < finalizationRegistries.length; i++) { + const registry = finalizationRegistries[i]; + const callback = registry._callback; + const heldValues = registry._heldValues; + for (let j = 0; j < heldValues.length; j++) { + callback(heldValues[j]); + } + heldValues.length = 0; } - heldValues.length = 0; } let act; @@ -320,7 +326,6 @@ describe('ReactFlight', () => { name: 'Greeting', env: 'Server', key: null, - owner: null, stack: ' in Object. (at **)', props: { firstName: 'Seb', @@ -364,7 +369,6 @@ describe('ReactFlight', () => { name: 'Greeting', env: 'Server', key: null, - owner: null, stack: ' in Object. (at **)', props: { firstName: 'Seb', @@ -1308,6 +1312,11 @@ describe('ReactFlight', () => { ' at file:///testing.js:42:3', // async anon function (https://github.com/ChromeDevTools/devtools-frontend/blob/831be28facb4e85de5ee8c1acc4d98dfeda7a73b/test/unittests/front_end/panels/console/ErrorStackParser_test.ts#L130C9-L130C41) ' at async file:///testing.js:42:3', + // third-party RSC frame + // Ideally this would be a real frame produced by React not a mocked one. + ' at ThirdParty (about://React/ThirdParty/file:///code/%5Broot%2520of%2520the%2520server%5D.js?42:1:1)', + // We'll later filter this out based on line/column in `filterStackFrame`. + ' at ThirdPartyModule (file:///file-with-index-source-map.js:52656:16374)', // host component in parent stack ' at div ()', ...originalStackLines.slice(2), @@ -1356,13 +1365,19 @@ describe('ReactFlight', () => { } return `digest(${String(x)})`; }, - filterStackFrame(filename, functionName) { + filterStackFrame(filename, functionName, lineNumber, columnNumber) { + if (lineNumber === 52656 && columnNumber === 16374) { + return false; + } if (!filename) { // Allow anonymous return functionName === 'div'; } return ( - !filename.startsWith('node:') && !filename.includes('node_modules') + !filename.startsWith('node:') && + !filename.includes('node_modules') && + // sourceURL from an ES module in `/code/[root of the server].js` + filename !== 'file:///code/[root%20of%20the%20server].js' ); }, }); @@ -1957,8 +1972,8 @@ describe('ReactFlight', () => { }); expect(ReactNoop).toMatchRenderedOutput( <> -
    -
    +
    +
    , ); }); @@ -1981,8 +1996,8 @@ describe('ReactFlight', () => { }); expect(ReactNoop).toMatchRenderedOutput( <> -
    -
    +
    +
    , ); }); @@ -2021,8 +2036,8 @@ describe('ReactFlight', () => { assertLog(['ClientDoubler']); expect(ReactNoop).toMatchRenderedOutput( <> -
    «S1»
    -
    «S1»
    +
    _S_1_
    +
    _S_1_
    , ); }); @@ -2807,18 +2822,17 @@ describe('ReactFlight', () => { expect(getDebugInfo(promise)).toEqual( __DEV__ ? [ - {time: 20}, + {time: gate(flags => flags.enableAsyncDebugInfo) ? 22 : 20}, { name: 'ServerComponent', env: 'Server', key: null, - owner: null, stack: ' in Object. (at **)', props: { transport: expect.arrayContaining([]), }, }, - {time: 21}, + {time: gate(flags => flags.enableAsyncDebugInfo) ? 23 : 21}, ] : undefined, ); @@ -2829,49 +2843,46 @@ describe('ReactFlight', () => { expect(getDebugInfo(thirdPartyChildren[0])).toEqual( __DEV__ ? [ - {time: 14}, + {time: gate(flags => flags.enableAsyncDebugInfo) ? 24 : 22}, // Clamped to the start { name: 'ThirdPartyComponent', env: 'third-party', key: null, - owner: null, stack: ' in Object. (at **)', props: {}, }, - {time: 15}, - {time: 23}, // This last one is when the promise resolved into the first party. + {time: gate(flags => flags.enableAsyncDebugInfo) ? 24 : 22}, + {time: gate(flags => flags.enableAsyncDebugInfo) ? 25 : 23}, // This last one is when the promise resolved into the first party. ] : undefined, ); expect(getDebugInfo(thirdPartyChildren[1])).toEqual( __DEV__ ? [ - {time: 16}, + {time: gate(flags => flags.enableAsyncDebugInfo) ? 24 : 22}, // Clamped to the start { name: 'ThirdPartyLazyComponent', env: 'third-party', key: null, - owner: null, stack: ' in myLazy (at **)\n in lazyInitializer (at **)', props: {}, }, - {time: 17}, + {time: gate(flags => flags.enableAsyncDebugInfo) ? 24 : 22}, ] : undefined, ); expect(getDebugInfo(thirdPartyChildren[2])).toEqual( __DEV__ ? [ - {time: 12}, + {time: gate(flags => flags.enableAsyncDebugInfo) ? 24 : 22}, { name: 'ThirdPartyFragmentComponent', env: 'third-party', key: '3', - owner: null, stack: ' in Object. (at **)', props: {}, }, - {time: 13}, + {time: gate(flags => flags.enableAsyncDebugInfo) ? 24 : 22}, ] : undefined, ); @@ -2887,7 +2898,7 @@ describe('ReactFlight', () => { ); }); - // @gate enableAsyncIterableChildren + // @gate enableAsyncIterableChildren && enableComponentPerformanceTrack it('preserves debug info for server-to-server pass through of async iterables', async () => { let resolve; const iteratorPromise = new Promise(r => (resolve = r)); @@ -2941,12 +2952,24 @@ describe('ReactFlight', () => { name: 'ServerComponent', env: 'Server', key: null, - owner: null, stack: ' in Object. (at **)', props: { transport: expect.arrayContaining([]), }, }, + { + time: 16, + }, + { + env: 'third-party', + key: null, + name: 'ThirdPartyAsyncIterableComponent', + props: {}, + stack: ' in Object. (at **)', + }, + { + time: 16, + }, {time: 17}, ] : undefined, @@ -2961,12 +2984,24 @@ describe('ReactFlight', () => { name: 'Keyed', env: 'Server', key: 'keyed', - owner: null, stack: ' in ServerComponent (at **)', props: { children: {}, }, }, + { + time: 19, + }, + { + time: 19, + }, + { + env: 'third-party', + key: null, + name: 'ThirdPartyAsyncIterableComponent', + props: {}, + stack: ' in Object. (at **)', + }, {time: 19}, ] : undefined, @@ -2975,16 +3010,15 @@ describe('ReactFlight', () => { expect(getDebugInfo(thirdPartyFragment.props.children)).toEqual( __DEV__ ? [ - {time: 12}, + {time: 19}, // Clamp to the start { name: 'ThirdPartyAsyncIterableComponent', env: 'third-party', key: null, - owner: null, stack: ' in Object. (at **)', props: {}, }, - {time: 13}, + {time: 19}, ] : undefined, ); @@ -3000,6 +3034,64 @@ describe('ReactFlight', () => { ); }); + // @gate !__DEV__ || enableComponentPerformanceTrack + it('preserves debug info for server-to-server through use()', async () => { + function ThirdPartyComponent() { + return 'hi'; + } + + function ServerComponent({transport}) { + // This is a Server Component that receives other Server Components from a third party. + const text = ReactServer.use(ReactNoopFlightClient.read(transport)); + return
    {text.toUpperCase()}
    ; + } + + const thirdPartyTransport = ReactNoopFlightServer.render( + , + { + environmentName: 'third-party', + }, + ); + + const transport = ReactNoopFlightServer.render( + , + ); + + await act(async () => { + const promise = ReactNoopFlightClient.read(transport); + expect(getDebugInfo(promise)).toEqual( + __DEV__ + ? [ + {time: 16}, + { + name: 'ServerComponent', + env: 'Server', + key: null, + stack: ' in Object. (at **)', + props: { + transport: expect.arrayContaining([]), + }, + }, + {time: 16}, + { + name: 'ThirdPartyComponent', + env: 'third-party', + key: null, + stack: ' in Object. (at **)', + props: {}, + }, + {time: 16}, + {time: 17}, + ] + : undefined, + ); + const result = await promise; + ReactNoop.render(result); + }); + + expect(ReactNoop).toMatchRenderedOutput(
    HI
    ); + }); + it('preserves error stacks passed through server-to-server with source maps', async () => { async function ServerComponent({transport}) { // This is a Server Component that receives other Server Components from a third party. @@ -3007,7 +3099,7 @@ describe('ReactFlight', () => { ReactNoopFlightClient.read(transport, { findSourceMapURL(url) { // By giving a source map url we're saying that we can't use the original - // file as the sourceURL, which gives stack traces a rsc://React/ prefix. + // file as the sourceURL, which gives stack traces a about://React/ prefix. return 'source-map://' + url; }, }), @@ -3081,7 +3173,7 @@ describe('ReactFlight', () => { expectedErrorStack={expectedErrorStack}> {ReactNoopFlightClient.read(transport, { findSourceMapURL(url, environmentName) { - if (url.startsWith('rsc://React/')) { + if (url.startsWith('about://React/')) { // We don't expect to see any React prefixed URLs here. sawReactPrefix = true; } @@ -3137,7 +3229,6 @@ describe('ReactFlight', () => { name: 'Component', env: 'A', key: null, - owner: null, stack: ' in Object. (at **)', props: {}, }, @@ -3160,12 +3251,31 @@ describe('ReactFlight', () => { return 'hello'; } + class MyClass { + constructor() { + this.x = 1; + } + method() {} + get y() { + return this.x + 1; + } + get z() { + return this.x + 5; + } + } + Object.defineProperty(MyClass.prototype, 'y', {enumerable: true}); + + Object.defineProperty(MyClass, 'name', {value: 'MyClassName'}); + function ServerComponent() { console.log('hi', { prop: 123, fn: foo, map: new Map([['foo', foo]]), - promise: new Promise(() => {}), + promise: Promise.resolve('yo'), + infinitePromise: new Promise(() => {}), + Class: MyClass, + instance: new MyClass(), }); throw new Error('err'); } @@ -3210,9 +3320,14 @@ describe('ReactFlight', () => { }); ownerStacks = []; + // Let the Promises resolve. + await 0; + await 0; + await 0; + // The error should not actually get logged because we're not awaiting the root // so it's not thrown but the server log also shouldn't be replayed. - await ReactNoopFlightClient.read(transport); + await ReactNoopFlightClient.read(transport, {close: true}); expect(mockConsoleLog).toHaveBeenCalledTimes(1); expect(mockConsoleLog.mock.calls[0][0]).toBe('hi'); @@ -3228,9 +3343,41 @@ describe('ReactFlight', () => { expect(typeof loggedFn2).toBe('function'); expect(loggedFn2).not.toBe(foo); expect(loggedFn2.toString()).toBe(foo.toString()); + expect(loggedFn2).toBe(loggedFn); const promise = mockConsoleLog.mock.calls[0][1].promise; expect(promise).toBeInstanceOf(Promise); + expect(await promise).toBe('yo'); + + const infinitePromise = mockConsoleLog.mock.calls[0][1].infinitePromise; + expect(infinitePromise).toBeInstanceOf(Promise); + let resolved = false; + infinitePromise.then( + () => (resolved = true), + x => { + console.error(x); + resolved = true; + }, + ); + await 0; + await 0; + await 0; + // This should not reject upon aborting the stream. + expect(resolved).toBe(false); + + const Class = mockConsoleLog.mock.calls[0][1].Class; + const instance = mockConsoleLog.mock.calls[0][1].instance; + expect(typeof Class).toBe('function'); + expect(Class.prototype.constructor).toBe(Class); + expect(Class.name).toBe('MyClassName'); + expect(instance instanceof Class).toBe(true); + expect(Object.getPrototypeOf(instance)).toBe(Class.prototype); + expect(instance.x).toBe(1); + expect(instance.hasOwnProperty('y')).toBe(true); + expect(instance.y).toBe(2); // Enumerable getter was reified + expect(instance.hasOwnProperty('z')).toBe(false); + expect(instance.z).toBe(6); // Not enumerable getter was transferred as part of the toString() of the class + expect(typeof instance.method).toBe('function'); // Methods are included only if they're part of the toString() expect(ownerStacks).toEqual(['\n in App (at **)']); }); @@ -3279,19 +3426,10 @@ describe('ReactFlight', () => { await ReactNoopFlightClient.read(transport); expect(mockConsoleLog).toHaveBeenCalledTimes(1); - // TODO: Support cyclic objects in console encoding. - // expect(mockConsoleLog.mock.calls[0][0]).toBe('hi'); - // const cyclic2 = mockConsoleLog.mock.calls[0][1].cyclic; - // expect(cyclic2).not.toBe(cyclic); // Was serialized and therefore cloned - // expect(cyclic2.cycle).toBe(cyclic2); - expect(mockConsoleLog.mock.calls[0][0]).toBe( - 'Unknown Value: React could not send it from the server.', - ); - expect(mockConsoleLog.mock.calls[0][1].message).toBe( - 'Converting circular structure to JSON\n' + - " --> starting at object with constructor 'Object'\n" + - " --- property 'cycle' closes the circle", - ); + expect(mockConsoleLog.mock.calls[0][0]).toBe('hi'); + const cyclic2 = mockConsoleLog.mock.calls[0][1].cyclic; + expect(cyclic2).not.toBe(cyclic); // Was serialized and therefore cloned + expect(cyclic2.cycle).toBe(cyclic2); }); // @gate !__DEV__ || enableComponentPerformanceTrack @@ -3325,7 +3463,6 @@ describe('ReactFlight', () => { name: 'Greeting', env: 'Server', key: null, - owner: null, stack: ' in Object. (at **)', props: { firstName: 'Seb', @@ -3585,7 +3722,7 @@ describe('ReactFlight', () => { onError(x) { return `digest("${x.message}")`; }, - filterStackFrame(url, functionName) { + filterStackFrame(url, functionName, lineNumber, columnNumber) { return functionName !== 'intermediate'; }, }, @@ -3619,7 +3756,7 @@ describe('ReactFlight', () => { expect(caughtError.digest).toBe('digest("my-error")'); }); - // @gate __DEV__ && enableComponentPerformanceTrack + // @gate __DEV__ && enableComponentPerformanceTrack it('can render deep but cut off JSX in debug info', async () => { function createDeepJSX(n) { if (n <= 0) { diff --git a/packages/react-client/src/__tests__/ReactFlightDebugChannel-test.js b/packages/react-client/src/__tests__/ReactFlightDebugChannel-test.js new file mode 100644 index 0000000000000..e9428c3ba4074 --- /dev/null +++ b/packages/react-client/src/__tests__/ReactFlightDebugChannel-test.js @@ -0,0 +1,139 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @emails react-core + * @jest-environment node + */ + +'use strict'; + +if (typeof Blob === 'undefined') { + global.Blob = require('buffer').Blob; +} +if (typeof File === 'undefined' || typeof FormData === 'undefined') { + global.File = require('undici').File; + global.FormData = require('undici').FormData; +} + +function formatV8Stack(stack) { + let v8StyleStack = ''; + if (stack) { + for (let i = 0; i < stack.length; i++) { + const [name] = stack[i]; + if (v8StyleStack !== '') { + v8StyleStack += '\n'; + } + v8StyleStack += ' in ' + name + ' (at **)'; + } + } + return v8StyleStack; +} + +function normalizeComponentInfo(debugInfo) { + if (Array.isArray(debugInfo.stack)) { + const {debugTask, debugStack, ...copy} = debugInfo; + copy.stack = formatV8Stack(debugInfo.stack); + if (debugInfo.owner) { + copy.owner = normalizeComponentInfo(debugInfo.owner); + } + return copy; + } else { + return debugInfo; + } +} + +function getDebugInfo(obj) { + const debugInfo = obj._debugInfo; + if (debugInfo) { + const copy = []; + for (let i = 0; i < debugInfo.length; i++) { + copy.push(normalizeComponentInfo(debugInfo[i])); + } + return copy; + } + return debugInfo; +} + +let act; +let React; +let ReactNoop; +let ReactNoopFlightServer; +let ReactNoopFlightClient; + +describe('ReactFlight', () => { + beforeEach(() => { + // Mock performance.now for timing tests + let time = 10; + const now = jest.fn().mockImplementation(() => { + return time++; + }); + Object.defineProperty(performance, 'timeOrigin', { + value: time, + configurable: true, + }); + Object.defineProperty(performance, 'now', { + value: now, + configurable: true, + }); + + jest.resetModules(); + jest.mock('react', () => require('react/react.react-server')); + ReactNoopFlightServer = require('react-noop-renderer/flight-server'); + // This stores the state so we need to preserve it + const flightModules = require('react-noop-renderer/flight-modules'); + jest.resetModules(); + __unmockReact(); + jest.mock('react-noop-renderer/flight-modules', () => flightModules); + React = require('react'); + ReactNoop = require('react-noop-renderer'); + ReactNoopFlightClient = require('react-noop-renderer/flight-client'); + act = require('internal-test-utils').act; + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + // @gate __DEV__ && enableComponentPerformanceTrack + it('can render deep but cut off JSX in debug info', async () => { + function createDeepJSX(n) { + if (n <= 0) { + return null; + } + return
    {createDeepJSX(n - 1)}
    ; + } + + function ServerComponent(props) { + return
    not using props
    ; + } + + const debugChannel = {onMessage(message) {}}; + + const transport = ReactNoopFlightServer.render( + { + root: ( + + {createDeepJSX(100) /* deper than objectLimit */} + + ), + }, + {debugChannel}, + ); + + await act(async () => { + const rootModel = await ReactNoopFlightClient.read(transport, { + debugChannel, + }); + const root = rootModel.root; + const children = getDebugInfo(root)[1].props.children; + expect(children.type).toBe('div'); + expect(children.props.children.type).toBe('div'); + ReactNoop.render(root); + }); + + expect(ReactNoop).toMatchRenderedOutput(
    not using props
    ); + }); +}); diff --git a/packages/react-client/src/forks/ReactFlightClientConfig.custom.js b/packages/react-client/src/forks/ReactFlightClientConfig.custom.js index 2f204fd51b098..a6c0c933d2a58 100644 --- a/packages/react-client/src/forks/ReactFlightClientConfig.custom.js +++ b/packages/react-client/src/forks/ReactFlightClientConfig.custom.js @@ -35,6 +35,7 @@ export const resolveClientReference = $$$config.resolveClientReference; export const resolveServerReference = $$$config.resolveServerReference; export const preloadModule = $$$config.preloadModule; export const requireModule = $$$config.requireModule; +export const getModuleDebugInfo = $$$config.getModuleDebugInfo; export const dispatchHint = $$$config.dispatchHint; export const prepareDestinationForModule = $$$config.prepareDestinationForModule; diff --git a/packages/react-client/src/forks/ReactFlightClientConfig.dom-bun.js b/packages/react-client/src/forks/ReactFlightClientConfig.dom-bun.js index 75c942966bd99..24caf0df88f52 100644 --- a/packages/react-client/src/forks/ReactFlightClientConfig.dom-bun.js +++ b/packages/react-client/src/forks/ReactFlightClientConfig.dom-bun.js @@ -24,5 +24,6 @@ export const resolveClientReference: any = null; export const resolveServerReference: any = null; export const preloadModule: any = null; export const requireModule: any = null; +export const getModuleDebugInfo: any = null; export const prepareDestinationForModule: any = null; export const usedWithSSR = true; diff --git a/packages/react-client/src/forks/ReactFlightClientConfig.dom-legacy.js b/packages/react-client/src/forks/ReactFlightClientConfig.dom-legacy.js index 5be648ff0ad93..0f7381fc5a3f2 100644 --- a/packages/react-client/src/forks/ReactFlightClientConfig.dom-legacy.js +++ b/packages/react-client/src/forks/ReactFlightClientConfig.dom-legacy.js @@ -24,6 +24,7 @@ export const resolveClientReference: any = null; export const resolveServerReference: any = null; export const preloadModule: any = null; export const requireModule: any = null; +export const getModuleDebugInfo: any = null; export const dispatchHint: any = null; export const prepareDestinationForModule: any = null; export const usedWithSSR = true; diff --git a/packages/react-client/src/forks/ReactFlightClientConfig.markup.js b/packages/react-client/src/forks/ReactFlightClientConfig.markup.js index b0b2f198fd97b..fcd672450446f 100644 --- a/packages/react-client/src/forks/ReactFlightClientConfig.markup.js +++ b/packages/react-client/src/forks/ReactFlightClientConfig.markup.js @@ -62,6 +62,12 @@ export function requireModule(metadata: ClientReference): T { ); } +export function getModuleDebugInfo(metadata: ClientReference): null { + throw new Error( + 'renderToHTML should not have emitted Client References. This is a bug in React.', + ); +} + export const usedWithSSR = true; type HintCode = string; diff --git a/packages/react-debug-tools/src/ReactDebugHooks.js b/packages/react-debug-tools/src/ReactDebugHooks.js index 8242b27d4e5be..db9495a97dd4d 100644 --- a/packages/react-debug-tools/src/ReactDebugHooks.js +++ b/packages/react-debug-tools/src/ReactDebugHooks.js @@ -6,7 +6,7 @@ * * @flow */ - +import type {StackFrame as ParsedStackFrame} from 'error-stack-parser'; import type { Awaited, ReactContext, @@ -147,6 +147,8 @@ function getPrimitiveStackCache(): Map> { let currentFiber: null | Fiber = null; let currentHook: null | Hook = null; let currentContextDependency: null | ContextDependency = null; +let currentThenableIndex: number = 0; +let currentThenableState: null | Array> = null; function nextHook(): null | Hook { const hook = currentHook; @@ -201,7 +203,15 @@ function use(usable: Usable): T { if (usable !== null && typeof usable === 'object') { // $FlowFixMe[method-unbinding] if (typeof usable.then === 'function') { - const thenable: Thenable = (usable: any); + const thenable: Thenable = + // If we have thenable state, then the actually used thenable will be the one + // stashed in it. It's possible for uncached Promises to be new each render + // and in that case the one we're inspecting is the in the thenable state. + currentThenableState !== null && + currentThenableIndex < currentThenableState.length + ? currentThenableState[currentThenableIndex++] + : (usable: any); + switch (thenable.status) { case 'fulfilled': { const fulfilledValue: T = thenable.value; @@ -779,7 +789,7 @@ const Dispatcher: DispatcherType = { // create a proxy to throw a custom error // in case future versions of React adds more hooks -const DispatcherProxyHandler = { +const DispatcherProxyHandler: Proxy$traps = { get(target: DispatcherType, prop: string) { if (target.hasOwnProperty(prop)) { // $FlowFixMe[invalid-computed-prop] @@ -834,7 +844,11 @@ export type HooksTree = Array; let mostLikelyAncestorIndex = 0; -function findSharedIndex(hookStack: any, rootStack: any, rootIndex: number) { +function findSharedIndex( + hookStack: ParsedStackFrame[], + rootStack: ParsedStackFrame[], + rootIndex: number, +) { const source = rootStack[rootIndex].source; hookSearch: for (let i = 0; i < hookStack.length; i++) { if (hookStack[i].source === source) { @@ -855,7 +869,10 @@ function findSharedIndex(hookStack: any, rootStack: any, rootIndex: number) { return -1; } -function findCommonAncestorIndex(rootStack: any, hookStack: any) { +function findCommonAncestorIndex( + rootStack: ParsedStackFrame[], + hookStack: ParsedStackFrame[], +) { let rootIndex = findSharedIndex( hookStack, rootStack, @@ -876,7 +893,7 @@ function findCommonAncestorIndex(rootStack: any, hookStack: any) { return -1; } -function isReactWrapper(functionName: any, wrapperName: string) { +function isReactWrapper(functionName: void | string, wrapperName: string) { const hookName = parseHookName(functionName); if (wrapperName === 'HostTransitionStatus') { return hookName === wrapperName || hookName === 'FormStatus'; @@ -885,7 +902,7 @@ function isReactWrapper(functionName: any, wrapperName: string) { return hookName === wrapperName; } -function findPrimitiveIndex(hookStack: any, hook: HookLogEntry) { +function findPrimitiveIndex(hookStack: ParsedStackFrame[], hook: HookLogEntry) { const stackCache = getPrimitiveStackCache(); const primitiveStack = stackCache.get(hook.primitive); if (primitiveStack === undefined) { @@ -916,7 +933,7 @@ function findPrimitiveIndex(hookStack: any, hook: HookLogEntry) { return -1; } -function parseTrimmedStack(rootStack: any, hook: HookLogEntry) { +function parseTrimmedStack(rootStack: ParsedStackFrame[], hook: HookLogEntry) { // Get the stack trace between the primitive hook function and // the root function call. I.e. the stack frames of custom hooks. const hookStack = ErrorStackParser.parse(hook.stackError); @@ -977,7 +994,7 @@ function parseHookName(functionName: void | string): string { } function buildTree( - rootStack: any, + rootStack: ParsedStackFrame[], readHookLog: Array, ): HooksTree { const rootChildren: Array = []; @@ -1034,10 +1051,20 @@ function buildTree( subHooks: children, debugInfo: null, hookSource: { - lineNumber: stackFrame.lineNumber, - columnNumber: stackFrame.columnNumber, - functionName: stackFrame.functionName, - fileName: stackFrame.fileName, + lineNumber: + stackFrame.lineNumber === undefined + ? null + : stackFrame.lineNumber, + columnNumber: + stackFrame.columnNumber === undefined + ? null + : stackFrame.columnNumber, + functionName: + stackFrame.functionName === undefined + ? null + : stackFrame.functionName, + fileName: + stackFrame.fileName === undefined ? null : stackFrame.fileName, }, }; @@ -1082,10 +1109,14 @@ function buildTree( }; if (stack && stack.length >= 1) { const stackFrame = stack[0]; - hookSource.lineNumber = stackFrame.lineNumber; - hookSource.functionName = stackFrame.functionName; - hookSource.fileName = stackFrame.fileName; - hookSource.columnNumber = stackFrame.columnNumber; + hookSource.lineNumber = + stackFrame.lineNumber === undefined ? null : stackFrame.lineNumber; + hookSource.functionName = + stackFrame.functionName === undefined ? null : stackFrame.functionName; + hookSource.fileName = + stackFrame.fileName === undefined ? null : stackFrame.fileName; + hookSource.columnNumber = + stackFrame.columnNumber === undefined ? null : stackFrame.columnNumber; } levelChild.hookSource = hookSource; @@ -1191,7 +1222,10 @@ export function inspectHooks( // $FlowFixMe[incompatible-use] found when upgrading Flow currentDispatcher.H = previousDispatcher; } - const rootStack = ErrorStackParser.parse(ancestorStackError); + const rootStack = + ancestorStackError === undefined + ? ([]: ParsedStackFrame[]) + : ErrorStackParser.parse(ancestorStackError); return buildTree(rootStack, readHookLog); } @@ -1239,7 +1273,10 @@ function inspectHooksOfForwardRef( hookLog = []; currentDispatcher.H = previousDispatcher; } - const rootStack = ErrorStackParser.parse(ancestorStackError); + const rootStack = + ancestorStackError === undefined + ? ([]: ParsedStackFrame[]) + : ErrorStackParser.parse(ancestorStackError); return buildTree(rootStack, readHookLog); } @@ -1285,6 +1322,14 @@ export function inspectHooksOfFiber( // current state from them. currentHook = (fiber.memoizedState: Hook); currentFiber = fiber; + const thenableState = + fiber.dependencies && fiber.dependencies._debugThenableState; + // In DEV the thenableState is an inner object. + const usedThenables: any = thenableState + ? thenableState.thenables || thenableState + : null; + currentThenableState = Array.isArray(usedThenables) ? usedThenables : null; + currentThenableIndex = 0; if (hasOwnProperty.call(currentFiber, 'dependencies')) { // $FlowFixMe[incompatible-use]: Flow thinks hasOwnProperty might have nulled `currentFiber` @@ -1339,6 +1384,8 @@ export function inspectHooksOfFiber( currentFiber = null; currentHook = null; currentContextDependency = null; + currentThenableState = null; + currentThenableIndex = 0; restoreContexts(contextMap); } diff --git a/packages/react-debug-tools/src/__tests__/ReactHooksInspection-test.js b/packages/react-debug-tools/src/__tests__/ReactHooksInspection-test.js index 8d159a22105c0..18dab0710f259 100644 --- a/packages/react-debug-tools/src/__tests__/ReactHooksInspection-test.js +++ b/packages/react-debug-tools/src/__tests__/ReactHooksInspection-test.js @@ -720,6 +720,53 @@ describe('ReactHooksInspection', () => { ); }); + it('should inspect use() calls in anonymous loops', () => { + function Foo({entries}) { + const values = Object.fromEntries( + Object.entries(entries).map(([key, value]) => { + return [key, React.use(value)]; + }), + ); + return
    {values}
    ; + } + const tree = ReactDebugTools.inspectHooks(Foo, { + entries: {one: Promise.resolve('one'), two: Promise.resolve('two')}, + }); + const results = normalizeSourceLoc(tree); + expect(results).toHaveLength(1); + expect(results[0]).toMatchInlineSnapshot(` + { + "debugInfo": null, + "hookSource": { + "columnNumber": 0, + "fileName": "**", + "functionName": "Foo", + "lineNumber": 0, + }, + "id": null, + "isStateEditable": false, + "name": "", + "subHooks": [ + { + "debugInfo": null, + "hookSource": { + "columnNumber": 0, + "fileName": "**", + "functionName": null, + "lineNumber": 0, + }, + "id": null, + "isStateEditable": false, + "name": "Use", + "subHooks": [], + "value": Promise {}, + }, + ], + "value": undefined, + } + `); + }); + describe('useDebugValue', () => { it('should be ignored when called outside of a custom hook', () => { function Foo(props) { diff --git a/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js b/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js index efa59d8605ed2..a150ded10f75f 100644 --- a/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js +++ b/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js @@ -14,7 +14,6 @@ let React; let ReactTestRenderer; let ReactDebugTools; let act; -let assertConsoleErrorDev; let useMemoCache; function normalizeSourceLoc(tree) { @@ -34,7 +33,7 @@ describe('ReactHooksInspectionIntegration', () => { jest.resetModules(); React = require('react'); ReactTestRenderer = require('react-test-renderer'); - ({act, assertConsoleErrorDev} = require('internal-test-utils')); + ({act} = require('internal-test-utils')); ReactDebugTools = require('react-debug-tools'); useMemoCache = require('react/compiler-runtime').c; }); @@ -886,7 +885,7 @@ describe('ReactHooksInspectionIntegration', () => { "hookSource": { "columnNumber": 0, "fileName": "**", - "functionName": undefined, + "functionName": null, "lineNumber": 0, }, "id": 0, @@ -1553,7 +1552,7 @@ describe('ReactHooksInspectionIntegration', () => { expect(tree[0].id).toEqual(0); expect(tree[0].isStateEditable).toEqual(false); expect(tree[0].name).toEqual('Id'); - expect(String(tree[0].value).startsWith('\u00ABr')).toBe(true); + expect(String(tree[0].value).startsWith('_r_')).toBe(true); expect(normalizeSourceLoc(tree)[1]).toMatchInlineSnapshot(` { @@ -2321,57 +2320,6 @@ describe('ReactHooksInspectionIntegration', () => { }); }); - // @gate !disableDefaultPropsExceptForClasses - it('should support defaultProps and lazy', async () => { - const Suspense = React.Suspense; - - function Foo(props) { - const [value] = React.useState(props.defaultValue.slice(0, 3)); - return
    {value}
    ; - } - Foo.defaultProps = { - defaultValue: 'default', - }; - - async function fakeImport(result) { - return {default: result}; - } - - const LazyFoo = React.lazy(() => fakeImport(Foo)); - - const renderer = ReactTestRenderer.create( - - - , - ); - - await act(async () => await LazyFoo); - assertConsoleErrorDev([ - 'Foo: Support for defaultProps will be removed from function components in a future major release. Use JavaScript default parameters instead.', - ]); - - const childFiber = renderer.root._currentFiber(); - const tree = ReactDebugTools.inspectHooksOfFiber(childFiber); - expect(normalizeSourceLoc(tree)).toMatchInlineSnapshot(` - [ - { - "debugInfo": null, - "hookSource": { - "columnNumber": 0, - "fileName": "**", - "functionName": "Foo", - "lineNumber": 0, - }, - "id": 0, - "isStateEditable": true, - "name": "State", - "subHooks": [], - "value": "def", - }, - ] - `); - }); - // This test case is based on an open source bug report: // https://github.com/facebookincubator/redux-react-hook/issues/34#issuecomment-466693787 it('should properly advance the current hook for useContext', async () => { diff --git a/packages/react-devtools-core/package.json b/packages/react-devtools-core/package.json index 581a24edba342..8ce8f436e815d 100644 --- a/packages/react-devtools-core/package.json +++ b/packages/react-devtools-core/package.json @@ -1,6 +1,6 @@ { "name": "react-devtools-core", - "version": "6.1.2", + "version": "6.1.5", "description": "Use react-devtools outside of the browser", "license": "MIT", "main": "./dist/backend.js", diff --git a/packages/react-devtools-core/src/standalone.js b/packages/react-devtools-core/src/standalone.js index d65b41b478fa2..f8286783a9b44 100644 --- a/packages/react-devtools-core/src/standalone.js +++ b/packages/react-devtools-core/src/standalone.js @@ -26,7 +26,7 @@ import { import {localStorageSetItem} from 'react-devtools-shared/src/storage'; import type {FrontendBridge} from 'react-devtools-shared/src/bridge'; -import type {Source} from 'react-devtools-shared/src/shared/types'; +import type {ReactFunctionLocation, ReactCallSite} from 'shared/ReactTypes'; export type StatusTypes = 'server-connected' | 'devtools-connected' | 'error'; export type StatusListener = (message: string, status: StatusTypes) => void; @@ -144,29 +144,27 @@ async function fetchFileWithCaching(url: string) { } function canViewElementSourceFunction( - _source: Source, - symbolicatedSource: Source | null, + _source: ReactFunctionLocation | ReactCallSite, + symbolicatedSource: ReactFunctionLocation | ReactCallSite | null, ): boolean { if (symbolicatedSource == null) { return false; } + const [, sourceURL, ,] = symbolicatedSource; - return doesFilePathExist(symbolicatedSource.sourceURL, projectRoots); + return doesFilePathExist(sourceURL, projectRoots); } function viewElementSourceFunction( - _source: Source, - symbolicatedSource: Source | null, + _source: ReactFunctionLocation | ReactCallSite, + symbolicatedSource: ReactFunctionLocation | ReactCallSite | null, ): void { if (symbolicatedSource == null) { return; } - launchEditor( - symbolicatedSource.sourceURL, - symbolicatedSource.line, - projectRoots, - ); + const [, sourceURL, line] = symbolicatedSource; + launchEditor(sourceURL, line, projectRoots); } function onDisconnected() { diff --git a/packages/react-devtools-core/webpack.backend.js b/packages/react-devtools-core/webpack.backend.js index 32d4fadcb5884..c1312fc6d8ec8 100644 --- a/packages/react-devtools-core/webpack.backend.js +++ b/packages/react-devtools-core/webpack.backend.js @@ -72,6 +72,7 @@ module.exports = { __IS_CHROME__: false, __IS_EDGE__: false, __IS_NATIVE__: true, + __IS_INTERNAL_MCP_BUILD__: 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-core/webpack.standalone.js b/packages/react-devtools-core/webpack.standalone.js index 8caadec10b070..6a9636c6911b1 100644 --- a/packages/react-devtools-core/webpack.standalone.js +++ b/packages/react-devtools-core/webpack.standalone.js @@ -91,6 +91,7 @@ module.exports = { __IS_FIREFOX__: false, __IS_CHROME__: false, __IS_EDGE__: false, + __IS_INTERNAL_MCP_BUILD__: false, 'process.env.DEVTOOLS_PACKAGE': `"react-devtools-core"`, 'process.env.DEVTOOLS_VERSION': `"${DEVTOOLS_VERSION}"`, 'process.env.EDITOR_URL': EDITOR_URL != null ? `"${EDITOR_URL}"` : null, diff --git a/packages/react-devtools-extensions/chrome/manifest.json b/packages/react-devtools-extensions/chrome/manifest.json index fa4607d1c69cd..127185f73e430 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": "6.1.2", - "version_name": "6.1.2", + "version": "6.1.5", + "version_name": "6.1.5", "minimum_chrome_version": "114", "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 59d4a8b1ff996..f930a6d0ac7cf 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": "6.1.2", - "version_name": "6.1.2", + "version": "6.1.5", + "version_name": "6.1.5", "minimum_chrome_version": "114", "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 3080eadbb003b..24b5b11323f69 100644 --- a/packages/react-devtools-extensions/firefox/manifest.json +++ b/packages/react-devtools-extensions/firefox/manifest.json @@ -2,7 +2,7 @@ "manifest_version": 3, "name": "React Developer Tools", "description": "Adds React debugging tools to the Firefox Developer Tools.", - "version": "6.1.2", + "version": "6.1.5", "browser_specific_settings": { "gecko": { "id": "@react-devtools", diff --git a/packages/react-devtools-extensions/src/background/dynamicallyInjectContentScripts.js b/packages/react-devtools-extensions/src/background/dynamicallyInjectContentScripts.js index f1a3598a519ca..3e2f6b7c1ea83 100644 --- a/packages/react-devtools-extensions/src/background/dynamicallyInjectContentScripts.js +++ b/packages/react-devtools-extensions/src/background/dynamicallyInjectContentScripts.js @@ -6,7 +6,7 @@ const contentScriptsToInject = [ js: ['build/proxy.js'], matches: [''], persistAcrossSessions: true, - runAt: 'document_end', + runAt: 'document_start', world: chrome.scripting.ExecutionWorld.ISOLATED, }, { diff --git a/packages/react-devtools-extensions/src/background/executeScript.js b/packages/react-devtools-extensions/src/background/executeScript.js index 8b80095d33c2e..a196a7391cdff 100644 --- a/packages/react-devtools-extensions/src/background/executeScript.js +++ b/packages/react-devtools-extensions/src/background/executeScript.js @@ -1,6 +1,20 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ /* global chrome */ -export function executeScriptInIsolatedWorld({target, files}) { +export function executeScriptInIsolatedWorld({ + target, + files, +}: { + files: any, + target: any, +}): Promise { return chrome.scripting.executeScript({ target, files, @@ -8,10 +22,20 @@ export function executeScriptInIsolatedWorld({target, files}) { }); } -export function executeScriptInMainWorld({target, files}) { +export function executeScriptInMainWorld({ + target, + files, + injectImmediately, +}: { + files: any, + target: any, + // It's nice to have this required to make active choices. + injectImmediately: boolean, +}): Promise { return chrome.scripting.executeScript({ target, files, + injectImmediately, world: chrome.scripting.ExecutionWorld.MAIN, }); } diff --git a/packages/react-devtools-extensions/src/background/messageHandlers.js b/packages/react-devtools-extensions/src/background/messageHandlers.js index 5afcd6aadcc07..0152418633fbb 100644 --- a/packages/react-devtools-extensions/src/background/messageHandlers.js +++ b/packages/react-devtools-extensions/src/background/messageHandlers.js @@ -1,5 +1,6 @@ /* global chrome */ +import {__DEBUG__} from 'react-devtools-shared/src/constants'; import setExtensionIconAndPopup from './setExtensionIconAndPopup'; import {executeScriptInMainWorld} from './executeScript'; @@ -25,6 +26,7 @@ export function handleBackendManagerMessage(message, sender) { payload.versions.forEach(version => { if (EXTENSION_CONTAINED_VERSIONS.includes(version)) { executeScriptInMainWorld({ + injectImmediately: true, target: {tabId: sender.tab.id}, files: [`/build/react_devtools_backend_${version}.js`], }); @@ -46,22 +48,26 @@ export function handleDevToolsPageMessage(message) { payload: {tabId, url}, } = message; - if (!tabId) { - throw new Error("Couldn't fetch file sources: tabId not specified"); - } - - if (!url) { - throw new Error("Couldn't fetch file sources: url not specified"); + if (!tabId || !url) { + // Send a response straight away to get the Promise fulfilled. + chrome.runtime.sendMessage({ + source: 'react-devtools-background', + payload: { + type: 'fetch-file-with-cache-error', + url, + value: null, + }, + }); + } else { + chrome.tabs.sendMessage(tabId, { + source: 'devtools-page', + payload: { + type: 'fetch-file-with-cache', + url, + }, + }); } - chrome.tabs.sendMessage(tabId, { - source: 'devtools-page', - payload: { - type: 'fetch-file-with-cache', - url, - }, - }); - break; } @@ -75,9 +81,19 @@ export function handleDevToolsPageMessage(message) { } executeScriptInMainWorld({ + injectImmediately: true, target: {tabId}, files: ['/build/backendManager.js'], - }); + }).then( + () => { + if (__DEBUG__) { + console.log('Successfully injected backend manager'); + } + }, + reason => { + console.error('Failed to inject backend manager:', reason); + }, + ); break; } diff --git a/packages/react-devtools-extensions/src/contentScripts/proxy.js b/packages/react-devtools-extensions/src/contentScripts/proxy.js index 8ffeeffb2af53..02253f65d4a83 100644 --- a/packages/react-devtools-extensions/src/contentScripts/proxy.js +++ b/packages/react-devtools-extensions/src/contentScripts/proxy.js @@ -1,8 +1,16 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ /* global chrome */ 'use strict'; -window.addEventListener('pageshow', function ({target}) { +function injectProxy({target}: {target: any}) { // Firefox's behaviour for injecting this content script can be unpredictable // While navigating the history, some content scripts might not be re-injected and still be alive if (!window.__REACT_DEVTOOLS_PROXY_INJECTED__) { @@ -14,7 +22,7 @@ window.addEventListener('pageshow', function ({target}) { // The backend waits to install the global hook until notified by the content script. // In the event of a page reload, the content script might be loaded before the backend manager is injected. // Because of this we need to poll the backend manager until it has been initialized. - const intervalID = setInterval(() => { + const intervalID: IntervalID = setInterval(() => { if (backendInitialized) { clearInterval(intervalID); } else { @@ -22,7 +30,11 @@ window.addEventListener('pageshow', function ({target}) { } }, 500); } -}); +} + +window.addEventListener('pagereveal', injectProxy); +// For backwards compat with browsers not implementing `pagereveal` which is a fairly new event. +window.addEventListener('pageshow', injectProxy); window.addEventListener('pagehide', function ({target}) { if (target !== window.document) { @@ -45,7 +57,7 @@ function sayHelloToBackendManager() { ); } -function handleMessageFromDevtools(message) { +function handleMessageFromDevtools(message: any) { window.postMessage( { source: 'react-devtools-content-script', @@ -55,7 +67,7 @@ function handleMessageFromDevtools(message) { ); } -function handleMessageFromPage(event) { +function handleMessageFromPage(event: any) { if (event.source !== window || !event.data) { return; } @@ -65,6 +77,7 @@ function handleMessageFromPage(event) { case 'react-devtools-bridge': { backendInitialized = true; + // $FlowFixMe[incompatible-use] port.postMessage(event.data.payload); break; } @@ -99,6 +112,8 @@ function connectPort() { window.addEventListener('message', handleMessageFromPage); + // $FlowFixMe[incompatible-use] port.onMessage.addListener(handleMessageFromDevtools); + // $FlowFixMe[incompatible-use] port.onDisconnect.addListener(handleDisconnect); } diff --git a/packages/react-devtools-extensions/src/main/index.js b/packages/react-devtools-extensions/src/main/index.js index 63d25819539f1..362793e3eb430 100644 --- a/packages/react-devtools-extensions/src/main/index.js +++ b/packages/react-devtools-extensions/src/main/index.js @@ -1,5 +1,7 @@ /* global chrome */ +import type {SourceSelection} from 'react-devtools-shared/src/devtools/views/Editor/EditorPane'; + import {createElement} from 'react'; import {flushSync} from 'react-dom'; import {createRoot} from 'react-dom/client'; @@ -16,7 +18,12 @@ import { LOCAL_STORAGE_TRACE_UPDATES_ENABLED_KEY, } from 'react-devtools-shared/src/constants'; import {logEvent} from 'react-devtools-shared/src/Logger'; -import {normalizeUrlIfValid} from 'react-devtools-shared/src/utils'; +import { + getAlwaysOpenInEditor, + getOpenInEditorURL, + normalizeUrlIfValid, +} from 'react-devtools-shared/src/utils'; +import {checkConditions} from 'react-devtools-shared/src/devtools/views/Editor/utils'; import { setBrowserSelectionFromReact, @@ -73,12 +80,48 @@ function createBridge() { ); }); + const sourcesPanel = chrome.devtools.panels.sources; + const onBrowserElementSelectionChanged = () => setReactSelectionFromBrowser(bridge); + const onBrowserSourceSelectionChanged = (location: { + url: string, + startLine: number, + startColumn: number, + endLine: number, + endColumn: number, + }) => { + if ( + currentSelectedSource === null || + currentSelectedSource.url !== location.url + ) { + currentSelectedSource = { + url: location.url, + selectionRef: { + // We use 1-based line and column, Chrome provides them 0-based. + line: location.startLine + 1, + column: location.startColumn + 1, + }, + }; + // Rerender with the new file selection. + render(); + } else { + // Update the ref to the latest position without updating the url. No need to rerender. + const selectionRef = currentSelectedSource.selectionRef; + selectionRef.line = location.startLine + 1; + selectionRef.column = location.startColumn + 1; + } + }; const onBridgeShutdown = () => { chrome.devtools.panels.elements.onSelectionChanged.removeListener( onBrowserElementSelectionChanged, ); + if (sourcesPanel && sourcesPanel.onSelectionChanged) { + currentSelectedSource = null; + sourcesPanel.onSelectionChanged.removeListener( + onBrowserSourceSelectionChanged, + ); + } }; bridge.addListener('shutdown', onBridgeShutdown); @@ -86,6 +129,11 @@ function createBridge() { chrome.devtools.panels.elements.onSelectionChanged.addListener( onBrowserElementSelectionChanged, ); + if (sourcesPanel && sourcesPanel.onSelectionChanged) { + sourcesPanel.onSelectionChanged.addListener( + onBrowserSourceSelectionChanged, + ); + } } function createBridgeAndStore() { @@ -103,6 +151,10 @@ function createBridgeAndStore() { supportsClickToInspect: true, }); + store.addListener('enableSuspenseTab', () => { + createSuspensePanel(); + }); + store.addListener('settingsUpdated', settings => { chrome.storage.local.set(settings); }); @@ -124,7 +176,7 @@ function createBridgeAndStore() { }; const viewElementSourceFunction = (source, symbolicatedSource) => { - const {sourceURL, line, column} = symbolicatedSource + const [, sourceURL, line, column] = symbolicatedSource ? symbolicatedSource : source; @@ -152,13 +204,16 @@ function createBridgeAndStore() { bridge, browserTheme: getBrowserTheme(), componentsPortalContainer, + profilerPortalContainer, + editorPortalContainer, + currentSelectedSource, enabledInspectedElementContextMenu: true, fetchFileWithCaching, hookNamesModuleLoaderFunction, overrideTab, - profilerPortalContainer, showTabBar: false, store, + suspensePortalContainer, warnIfUnsupportedVersionDetected: true, viewAttributeSourceFunction, // Firefox doesn't support chrome.devtools.panels.openResource yet @@ -257,6 +312,89 @@ function createProfilerPanel() { ); } +function createSourcesEditorPanel() { + if (editorPortalContainer) { + // Panel is created and user opened it at least once + ensureInitialHTMLIsCleared(editorPortalContainer); + render(); + + return; + } + + if (editorPane) { + // Panel is created, but wasn't opened yet, so no document is present for it + return; + } + + const sourcesPanel = chrome.devtools.panels.sources; + if (!sourcesPanel || !sourcesPanel.createSidebarPane) { + // Firefox doesn't currently support extending the source panel. + return; + } + + sourcesPanel.createSidebarPane('Code Editor ⚛', createdPane => { + editorPane = createdPane; + + createdPane.setPage('panel.html'); + createdPane.setHeight('75px'); + + createdPane.onShown.addListener(portal => { + editorPortalContainer = portal.container; + if (editorPortalContainer != null && render) { + ensureInitialHTMLIsCleared(editorPortalContainer); + + render(); + portal.injectStyles(cloneStyleTags); + + logEvent({event_name: 'selected-editor-pane'}); + } + }); + + createdPane.onShown.addListener(() => { + bridge.emit('extensionEditorPaneShown'); + }); + createdPane.onHidden.addListener(() => { + bridge.emit('extensionEditorPaneHidden'); + }); + }); +} + +function createSuspensePanel() { + if (suspensePortalContainer) { + // Panel is created and user opened it at least once + ensureInitialHTMLIsCleared(suspensePortalContainer); + render('suspense'); + + return; + } + + if (suspensePanel) { + // Panel is created, but wasn't opened yet, so no document is present for it + return; + } + + chrome.devtools.panels.create( + __IS_CHROME__ || __IS_EDGE__ ? 'Suspense ⚛' : 'Suspense', + __IS_EDGE__ ? 'icons/production.svg' : '', + 'panel.html', + createdPanel => { + suspensePanel = createdPanel; + + createdPanel.onShown.addListener(portal => { + suspensePortalContainer = portal.container; + if (suspensePortalContainer != null && render) { + ensureInitialHTMLIsCleared(suspensePortalContainer); + + render('suspense'); + portal.injectStyles(cloneStyleTags); + + logEvent({event_name: 'selected-suspense-tab'}); + } + }); + }, + ); +} + function performInTabNavigationCleanup() { // Potentially, if react hasn't loaded yet and user performs in-tab navigation clearReactPollingInstance(); @@ -268,7 +406,12 @@ function performInTabNavigationCleanup() { // If panels were already created, and we have already mounted React root to display // tabs (Components or Profiler), we should unmount root first and render them again - if ((componentsPortalContainer || profilerPortalContainer) && root) { + if ( + (componentsPortalContainer || + profilerPortalContainer || + suspensePortalContainer) && + root + ) { // It's easiest to recreate the DevTools panel (to clean up potential stale state). // We can revisit this in the future as a small optimization. // This should also emit bridge.shutdown, but only if this root was mounted @@ -298,7 +441,12 @@ function performFullCleanup() { // Potentially, if react hasn't loaded yet and user closed the browser DevTools clearReactPollingInstance(); - if ((componentsPortalContainer || profilerPortalContainer) && root) { + if ( + (componentsPortalContainer || + profilerPortalContainer || + suspensePortalContainer) && + root + ) { // This should also emit bridge.shutdown, but only if this root was mounted flushSync(() => root.unmount()); } else { @@ -307,6 +455,7 @@ function performFullCleanup() { componentsPortalContainer = null; profilerPortalContainer = null; + suspensePortalContainer = null; root = null; mostRecentOverrideTab = null; @@ -356,6 +505,9 @@ function mountReactDevTools() { createComponentsPanel(); createProfilerPanel(); + createSourcesEditorPanel(); + // Suspense Tab is created via the hook + // TODO(enableSuspenseTab): Create eagerly once Suspense tab is stable } let reactPollingInstance = null; @@ -376,6 +528,12 @@ function showNoReactDisclaimer() { '

    Looks like this page doesn\'t have React, or it hasn\'t been loaded yet.

    '; delete profilerPortalContainer._hasInitialHTMLBeenCleared; } + + if (suspensePortalContainer) { + suspensePortalContainer.innerHTML = + '

    Looks like this page doesn\'t have React, or it hasn\'t been loaded yet.

    '; + delete suspensePortalContainer._hasInitialHTMLBeenCleared; + } } function mountReactDevToolsWhenReactHasLoaded() { @@ -394,13 +552,19 @@ let profilingData = null; let componentsPanel = null; let profilerPanel = null; +let suspensePanel = null; +let editorPane = null; let componentsPortalContainer = null; let profilerPortalContainer = null; +let suspensePortalContainer = null; +let editorPortalContainer = null; let mostRecentOverrideTab = null; let render = null; let root = null; +let currentSelectedSource: null | SourceSelection = null; + let port = null; // In case when multiple navigation events emitted in a short period of time @@ -433,3 +597,45 @@ if (__IS_FIREFOX__) { connectExtensionPort(); mountReactDevToolsWhenReactHasLoaded(); + +function onThemeChanged(themeName) { + // Rerender with the new theme + render(); +} + +if (chrome.devtools.panels.setThemeChangeHandler) { + // Chrome + chrome.devtools.panels.setThemeChangeHandler(onThemeChanged); +} else if (chrome.devtools.panels.onThemeChanged) { + // Firefox + chrome.devtools.panels.onThemeChanged.addListener(onThemeChanged); +} + +// Firefox doesn't support resources handlers yet. +if (chrome.devtools.panels.setOpenResourceHandler) { + chrome.devtools.panels.setOpenResourceHandler( + ( + resource, + lineNumber = 1, + // The column is a new feature so we have to specify a default if it doesn't exist + columnNumber = 1, + ) => { + const alwaysOpenInEditor = getAlwaysOpenInEditor(); + const editorURL = getOpenInEditorURL(); + if (alwaysOpenInEditor && editorURL) { + const location = ['', resource.url, lineNumber, columnNumber]; + const {url, shouldDisableButton} = checkConditions(editorURL, location); + if (!shouldDisableButton) { + window.open(url); + return; + } + } + // Otherwise fallback to the built-in behavior. + chrome.devtools.panels.openResource( + resource.url, + lineNumber - 1, + columnNumber - 1, + ); + }, + ); +} diff --git a/packages/react-devtools-extensions/webpack.backend.js b/packages/react-devtools-extensions/webpack.backend.js index effa6cc330bb0..4bfa05183067e 100644 --- a/packages/react-devtools-extensions/webpack.backend.js +++ b/packages/react-devtools-extensions/webpack.backend.js @@ -78,6 +78,7 @@ module.exports = { __IS_FIREFOX__: IS_FIREFOX, __IS_EDGE__: IS_EDGE, __IS_NATIVE__: false, + __IS_INTERNAL_MCP_BUILD__: false, }), new Webpack.SourceMapDevToolPlugin({ filename: '[file].map', diff --git a/packages/react-devtools-extensions/webpack.config.js b/packages/react-devtools-extensions/webpack.config.js index 51b8f4e2105e3..4a3052517c851 100644 --- a/packages/react-devtools-extensions/webpack.config.js +++ b/packages/react-devtools-extensions/webpack.config.js @@ -33,6 +33,8 @@ const IS_FIREFOX = process.env.IS_FIREFOX === 'true'; const IS_EDGE = process.env.IS_EDGE === 'true'; const IS_INTERNAL_VERSION = process.env.FEATURE_FLAG_TARGET === 'extension-fb'; +const IS_INTERNAL_MCP_BUILD = process.env.IS_INTERNAL_MCP_BUILD === 'true'; + const featureFlagTarget = process.env.FEATURE_FLAG_TARGET || 'extension-oss'; const babelOptions = { @@ -113,6 +115,7 @@ module.exports = { __IS_FIREFOX__: IS_FIREFOX, __IS_EDGE__: IS_EDGE, __IS_NATIVE__: false, + __IS_INTERNAL_MCP_BUILD__: IS_INTERNAL_MCP_BUILD, __IS_INTERNAL_VERSION__: IS_INTERNAL_VERSION, 'process.env.DEVTOOLS_PACKAGE': `"react-devtools-extensions"`, 'process.env.DEVTOOLS_VERSION': `"${DEVTOOLS_VERSION}"`, diff --git a/packages/react-devtools-fusebox/src/frontend.d.ts b/packages/react-devtools-fusebox/src/frontend.d.ts index 52536739ccaa1..a1142178f47a7 100644 --- a/packages/react-devtools-fusebox/src/frontend.d.ts +++ b/packages/react-devtools-fusebox/src/frontend.d.ts @@ -28,22 +28,32 @@ export type Config = { export function createBridge(wall: Wall): Bridge; export function createStore(bridge: Bridge, config?: Config): Store; -export type Source = { - sourceURL: string, - line: number, - column: number, -}; +export type ReactFunctionLocation = [ + string, // function name + string, // file name TODO: model nested eval locations as nested arrays + number, // enclosing line number + number, // enclosing column number +]; +export type ReactCallSite = [ + string, // function name + string, // file name TODO: model nested eval locations as nested arrays + number, // line number + number, // column number + number, // enclosing line number + number, // enclosing column number + boolean, // async resume +]; export type ViewElementSource = ( - source: Source, - symbolicatedSource: Source | null, + source: ReactFunctionLocation | ReactCallSite, + symbolicatedSource: ReactFunctionLocation | ReactCallSite | null, ) => void; export type ViewAttributeSource = ( id: number, path: Array, ) => void; export type CanViewElementSource = ( - source: Source, - symbolicatedSource: Source | null, + source: ReactFunctionLocation | ReactCallSite, + symbolicatedSource: ReactFunctionLocation | ReactCallSite | null, ) => boolean; export type InitializationOptions = { diff --git a/packages/react-devtools-fusebox/webpack.config.frontend.js b/packages/react-devtools-fusebox/webpack.config.frontend.js index ab7906ca84d63..ea04f4dad2d0d 100644 --- a/packages/react-devtools-fusebox/webpack.config.frontend.js +++ b/packages/react-devtools-fusebox/webpack.config.frontend.js @@ -86,6 +86,7 @@ module.exports = { __IS_CHROME__: false, __IS_FIREFOX__: false, __IS_EDGE__: false, + __IS_INTERNAL_MCP_BUILD__: false, 'process.env.DEVTOOLS_PACKAGE': `"react-devtools-fusebox"`, 'process.env.DEVTOOLS_VERSION': `"${DEVTOOLS_VERSION}"`, 'process.env.EDITOR_URL': EDITOR_URL != null ? `"${EDITOR_URL}"` : null, diff --git a/packages/react-devtools-inline/__tests__/__e2e__/components.test.js b/packages/react-devtools-inline/__tests__/__e2e__/components.test.js index 9a6b67d8ec65b..ae451d29587b0 100644 --- a/packages/react-devtools-inline/__tests__/__e2e__/components.test.js +++ b/packages/react-devtools-inline/__tests__/__e2e__/components.test.js @@ -52,7 +52,7 @@ test.describe('Components', () => { test('Should allow elements to be inspected', async () => { // Select the first list item in DevTools. - await devToolsUtils.selectElement(page, 'ListItem', 'List\nApp'); + await devToolsUtils.selectElement(page, 'ListItem', '\n'); // Prop names/values may not be editable based on the React version. // If they're not editable, make sure they degrade gracefully @@ -119,7 +119,7 @@ test.describe('Components', () => { runOnlyForReactRange('>=16.8'); // Select the first list item in DevTools. - await devToolsUtils.selectElement(page, 'ListItem', 'List\nApp', true); + await devToolsUtils.selectElement(page, 'ListItem', '\n', true); // Then read the inspected values. const sourceText = await page.evaluate(() => { @@ -142,7 +142,7 @@ test.describe('Components', () => { runOnlyForReactRange('>=16.8'); // Select the first list item in DevTools. - await devToolsUtils.selectElement(page, 'ListItem', 'List\nApp'); + await devToolsUtils.selectElement(page, 'ListItem', '\n'); // Then edit the label prop. await page.evaluate(() => { @@ -177,7 +177,7 @@ test.describe('Components', () => { runOnlyForReactRange('>=16.8'); // Select the List component DevTools. - await devToolsUtils.selectElement(page, 'List', 'App'); + await devToolsUtils.selectElement(page, 'List', ''); // Then click to load and parse hook names. await devToolsUtils.clickButton(page, 'LoadHookNamesButton'); diff --git a/packages/react-devtools-inline/__tests__/__e2e__/devtools-utils.js b/packages/react-devtools-inline/__tests__/__e2e__/devtools-utils.js index fe2bb3f6f222e..c39f63dc5bb4c 100644 --- a/packages/react-devtools-inline/__tests__/__e2e__/devtools-utils.js +++ b/packages/react-devtools-inline/__tests__/__e2e__/devtools-utils.js @@ -64,11 +64,22 @@ async function selectElement( createTestNameSelector('InspectedElementView-Owners'), ])[0]; + if (!ownersList) { + return false; + } + + const owners = findAllNodes(ownersList, [ + createTestNameSelector('OwnerView'), + ]); + return ( title && title.innerText.includes(titleText) && - ownersList && - ownersList.innerText.includes(ownersListText) + owners && + owners + .map(node => node.innerText) + .join('\n') + .includes(ownersListText) ); }, {titleText: displayName, ownersListText: waitForOwnersText} diff --git a/packages/react-devtools-inline/package.json b/packages/react-devtools-inline/package.json index bfa564b73bb91..8d3f1e71c10ff 100644 --- a/packages/react-devtools-inline/package.json +++ b/packages/react-devtools-inline/package.json @@ -1,6 +1,6 @@ { "name": "react-devtools-inline", - "version": "6.1.2", + "version": "6.1.5", "description": "Embed react-devtools within a website", "license": "MIT", "main": "./dist/backend.js", diff --git a/packages/react-devtools-inline/src/frontend.js b/packages/react-devtools-inline/src/frontend.js index 056d5d2c27052..b8d1b2ef4d811 100644 --- a/packages/react-devtools-inline/src/frontend.js +++ b/packages/react-devtools-inline/src/frontend.js @@ -52,7 +52,7 @@ export function initialize( bridge?: FrontendBridge, store?: Store, } = {}, -): React.ComponentType { +): component(...props: Props) { if (bridge == null) { bridge = createBridge(contentWindow); } diff --git a/packages/react-devtools-inline/webpack.config.js b/packages/react-devtools-inline/webpack.config.js index 3a92dff1f2195..9fa900dfa65f2 100644 --- a/packages/react-devtools-inline/webpack.config.js +++ b/packages/react-devtools-inline/webpack.config.js @@ -78,6 +78,7 @@ module.exports = { __IS_FIREFOX__: false, __IS_EDGE__: false, __IS_NATIVE__: false, + __IS_INTERNAL_MCP_BUILD__: 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/babel.config.js b/packages/react-devtools-shared/babel.config.js index 78af34817e0a9..dfd46013e944e 100644 --- a/packages/react-devtools-shared/babel.config.js +++ b/packages/react-devtools-shared/babel.config.js @@ -34,6 +34,7 @@ module.exports = api => { } } const plugins = [ + ['babel-plugin-syntax-hermes-parser'], ['@babel/plugin-transform-flow-strip-types'], ['@babel/plugin-proposal-class-properties', {loose: false}], ]; diff --git a/packages/react-devtools-shared/package.json b/packages/react-devtools-shared/package.json index 94f3f9411b0db..a8daa42a0d0c2 100644 --- a/packages/react-devtools-shared/package.json +++ b/packages/react-devtools-shared/package.json @@ -20,7 +20,7 @@ "clipboard-js": "^0.3.6", "compare-versions": "^5.0.3", "jsc-safe-url": "^0.2.4", - "json5": "^2.1.3", + "json5": "^2.2.3", "local-storage-fallback": "^4.1.1", "react-virtualized-auto-sizer": "^1.0.23", "react-window": "^1.8.10" diff --git a/packages/react-devtools-shared/src/Logger.js b/packages/react-devtools-shared/src/Logger.js index d37a33cf1c7eb..dd9dfb6202544 100644 --- a/packages/react-devtools-shared/src/Logger.js +++ b/packages/react-devtools-shared/src/Logger.js @@ -25,6 +25,9 @@ export type LoggerEvent = | { +event_name: 'selected-profiler-tab', } + | { + +event_name: 'selected-suspense-tab', + } | { +event_name: 'load-hook-names', +event_status: 'success' | 'error' | 'timeout' | 'unknown', diff --git a/packages/react-devtools-shared/src/__tests__/__serializers__/inspectedElementSerializer.js b/packages/react-devtools-shared/src/__tests__/__serializers__/inspectedElementSerializer.js index 870ad35e4b03c..55029252843c8 100644 --- a/packages/react-devtools-shared/src/__tests__/__serializers__/inspectedElementSerializer.js +++ b/packages/react-devtools-shared/src/__tests__/__serializers__/inspectedElementSerializer.js @@ -15,8 +15,7 @@ export function test(maybeInspectedElement) { hasOwnProperty('canEditFunctionProps') && hasOwnProperty('canEditHooks') && hasOwnProperty('canToggleSuspense') && - hasOwnProperty('canToggleError') && - hasOwnProperty('canViewSource') + hasOwnProperty('canToggleError') ); } diff --git a/packages/react-devtools-shared/src/__tests__/inspectedElement-test.js b/packages/react-devtools-shared/src/__tests__/inspectedElement-test.js index 522d211aeb06f..09f811172f30d 100644 --- a/packages/react-devtools-shared/src/__tests__/inspectedElement-test.js +++ b/packages/react-devtools-shared/src/__tests__/inspectedElement-test.js @@ -682,6 +682,7 @@ describe('InspectedElement', () => { object_with_symbol={objectWithSymbol} proxy={proxyInstance} react_element={} + react_lazy={React.lazy(async () => ({default: 'foo'}))} regexp={/abc/giu} set={setShallow} set_of_sets={setOfSets} @@ -780,9 +781,18 @@ describe('InspectedElement', () => { "preview_short": () => {}, "preview_long": () => {}, }, - "react_element": Dehydrated { - "preview_short": , - "preview_long": , + "react_element": { + "key": null, + "props": Dehydrated { + "preview_short": {…}, + "preview_long": {}, + }, + }, + "react_lazy": { + "_payload": Dehydrated { + "preview_short": {…}, + "preview_long": {_ioInfo: {…}, _result: () => {}, _status: -1}, + }, }, "regexp": Dehydrated { "preview_short": /abc/giu, @@ -930,13 +940,13 @@ describe('InspectedElement', () => { const inspectedElement = await inspectElementAtIndex(0); expect(inspectedElement.props).toMatchInlineSnapshot(` - { - "unusedPromise": Dehydrated { - "preview_short": Promise, - "preview_long": Promise, - }, - } - `); + { + "unusedPromise": Dehydrated { + "preview_short": Promise, + "preview_long": Promise, + }, + } + `); }); it('should not consume iterables while inspecting', async () => { diff --git a/packages/react-devtools-shared/src/__tests__/legacy/inspectElement-test.js b/packages/react-devtools-shared/src/__tests__/legacy/inspectElement-test.js index cf1ce1ffa3e38..f306ab97093d9 100644 --- a/packages/react-devtools-shared/src/__tests__/legacy/inspectElement-test.js +++ b/packages/react-devtools-shared/src/__tests__/legacy/inspectElement-test.js @@ -289,9 +289,13 @@ describe('InspectedElementContext', () => { "preview_long": {boolean: true, number: 123, string: "abc"}, }, }, - "react_element": Dehydrated { - "preview_short": , - "preview_long": , + "react_element": { + "key": null, + "props": Dehydrated { + "preview_short": {…}, + "preview_long": {}, + }, + "ref": null, }, "regexp": Dehydrated { "preview_short": /abc/giu, diff --git a/packages/react-devtools-shared/src/__tests__/preprocessData-test.js b/packages/react-devtools-shared/src/__tests__/preprocessData-test.js index 7a77ab20eb9cf..1de972658c232 100644 --- a/packages/react-devtools-shared/src/__tests__/preprocessData-test.js +++ b/packages/react-devtools-shared/src/__tests__/preprocessData-test.js @@ -857,7 +857,7 @@ describe('Timeline profiler', () => { { "batchUID": 0, "depth": 0, - "duration": 0.014, + "duration": 0.012, "lanes": "0b0000000000000000000000000000101", "timestamp": 0.008, "type": "render-idle", @@ -873,25 +873,17 @@ describe('Timeline profiler', () => { { "batchUID": 0, "depth": 0, - "duration": 0.010, + "duration": 0.008, "lanes": "0b0000000000000000000000000000101", "timestamp": 0.012, "type": "commit", }, - { - "batchUID": 0, - "depth": 1, - "duration": 0.001, - "lanes": "0b0000000000000000000000000000101", - "timestamp": 0.02, - "type": "layout-effects", - }, { "batchUID": 0, "depth": 0, "duration": 0.004, "lanes": "0b0000000000000000000000000000101", - "timestamp": 0.023, + "timestamp": 0.021, "type": "passive-effects", }, ], @@ -899,9 +891,9 @@ describe('Timeline profiler', () => { { "batchUID": 1, "depth": 0, - "duration": 0.014, + "duration": 0.012, "lanes": "0b0000000000000000000000000000101", - "timestamp": 0.028, + "timestamp": 0.026, "type": "render-idle", }, { @@ -909,31 +901,23 @@ describe('Timeline profiler', () => { "depth": 0, "duration": 0.003, "lanes": "0b0000000000000000000000000000101", - "timestamp": 0.028, + "timestamp": 0.026, "type": "render", }, { "batchUID": 1, "depth": 0, - "duration": 0.010, + "duration": 0.008, "lanes": "0b0000000000000000000000000000101", - "timestamp": 0.032, + "timestamp": 0.03, "type": "commit", }, - { - "batchUID": 1, - "depth": 1, - "duration": 0.001, - "lanes": "0b0000000000000000000000000000101", - "timestamp": 0.04, - "type": "layout-effects", - }, { "batchUID": 1, "depth": 0, "duration": 0.003, "lanes": "0b0000000000000000000000000000101", - "timestamp": 0.043, + "timestamp": 0.039, "type": "passive-effects", }, ], @@ -949,26 +933,26 @@ describe('Timeline profiler', () => { { "componentName": "App", "duration": 0.002, - "timestamp": 0.024, + "timestamp": 0.022, "type": "passive-effect-mount", "warning": null, }, { "componentName": "App", "duration": 0.001, - "timestamp": 0.029, + "timestamp": 0.027, "type": "render", "warning": null, }, { "componentName": "App", "duration": 0.001, - "timestamp": 0.044, + "timestamp": 0.04, "type": "passive-effect-mount", "warning": null, }, ], - "duration": 0.046, + "duration": 0.042, "flamechart": [], "internalModuleSourceToRanges": Map { undefined => [ @@ -1031,7 +1015,7 @@ describe('Timeline profiler', () => { { "batchUID": 0, "depth": 0, - "duration": 0.014, + "duration": 0.012, "lanes": "0b0000000000000000000000000000101", "timestamp": 0.008, "type": "render-idle", @@ -1047,33 +1031,25 @@ describe('Timeline profiler', () => { { "batchUID": 0, "depth": 0, - "duration": 0.010, + "duration": 0.008, "lanes": "0b0000000000000000000000000000101", "timestamp": 0.012, "type": "commit", }, - { - "batchUID": 0, - "depth": 1, - "duration": 0.001, - "lanes": "0b0000000000000000000000000000101", - "timestamp": 0.02, - "type": "layout-effects", - }, { "batchUID": 0, "depth": 0, "duration": 0.004, "lanes": "0b0000000000000000000000000000101", - "timestamp": 0.023, + "timestamp": 0.021, "type": "passive-effects", }, { "batchUID": 1, "depth": 0, - "duration": 0.014, + "duration": 0.012, "lanes": "0b0000000000000000000000000000101", - "timestamp": 0.028, + "timestamp": 0.026, "type": "render-idle", }, { @@ -1081,31 +1057,23 @@ describe('Timeline profiler', () => { "depth": 0, "duration": 0.003, "lanes": "0b0000000000000000000000000000101", - "timestamp": 0.028, + "timestamp": 0.026, "type": "render", }, { "batchUID": 1, "depth": 0, - "duration": 0.010, + "duration": 0.008, "lanes": "0b0000000000000000000000000000101", - "timestamp": 0.032, + "timestamp": 0.03, "type": "commit", }, - { - "batchUID": 1, - "depth": 1, - "duration": 0.001, - "lanes": "0b0000000000000000000000000000101", - "timestamp": 0.04, - "type": "layout-effects", - }, { "batchUID": 1, "depth": 0, "duration": 0.003, "lanes": "0b0000000000000000000000000000101", - "timestamp": 0.043, + "timestamp": 0.039, "type": "passive-effects", }, ], @@ -1149,7 +1117,7 @@ describe('Timeline profiler', () => { { "componentName": "App", "lanes": "0b0000000000000000000000000000101", - "timestamp": 0.025, + "timestamp": 0.023, "type": "schedule-state-update", "warning": null, }, @@ -1254,6 +1222,15 @@ describe('Timeline profiler', () => { let promise = null; let resolvedValue = null; function readValue(value) { + if (React.use) { + if (promise === null) { + promise = Promise.resolve(true).then(() => { + return value; + }); + promise.displayName = 'Testing displayName'; + } + return React.use(promise); + } if (resolvedValue !== null) { return resolvedValue; } else if (promise === null) { @@ -1273,7 +1250,7 @@ describe('Timeline profiler', () => { const testMarks = [creactCpuProfilerSample()]; const root = ReactDOMClient.createRoot(document.createElement('div')); - utils.act(() => + await utils.actAsync(() => root.render( @@ -1823,6 +1800,14 @@ describe('Timeline profiler', () => { let promise = null; let resolvedValue = null; function readValue(value) { + if (React.use) { + if (promise === null) { + promise = Promise.resolve(true).then(() => { + return value; + }); + } + return React.use(promise); + } if (resolvedValue !== null) { return resolvedValue; } else if (promise === null) { @@ -1881,6 +1866,14 @@ describe('Timeline profiler', () => { let promise = null; let resolvedValue = null; function readValue(value) { + if (React.use) { + if (promise === null) { + promise = Promise.resolve(true).then(() => { + return value; + }); + } + return React.use(promise); + } if (resolvedValue !== null) { return resolvedValue; } else if (promise === null) { @@ -2192,14 +2185,6 @@ describe('Timeline profiler', () => { "timestamp": 10, "type": "commit", }, - { - "batchUID": 1, - "depth": 1, - "duration": 0, - "lanes": "0b0000000000000000000000000100000", - "timestamp": 10, - "type": "layout-effects", - }, { "batchUID": 1, "depth": 0, @@ -2234,14 +2219,6 @@ describe('Timeline profiler', () => { "timestamp": 10, "type": "commit", }, - { - "batchUID": 2, - "depth": 1, - "duration": 0, - "lanes": "0b0000000000000000000000000100000", - "timestamp": 10, - "type": "layout-effects", - }, { "batchUID": 2, "depth": 0, @@ -2292,8 +2269,8 @@ describe('Timeline profiler', () => { 8 => "InputContinuous", 16 => "DefaultHydration", 32 => "Default", - 64 => "TransitionHydration", - 128 => "Transition", + 64 => undefined, + 128 => "TransitionHydration", 256 => "Transition", 512 => "Transition", 1024 => "Transition", @@ -2349,14 +2326,6 @@ describe('Timeline profiler', () => { "timestamp": 10, "type": "commit", }, - { - "batchUID": 1, - "depth": 1, - "duration": 0, - "lanes": "0b0000000000000000000000000100000", - "timestamp": 10, - "type": "layout-effects", - }, { "batchUID": 1, "depth": 0, @@ -2389,14 +2358,6 @@ describe('Timeline profiler', () => { "timestamp": 10, "type": "commit", }, - { - "batchUID": 2, - "depth": 1, - "duration": 0, - "lanes": "0b0000000000000000000000000100000", - "timestamp": 10, - "type": "layout-effects", - }, { "batchUID": 2, "depth": 0, diff --git a/packages/react-devtools-shared/src/__tests__/profilerStore-test.js b/packages/react-devtools-shared/src/__tests__/profilerStore-test.js index cead054d3183c..c8384b2fa42e8 100644 --- a/packages/react-devtools-shared/src/__tests__/profilerStore-test.js +++ b/packages/react-devtools-shared/src/__tests__/profilerStore-test.js @@ -215,7 +215,11 @@ describe('ProfilerStore', () => { it('should not throw while initializing context values for Fibers within a not-yet-mounted subtree', () => { const promise = new Promise(resolve => {}); const SuspendingView = () => { - throw promise; + if (React.use) { + React.use(promise); + } else { + throw promise; + } }; const App = () => { diff --git a/packages/react-devtools-shared/src/__tests__/profilingCache-test.js b/packages/react-devtools-shared/src/__tests__/profilingCache-test.js index fb1fa6c5f2298..d16062c69f488 100644 --- a/packages/react-devtools-shared/src/__tests__/profilingCache-test.js +++ b/packages/react-devtools-shared/src/__tests__/profilingCache-test.js @@ -682,6 +682,14 @@ describe('ProfilingCache', () => { it('should calculate durations correctly for suspended views', async () => { let data; const getData = () => { + if (React.use) { + if (!data) { + data = new Promise(resolve => { + resolve('abc'); + }); + } + return React.use(data); + } if (data) { return data; } else { @@ -854,6 +862,7 @@ describe('ProfilingCache', () => { { "compiledWithForget": false, "displayName": "render()", + "env": null, "hocDisplayNames": null, "id": 1, "key": null, @@ -895,6 +904,7 @@ describe('ProfilingCache', () => { { "compiledWithForget": false, "displayName": "createRoot()", + "env": null, "hocDisplayNames": null, "id": 1, "key": null, @@ -935,9 +945,11 @@ describe('ProfilingCache', () => { { "compiledWithForget": false, "displayName": "createRoot()", + "env": null, "hocDisplayNames": null, "id": 1, "key": null, + "stack": null, "type": 11, }, ], diff --git a/packages/react-devtools-shared/src/__tests__/profilingCommitTreeBuilder-test.js b/packages/react-devtools-shared/src/__tests__/profilingCommitTreeBuilder-test.js index f5b7e5fded401..a7c0893060b00 100644 --- a/packages/react-devtools-shared/src/__tests__/profilingCommitTreeBuilder-test.js +++ b/packages/react-devtools-shared/src/__tests__/profilingCommitTreeBuilder-test.js @@ -228,6 +228,8 @@ describe('commit tree', () => { [root] ▾ + [shell] + `); utils.act(() => modernRender()); expect(store).toMatchInlineSnapshot(` @@ -235,6 +237,8 @@ describe('commit tree', () => { ▾ + [shell] + `); utils.act(() => modernRender()); expect(store).toMatchInlineSnapshot(` @@ -299,6 +303,8 @@ describe('commit tree', () => { [root] ▾ + [shell] + `); utils.act(() => modernRender()); expect(store).toMatchInlineSnapshot(` diff --git a/packages/react-devtools-shared/src/__tests__/setupTests.js b/packages/react-devtools-shared/src/__tests__/setupTests.js index 50431b230ad87..610191fb2cdb8 100644 --- a/packages/react-devtools-shared/src/__tests__/setupTests.js +++ b/packages/react-devtools-shared/src/__tests__/setupTests.js @@ -141,7 +141,7 @@ function patchConsoleForTestingBeforeHookInstallation() { // if they use this code path. firstArg = firstArg.slice(9); } - if (firstArg === 'React instrumentation encountered an error: %s') { + if (firstArg === 'React instrumentation encountered an error: %o') { // Rethrow errors from React. throw args[1]; } else if ( diff --git a/packages/react-devtools-shared/src/__tests__/store-test.js b/packages/react-devtools-shared/src/__tests__/store-test.js index 93c7048b2bc5b..d4c3fcd9cad4b 100644 --- a/packages/react-devtools-shared/src/__tests__/store-test.js +++ b/packages/react-devtools-shared/src/__tests__/store-test.js @@ -17,11 +17,23 @@ describe('Store', () => { let act; let actAsync; let bridge; + let createDisplayNameFilter; let getRendererID; let legacyRender; + let previousComponentFilters; let store; let withErrorsOrWarningsIgnored; + beforeAll(() => { + // JSDDOM doesn't implement getClientRects so we're just faking one for testing purposes + Element.prototype.getClientRects = function (this: Element) { + const textContent = this.textContent; + return [ + new DOMRect(1, 2, textContent.length, textContent.split('\n').length), + ]; + }; + }); + beforeEach(() => { global.IS_REACT_ACT_ENVIRONMENT = true; @@ -29,6 +41,8 @@ describe('Store', () => { bridge = global.bridge; store = global.store; + previousComponentFilters = store.componentFilters; + React = require('react'); ReactDOM = require('react-dom'); ReactDOMClient = require('react-dom/client'); @@ -38,16 +52,21 @@ describe('Store', () => { actAsync = utils.actAsync; getRendererID = utils.getRendererID; legacyRender = utils.legacyRender; + createDisplayNameFilter = utils.createDisplayNameFilter; withErrorsOrWarningsIgnored = utils.withErrorsOrWarningsIgnored; }); + afterEach(() => { + store.componentFilters = previousComponentFilters; + }); + const {render, unmount, createContainer} = getVersionedRenderImplementation(); // @reactVersion >= 18.0 - it('should not allow a root node to be collapsed', () => { + it('should not allow a root node to be collapsed', async () => { const Component = () =>
    Hi
    ; - act(() => render()); + await act(() => render()); expect(store).toMatchInlineSnapshot(` [root] @@ -63,16 +82,16 @@ describe('Store', () => { }); // @reactVersion >= 18.0 - it('should properly handle a root with no visible nodes', () => { + it('should properly handle a root with no visible nodes', async () => { const Root = ({children}) => children; - act(() => render({null})); + await act(() => render({null})); expect(store).toMatchInlineSnapshot(` [root] `); - act(() => render(
    )); + await act(() => render(
    )); expect(store).toMatchInlineSnapshot(`[root]`); }); @@ -82,20 +101,24 @@ describe('Store', () => { // I'mnot yet sure of how to reduce the GitHub reported production case to a test though. // See https://github.com/facebook/react/issues/21445 // @reactVersion >= 18.0 - it('should handle when a component mounts before its owner', () => { + it('should handle when a component mounts before its owner', async () => { const promise = new Promise(resolve => {}); let Dynamic = null; const Owner = () => { Dynamic = ; - throw promise; + if (React.use) { + React.use(promise); + } else { + throw promise; + } }; const Parent = () => { return Dynamic; }; const Child = () => null; - act(() => + await act(() => render( <> @@ -110,23 +133,91 @@ describe('Store', () => { + [shell] + `); }); // @reactVersion >= 18.0 - it('should handle multibyte character strings', () => { + it('should handle multibyte character strings', async () => { const Component = () => null; Component.displayName = '🟩💜🔵'; - act(() => render()); + await act(() => render()); expect(store).toMatchInlineSnapshot(` [root] <🟩💜🔵> `); }); + it('should handle reorder of filtered elements', async () => { + function IgnoreMePassthrough({children}) { + return children; + } + function PassThrough({children}) { + return children; + } + + await actAsync( + async () => + (store.componentFilters = [createDisplayNameFilter('^IgnoreMe', true)]), + ); + + await act(() => { + render( + + + +

    e1

    +
    +
    + + +
    e2
    +
    +
    +
    , + ); + }); + + expect(store).toMatchInlineSnapshot(` + [root] + ▾ + ▾ +

    + ▾ +

    + `); + + await act(() => { + render( + + + +
    e2
    +
    +
    + + +

    e1

    +
    +
    +
    , + ); + }); + + expect(store).toMatchInlineSnapshot(` + [root] + ▾ + ▾ +
    + ▾ +

    + `); + }); + describe('StrictMode compliance', () => { - it('should mark strict root elements as strict', () => { + it('should mark strict root elements as strict', async () => { const App = () => ; const Component = () => null; @@ -134,7 +225,7 @@ describe('Store', () => { const root = ReactDOMClient.createRoot(container, { unstable_strictMode: true, }); - act(() => { + await act(() => { root.render(); }); @@ -143,13 +234,13 @@ describe('Store', () => { }); // @reactVersion >= 18.0 - it('should mark non strict root elements as not strict', () => { + it('should mark non strict root elements as not strict', async () => { const App = () => ; const Component = () => null; const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); - act(() => { + await act(() => { root.render(); }); @@ -157,7 +248,7 @@ describe('Store', () => { expect(store.getElementAtIndex(1).isStrictModeNonCompliant).toBe(true); }); - it('should mark StrictMode subtree elements as strict', () => { + it('should mark StrictMode subtree elements as strict', async () => { const App = () => ( @@ -167,7 +258,7 @@ describe('Store', () => { const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); - act(() => { + await act(() => { root.render(); }); @@ -182,7 +273,7 @@ describe('Store', () => { }); // @reactVersion >= 18.0 - it('should support mount and update operations', () => { + it('should support mount and update operations', async () => { const Grandparent = ({count}) => ( @@ -193,7 +284,7 @@ describe('Store', () => { new Array(count).fill(true).map((_, index) => ); const Child = () =>

    Hi!
    ; - act(() => render()); + await act(() => render()); expect(store).toMatchInlineSnapshot(` [root] ▾ @@ -209,7 +300,7 @@ describe('Store', () => { `); - act(() => render()); + await act(() => render()); expect(store).toMatchInlineSnapshot(` [root] ▾ @@ -221,14 +312,14 @@ describe('Store', () => { `); - act(() => unmount()); + await act(() => unmount()); expect(store).toMatchInlineSnapshot(``); }); // @reactVersion >= 18.0 // @reactVersion < 19 // @gate !disableLegacyMode - it('should support mount and update operations for multiple roots (legacy render)', () => { + it('should support mount and update operations for multiple roots (legacy render)', async () => { const Parent = ({count}) => new Array(count).fill(true).map((_, index) => ); const Child = () =>
    Hi!
    ; @@ -236,7 +327,7 @@ describe('Store', () => { const containerA = document.createElement('div'); const containerB = document.createElement('div'); - act(() => { + await act(() => { legacyRender(, containerA); legacyRender(, containerB); }); @@ -252,7 +343,7 @@ describe('Store', () => { `); - act(() => { + await act(() => { legacyRender(, containerA); legacyRender(, containerB); }); @@ -268,7 +359,7 @@ describe('Store', () => { `); - act(() => ReactDOM.unmountComponentAtNode(containerB)); + await act(() => ReactDOM.unmountComponentAtNode(containerB)); expect(store).toMatchInlineSnapshot(` [root] ▾ @@ -278,12 +369,12 @@ describe('Store', () => { `); - act(() => ReactDOM.unmountComponentAtNode(containerA)); + await act(() => ReactDOM.unmountComponentAtNode(containerA)); expect(store).toMatchInlineSnapshot(``); }); // @reactVersion >= 18.0 - it('should support mount and update operations for multiple roots (createRoot)', () => { + it('should support mount and update operations for multiple roots (createRoot)', async () => { const Parent = ({count}) => new Array(count).fill(true).map((_, index) => ); const Child = () =>
    Hi!
    ; @@ -294,7 +385,7 @@ describe('Store', () => { const rootA = ReactDOMClient.createRoot(containerA); const rootB = ReactDOMClient.createRoot(containerB); - act(() => { + await act(() => { rootA.render(); rootB.render(); }); @@ -310,7 +401,7 @@ describe('Store', () => { `); - act(() => { + await act(() => { rootA.render(); rootB.render(); }); @@ -326,7 +417,7 @@ describe('Store', () => { `); - act(() => rootB.unmount()); + await act(() => rootB.unmount()); expect(store).toMatchInlineSnapshot(` [root] ▾ @@ -336,12 +427,12 @@ describe('Store', () => { `); - act(() => rootA.unmount()); + await act(() => rootA.unmount()); expect(store).toMatchInlineSnapshot(``); }); // @reactVersion >= 18.0 - it('should filter DOM nodes from the store tree', () => { + it('should filter DOM nodes from the store tree', async () => { const Grandparent = () => (
    @@ -357,7 +448,7 @@ describe('Store', () => { ); const Child = () =>
    Hi!
    ; - act(() => render()); + await act(() => render()); expect(store).toMatchInlineSnapshot(` [root] ▾ @@ -369,10 +460,14 @@ describe('Store', () => { }); // @reactVersion >= 18.0 - it('should display Suspense nodes properly in various states', () => { + it('should display Suspense nodes properly in various states', async () => { const Loading = () =>
    Loading...
    ; const SuspendingComponent = () => { - throw new Promise(() => {}); + if (React.use) { + React.use(new Promise(() => {})); + } else { + throw new Promise(() => {}); + } }; const Component = () => { return
    Hello
    ; @@ -390,16 +485,18 @@ describe('Store', () => { ); - act(() => render()); + await act(() => render()); expect(store).toMatchInlineSnapshot(` [root] ▾ + [shell] + `); - act(() => { + await act(() => { render(); }); expect(store).toMatchInlineSnapshot(` @@ -408,15 +505,21 @@ describe('Store', () => { + [shell] + `); }); // @reactVersion >= 18.0 - it('should support nested Suspense nodes', () => { + it('should support nested Suspense nodes', async () => { const Component = () => null; const Loading = () =>
    Loading...
    ; const Never = () => { - throw new Promise(() => {}); + if (React.use) { + React.use(new Promise(() => {})); + } else { + throw new Promise(() => {}); + } }; const Wrapper = ({ @@ -426,23 +529,31 @@ describe('Store', () => { }) => ( - }> + }> - }> + }> {suspendFirst ? ( ) : ( )} - }> + }> {suspendSecond ? ( ) : ( )} - }> + }> {suspendParent && } @@ -451,7 +562,7 @@ describe('Store', () => { ); - act(() => + await actAsync(() => render( { [root] ▾ - ▾ + ▾ - ▾ + ▾ - ▾ + ▾ - ▾ + ▾ + [shell] + + + + `); - act(() => + await act(() => render( { [root] ▾ - ▾ + ▾ - ▾ + ▾ - ▾ + ▾ - ▾ + ▾ + [shell] + + + + `); - act(() => + await act(() => render( { [root] ▾ - ▾ + ▾ - ▾ + ▾ - ▾ + ▾ - ▾ + ▾ + [shell] + + + + `); - act(() => + await act(() => render( { [root] ▾ - ▾ + ▾ - ▾ + ▾ - ▾ + ▾ - ▾ + ▾ + [shell] + + + + `); - act(() => + await act(() => render( { [root] ▾ - ▾ + ▾ + [shell] + + + + `); - act(() => + await act(() => render( { [root] ▾ - ▾ + ▾ - ▾ + ▾ - ▾ + ▾ - ▾ + ▾ + [shell] + + + + `); - act(() => + await act(() => render( { [root] ▾ - ▾ + ▾ - ▾ + ▾ - ▾ + ▾ - ▾ + ▾ + [shell] + + + + `); const rendererID = getRendererID(); - act(() => + await act(() => agent.overrideSuspense({ id: store.getElementIDAtIndex(4), rendererID, @@ -618,17 +764,22 @@ describe('Store', () => { [root] ▾ - ▾ + ▾ - ▾ + ▾ - ▾ + ▾ - ▾ + ▾ + [shell] + + + + `); - act(() => + await act(() => agent.overrideSuspense({ id: store.getElementIDAtIndex(2), rendererID, @@ -639,10 +790,15 @@ describe('Store', () => { [root] ▾ - ▾ + ▾ + [shell] + + + + `); - act(() => + await act(() => render( { [root] ▾ - ▾ + ▾ + [shell] + + + + `); - act(() => + await act(() => agent.overrideSuspense({ id: store.getElementIDAtIndex(2), rendererID, @@ -669,17 +830,22 @@ describe('Store', () => { [root] ▾ - ▾ + ▾ - ▾ + ▾ - ▾ + ▾ - ▾ + ▾ + [shell] + + + + `); - act(() => + await act(() => agent.overrideSuspense({ id: store.getElementIDAtIndex(4), rendererID, @@ -690,17 +856,22 @@ describe('Store', () => { [root] ▾ - ▾ + ▾ - ▾ + ▾ - ▾ + ▾ - ▾ + ▾ + [shell] + + + + `); - act(() => + await act(() => render( { [root] ▾ - ▾ + ▾ - ▾ + ▾ - ▾ + ▾ - ▾ + ▾ + [shell] + + + + `); }); - it('should display a partially rendered SuspenseList', () => { + // @reactVersion >= 18.0 + it('can override multiple Suspense simultaneously', async () => { + const Component = () => { + return
    Hello
    ; + }; + const App = () => ( + + + }> + + }> + + + }> + + + }> + + + + + + ); + + await actAsync(() => render()); + + expect(store).toMatchInlineSnapshot(` + [root] + ▾ + + ▾ + + ▾ + + ▾ + + ▾ + + + [shell] + + + + + `); + + const rendererID = getRendererID(); + const rootID = store.getRootIDForElement(store.getElementIDAtIndex(0)); + await actAsync(() => { + agent.overrideSuspenseMilestone({ + rendererID, + rootID, + suspendedSet: [ + store.getElementIDAtIndex(4), + store.getElementIDAtIndex(8), + ], + }); + }); + + expect(store).toMatchInlineSnapshot(` + [root] + ▾ + + ▾ + + ▾ + + ▾ + + ▾ + + + [shell] + + + + + `); + + await actAsync(() => { + agent.overrideSuspenseMilestone({ + rendererID, + rootID, + suspendedSet: [], + }); + }); + + expect(store).toMatchInlineSnapshot(` + [root] + ▾ + + ▾ + + ▾ + + ▾ + + ▾ + + + [shell] + + + + + `); + }); + + it('should display a partially rendered SuspenseList', async () => { const Loading = () =>
    Loading...
    ; const SuspendingComponent = () => { - throw new Promise(() => {}); + if (React.use) { + React.use(new Promise(() => {})); + } else { + throw new Promise(() => {}); + } }; const Component = () => { return
    Hello
    ; @@ -747,7 +1043,7 @@ describe('Store', () => { const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); - act(() => { + await act(() => { root.render(); }); expect(store).toMatchInlineSnapshot(` @@ -757,9 +1053,11 @@ describe('Store', () => { + [shell] + `); - act(() => { + await act(() => { root.render(); }); expect(store).toMatchInlineSnapshot(` @@ -770,11 +1068,13 @@ describe('Store', () => { ▾ + [shell] + `); }); // @reactVersion >= 18.0 - it('should support collapsing parts of the tree', () => { + it('should support collapsing parts of the tree', async () => { const Grandparent = ({count}) => ( @@ -785,7 +1085,7 @@ describe('Store', () => { new Array(count).fill(true).map((_, index) => ); const Child = () =>
    Hi!
    ; - act(() => render()); + await act(() => render()); expect(store).toMatchInlineSnapshot(` [root] ▾ @@ -801,7 +1101,7 @@ describe('Store', () => { const parentOneID = store.getElementIDAtIndex(1); const parentTwoID = store.getElementIDAtIndex(4); - act(() => store.toggleIsCollapsed(parentOneID, true)); + await act(() => store.toggleIsCollapsed(parentOneID, true)); expect(store).toMatchInlineSnapshot(` [root] ▾ @@ -811,7 +1111,7 @@ describe('Store', () => { `); - act(() => store.toggleIsCollapsed(parentTwoID, true)); + await act(() => store.toggleIsCollapsed(parentTwoID, true)); expect(store).toMatchInlineSnapshot(` [root] ▾ @@ -819,7 +1119,7 @@ describe('Store', () => { ▸ `); - act(() => store.toggleIsCollapsed(parentOneID, false)); + await act(() => store.toggleIsCollapsed(parentOneID, false)); expect(store).toMatchInlineSnapshot(` [root] ▾ @@ -829,13 +1129,13 @@ describe('Store', () => { ▸ `); - act(() => store.toggleIsCollapsed(grandparentID, true)); + await act(() => store.toggleIsCollapsed(grandparentID, true)); expect(store).toMatchInlineSnapshot(` [root] ▸ `); - act(() => store.toggleIsCollapsed(grandparentID, false)); + await act(() => store.toggleIsCollapsed(grandparentID, false)); expect(store).toMatchInlineSnapshot(` [root] ▾ @@ -847,7 +1147,7 @@ describe('Store', () => { }); // @reactVersion >= 18.0 - it('should support reordering of children', () => { + it('should support reordering of children', async () => { const Root = ({children}) => children; const Component = () => null; @@ -856,7 +1156,7 @@ describe('Store', () => { const foo = ; const bar = ; - act(() => render({[foo, bar]})); + await act(() => render({[foo, bar]})); expect(store).toMatchInlineSnapshot(` [root] ▾ @@ -867,7 +1167,7 @@ describe('Store', () => { `); - act(() => render({[bar, foo]})); + await act(() => render({[bar, foo]})); expect(store).toMatchInlineSnapshot(` [root] ▾ @@ -878,13 +1178,17 @@ describe('Store', () => { `); - act(() => store.toggleIsCollapsed(store.getElementIDAtIndex(0), true)); + await act(() => + store.toggleIsCollapsed(store.getElementIDAtIndex(0), true), + ); expect(store).toMatchInlineSnapshot(` [root] ▸ `); - act(() => store.toggleIsCollapsed(store.getElementIDAtIndex(0), false)); + await act(() => + store.toggleIsCollapsed(store.getElementIDAtIndex(0), false), + ); expect(store).toMatchInlineSnapshot(` [root] ▾ @@ -903,12 +1207,12 @@ describe('Store', () => { }); // @reactVersion >= 18.0 - it('should support mount and update operations', () => { + it('should support mount and update operations', async () => { const Parent = ({count}) => new Array(count).fill(true).map((_, index) => ); const Child = () =>
    Hi!
    ; - act(() => + await act(() => render( @@ -922,7 +1226,7 @@ describe('Store', () => { ▸ `); - act(() => + await act(() => render( @@ -936,14 +1240,14 @@ describe('Store', () => { ▸ `); - act(() => unmount()); + await act(() => unmount()); expect(store).toMatchInlineSnapshot(``); }); // @reactVersion >= 18.0 // @reactVersion < 19 // @gate !disableLegacyMode - it('should support mount and update operations for multiple roots (legacy render)', () => { + it('should support mount and update operations for multiple roots (legacy render)', async () => { const Parent = ({count}) => new Array(count).fill(true).map((_, index) => ); const Child = () =>
    Hi!
    ; @@ -951,7 +1255,7 @@ describe('Store', () => { const containerA = document.createElement('div'); const containerB = document.createElement('div'); - act(() => { + await act(() => { legacyRender(, containerA); legacyRender(, containerB); }); @@ -962,7 +1266,7 @@ describe('Store', () => { ▸ `); - act(() => { + await act(() => { legacyRender(, containerA); legacyRender(, containerB); }); @@ -973,18 +1277,18 @@ describe('Store', () => { ▸ `); - act(() => ReactDOM.unmountComponentAtNode(containerB)); + await act(() => ReactDOM.unmountComponentAtNode(containerB)); expect(store).toMatchInlineSnapshot(` [root] ▸ `); - act(() => ReactDOM.unmountComponentAtNode(containerA)); + await act(() => ReactDOM.unmountComponentAtNode(containerA)); expect(store).toMatchInlineSnapshot(``); }); // @reactVersion >= 18.0 - it('should support mount and update operations for multiple roots (createRoot)', () => { + it('should support mount and update operations for multiple roots (createRoot)', async () => { const Parent = ({count}) => new Array(count).fill(true).map((_, index) => ); const Child = () =>
    Hi!
    ; @@ -995,7 +1299,7 @@ describe('Store', () => { const rootA = ReactDOMClient.createRoot(containerA); const rootB = ReactDOMClient.createRoot(containerB); - act(() => { + await act(() => { rootA.render(); rootB.render(); }); @@ -1006,7 +1310,7 @@ describe('Store', () => { ▸ `); - act(() => { + await act(() => { rootA.render(); rootB.render(); }); @@ -1017,18 +1321,18 @@ describe('Store', () => { ▸ `); - act(() => rootB.unmount()); + await act(() => rootB.unmount()); expect(store).toMatchInlineSnapshot(` [root] ▸ `); - act(() => rootA.unmount()); + await act(() => rootA.unmount()); expect(store).toMatchInlineSnapshot(``); }); // @reactVersion >= 18.0 - it('should filter DOM nodes from the store tree', () => { + it('should filter DOM nodes from the store tree', async () => { const Grandparent = () => (
    @@ -1044,13 +1348,15 @@ describe('Store', () => { ); const Child = () =>
    Hi!
    ; - act(() => render()); + await act(() => render()); expect(store).toMatchInlineSnapshot(` [root] ▸ `); - act(() => store.toggleIsCollapsed(store.getElementIDAtIndex(0), false)); + await act(() => + store.toggleIsCollapsed(store.getElementIDAtIndex(0), false), + ); expect(store).toMatchInlineSnapshot(` [root] ▾ @@ -1058,7 +1364,9 @@ describe('Store', () => { ▸ `); - act(() => store.toggleIsCollapsed(store.getElementIDAtIndex(1), false)); + await act(() => + store.toggleIsCollapsed(store.getElementIDAtIndex(1), false), + ); expect(store).toMatchInlineSnapshot(` [root] ▾ @@ -1069,10 +1377,14 @@ describe('Store', () => { }); // @reactVersion >= 18.0 - it('should display Suspense nodes properly in various states', () => { + it('should display Suspense nodes properly in various states', async () => { const Loading = () =>
    Loading...
    ; const SuspendingComponent = () => { - throw new Promise(() => {}); + if (React.use) { + React.use(new Promise(() => {})); + } else { + throw new Promise(() => {}); + } }; const Component = () => { return
    Hello
    ; @@ -1090,24 +1402,32 @@ describe('Store', () => { ); - act(() => render()); + await act(() => render()); expect(store).toMatchInlineSnapshot(` [root] ▸ + [shell] + `); // This test isn't meaningful unless we expand the suspended tree - act(() => store.toggleIsCollapsed(store.getElementIDAtIndex(0), false)); - act(() => store.toggleIsCollapsed(store.getElementIDAtIndex(2), false)); + await act(() => + store.toggleIsCollapsed(store.getElementIDAtIndex(0), false), + ); + await act(() => + store.toggleIsCollapsed(store.getElementIDAtIndex(2), false), + ); expect(store).toMatchInlineSnapshot(` [root] ▾ + [shell] + `); - act(() => { + await act(() => { render(); }); expect(store).toMatchInlineSnapshot(` @@ -1116,11 +1436,13 @@ describe('Store', () => { + [shell] + `); }); // @reactVersion >= 18.0 - it('should support expanding parts of the tree', () => { + it('should support expanding parts of the tree', async () => { const Grandparent = ({count}) => ( @@ -1131,7 +1453,7 @@ describe('Store', () => { new Array(count).fill(true).map((_, index) => ); const Child = () =>
    Hi!
    ; - act(() => render()); + await act(() => render()); expect(store).toMatchInlineSnapshot(` [root] ▸ @@ -1139,7 +1461,7 @@ describe('Store', () => { const grandparentID = store.getElementIDAtIndex(0); - act(() => store.toggleIsCollapsed(grandparentID, false)); + await act(() => store.toggleIsCollapsed(grandparentID, false)); expect(store).toMatchInlineSnapshot(` [root] ▾ @@ -1150,7 +1472,7 @@ describe('Store', () => { const parentOneID = store.getElementIDAtIndex(1); const parentTwoID = store.getElementIDAtIndex(2); - act(() => store.toggleIsCollapsed(parentOneID, false)); + await act(() => store.toggleIsCollapsed(parentOneID, false)); expect(store).toMatchInlineSnapshot(` [root] ▾ @@ -1160,7 +1482,7 @@ describe('Store', () => { ▸ `); - act(() => store.toggleIsCollapsed(parentTwoID, false)); + await act(() => store.toggleIsCollapsed(parentTwoID, false)); expect(store).toMatchInlineSnapshot(` [root] ▾ @@ -1172,7 +1494,7 @@ describe('Store', () => { `); - act(() => store.toggleIsCollapsed(parentOneID, true)); + await act(() => store.toggleIsCollapsed(parentOneID, true)); expect(store).toMatchInlineSnapshot(` [root] ▾ @@ -1182,7 +1504,7 @@ describe('Store', () => { `); - act(() => store.toggleIsCollapsed(parentTwoID, true)); + await act(() => store.toggleIsCollapsed(parentTwoID, true)); expect(store).toMatchInlineSnapshot(` [root] ▾ @@ -1190,7 +1512,7 @@ describe('Store', () => { ▸ `); - act(() => store.toggleIsCollapsed(grandparentID, true)); + await act(() => store.toggleIsCollapsed(grandparentID, true)); expect(store).toMatchInlineSnapshot(` [root] ▸ @@ -1198,7 +1520,7 @@ describe('Store', () => { }); // @reactVersion >= 18.0 - it('should support expanding deep parts of the tree', () => { + it('should support expanding deep parts of the tree', async () => { const Wrapper = ({forwardedRef}) => ( ); @@ -1211,7 +1533,7 @@ describe('Store', () => { const ref = React.createRef(); - act(() => render()); + await act(() => render()); expect(store).toMatchInlineSnapshot(` [root] ▸ @@ -1219,7 +1541,7 @@ describe('Store', () => { const deepestedNodeID = agent.getIDForHostInstance(ref.current); - act(() => store.toggleIsCollapsed(deepestedNodeID, false)); + await act(() => store.toggleIsCollapsed(deepestedNodeID, false)); expect(store).toMatchInlineSnapshot(` [root] ▾ @@ -1231,13 +1553,13 @@ describe('Store', () => { const rootID = store.getElementIDAtIndex(0); - act(() => store.toggleIsCollapsed(rootID, true)); + await act(() => store.toggleIsCollapsed(rootID, true)); expect(store).toMatchInlineSnapshot(` [root] ▸ `); - act(() => store.toggleIsCollapsed(rootID, false)); + await act(() => store.toggleIsCollapsed(rootID, false)); expect(store).toMatchInlineSnapshot(` [root] ▾ @@ -1249,14 +1571,14 @@ describe('Store', () => { const id = store.getElementIDAtIndex(1); - act(() => store.toggleIsCollapsed(id, true)); + await act(() => store.toggleIsCollapsed(id, true)); expect(store).toMatchInlineSnapshot(` [root] ▾ `); - act(() => store.toggleIsCollapsed(id, false)); + await act(() => store.toggleIsCollapsed(id, false)); expect(store).toMatchInlineSnapshot(` [root] ▾ @@ -1268,7 +1590,7 @@ describe('Store', () => { }); // @reactVersion >= 18.0 - it('should support reordering of children', () => { + it('should support reordering of children', async () => { const Root = ({children}) => children; const Component = () => null; @@ -1277,19 +1599,21 @@ describe('Store', () => { const foo = ; const bar = ; - act(() => render({[foo, bar]})); + await act(() => render({[foo, bar]})); expect(store).toMatchInlineSnapshot(` [root] ▸ `); - act(() => render({[bar, foo]})); + await act(() => render({[bar, foo]})); expect(store).toMatchInlineSnapshot(` [root] ▸ `); - act(() => store.toggleIsCollapsed(store.getElementIDAtIndex(0), false)); + await act(() => + store.toggleIsCollapsed(store.getElementIDAtIndex(0), false), + ); expect(store).toMatchInlineSnapshot(` [root] ▾ @@ -1297,7 +1621,7 @@ describe('Store', () => { ▸ `); - act(() => { + await act(() => { store.toggleIsCollapsed(store.getElementIDAtIndex(2), false); store.toggleIsCollapsed(store.getElementIDAtIndex(1), false); }); @@ -1311,7 +1635,9 @@ describe('Store', () => { `); - act(() => store.toggleIsCollapsed(store.getElementIDAtIndex(0), true)); + await act(() => + store.toggleIsCollapsed(store.getElementIDAtIndex(0), true), + ); expect(store).toMatchInlineSnapshot(` [root] ▸ @@ -1319,7 +1645,7 @@ describe('Store', () => { }); // @reactVersion >= 18.0 - it('should not add new nodes when suspense is toggled', () => { + it('should not add new nodes when suspense is toggled', async () => { const SuspenseTree = () => { return ( Loading outer}> @@ -1332,25 +1658,33 @@ describe('Store', () => { const Parent = () => ; const Child = () => null; - act(() => render()); + await act(() => render()); expect(store).toMatchInlineSnapshot(` [root] ▸ + [shell] + `); - act(() => store.toggleIsCollapsed(store.getElementIDAtIndex(0), false)); - act(() => store.toggleIsCollapsed(store.getElementIDAtIndex(1), false)); + await act(() => + store.toggleIsCollapsed(store.getElementIDAtIndex(0), false), + ); + await act(() => + store.toggleIsCollapsed(store.getElementIDAtIndex(1), false), + ); expect(store).toMatchInlineSnapshot(` [root] ▾ + [shell] + `); const rendererID = getRendererID(); const suspenseID = store.getElementIDAtIndex(1); - act(() => + await act(() => agent.overrideSuspense({ id: suspenseID, rendererID, @@ -1362,9 +1696,11 @@ describe('Store', () => { ▾ + [shell] + `); - act(() => + await act(() => agent.overrideSuspense({ id: suspenseID, rendererID, @@ -1376,6 +1712,8 @@ describe('Store', () => { ▾ + [shell] + `); }); }); @@ -1386,7 +1724,7 @@ describe('Store', () => { }); // @reactVersion >= 18.0 - it('should support a single root with a single child', () => { + it('should support a single root with a single child', async () => { const Grandparent = () => ( @@ -1396,7 +1734,7 @@ describe('Store', () => { const Parent = () => ; const Child = () => null; - act(() => render()); + await act(() => render()); for (let i = 0; i < store.numElements; i++) { expect(store.getIndexOfElementID(store.getElementIDAtIndex(i))).toBe(i); @@ -1404,12 +1742,12 @@ describe('Store', () => { }); // @reactVersion >= 18.0 - it('should support multiple roots with one children each', () => { + it('should support multiple roots with one children each', async () => { const Grandparent = () => ; const Parent = () => ; const Child = () => null; - act(() => { + await act(() => { render(); render(); }); @@ -1420,12 +1758,12 @@ describe('Store', () => { }); // @reactVersion >= 18.0 - it('should support a single root with multiple top level children', () => { + it('should support a single root with multiple top level children', async () => { const Grandparent = () => ; const Parent = () => ; const Child = () => null; - act(() => + await act(() => render( @@ -1440,12 +1778,12 @@ describe('Store', () => { }); // @reactVersion >= 18.0 - it('should support multiple roots with multiple top level children', () => { + it('should support multiple roots with multiple top level children', async () => { const Grandparent = () => ; const Parent = () => ; const Child = () => null; - act(() => { + await act(() => { render( @@ -1472,7 +1810,7 @@ describe('Store', () => { // @reactVersion >= 18.0 // @reactVersion < 19 // @gate !disableLegacyMode - it('detects and updates profiling support based on the attached roots (legacy render)', () => { + it('detects and updates profiling support based on the attached roots (legacy render)', async () => { const Component = () => null; const containerA = document.createElement('div'); @@ -1480,19 +1818,19 @@ describe('Store', () => { expect(store.rootSupportsBasicProfiling).toBe(false); - act(() => legacyRender(, containerA)); + await act(() => legacyRender(, containerA)); expect(store.rootSupportsBasicProfiling).toBe(true); - act(() => legacyRender(, containerB)); - act(() => ReactDOM.unmountComponentAtNode(containerA)); + await act(() => legacyRender(, containerB)); + await act(() => ReactDOM.unmountComponentAtNode(containerA)); expect(store.rootSupportsBasicProfiling).toBe(true); - act(() => ReactDOM.unmountComponentAtNode(containerB)); + await act(() => ReactDOM.unmountComponentAtNode(containerB)); expect(store.rootSupportsBasicProfiling).toBe(false); }); // @reactVersion >= 18 - it('detects and updates profiling support based on the attached roots (createRoot)', () => { + it('detects and updates profiling support based on the attached roots (createRoot)', async () => { const Component = () => null; const containerA = document.createElement('div'); @@ -1503,26 +1841,26 @@ describe('Store', () => { expect(store.rootSupportsBasicProfiling).toBe(false); - act(() => rootA.render()); + await act(() => rootA.render()); expect(store.rootSupportsBasicProfiling).toBe(true); - act(() => rootB.render()); - act(() => rootA.unmount()); + await act(() => rootB.render()); + await act(() => rootA.unmount()); expect(store.rootSupportsBasicProfiling).toBe(true); - act(() => rootB.unmount()); + await act(() => rootB.unmount()); expect(store.rootSupportsBasicProfiling).toBe(false); }); // @reactVersion >= 18.0 - it('should properly serialize non-string key values', () => { + it('should properly serialize non-string key values', async () => { const Child = () => null; // Bypass React element's automatic stringifying of keys intentionally. // This is pretty hacky. const fauxElement = Object.assign({}, , {key: 123}); - act(() => render([fauxElement])); + await act(() => render([fauxElement])); expect(store).toMatchInlineSnapshot(` [root] @@ -1581,12 +1919,12 @@ describe('Store', () => { ); // Render once to start fetching the lazy component - act(() => render()); + await act(() => render()); await Promise.resolve(); // Render again after it resolves - act(() => render()); + await act(() => render()); expect(store).toMatchInlineSnapshot(` [root] @@ -1638,7 +1976,7 @@ describe('Store', () => { const container = document.createElement('div'); // Render once to start fetching the lazy component - act(() => legacyRender(, container)); + await act(() => legacyRender(, container)); expect(store).toMatchInlineSnapshot(` [root] @@ -1649,7 +1987,7 @@ describe('Store', () => { await Promise.resolve(); // Render again after it resolves - act(() => legacyRender(, container)); + await act(() => legacyRender(, container)); expect(store).toMatchInlineSnapshot(` [root] @@ -1659,7 +1997,7 @@ describe('Store', () => { `); // Render again to unmount it - act(() => legacyRender(, container)); + await act(() => legacyRender(, container)); expect(store).toMatchInlineSnapshot(` [root] @@ -1673,28 +2011,32 @@ describe('Store', () => { const root = ReactDOMClient.createRoot(container); // Render once to start fetching the lazy component - act(() => root.render()); + await act(() => root.render()); expect(store).toMatchInlineSnapshot(` [root] ▾ + [shell] + `); await Promise.resolve(); // Render again after it resolves - act(() => root.render()); + await act(() => root.render()); expect(store).toMatchInlineSnapshot(` [root] ▾ + [shell] + `); // Render again to unmount it - act(() => root.render()); + await act(() => root.render()); expect(store).toMatchInlineSnapshot(` [root] @@ -1709,7 +2051,7 @@ describe('Store', () => { const container = document.createElement('div'); // Render once to start fetching the lazy component - act(() => legacyRender(, container)); + await act(() => legacyRender(, container)); expect(store).toMatchInlineSnapshot(` [root] @@ -1718,7 +2060,7 @@ describe('Store', () => { `); // Render again to unmount it before it finishes loading - act(() => legacyRender(, container)); + await act(() => legacyRender(, container)); expect(store).toMatchInlineSnapshot(` [root] @@ -1733,7 +2075,7 @@ describe('Store', () => { const root = ReactDOMClient.createRoot(container); // Render once to start fetching the lazy component - act(() => root.render()); + await act(() => root.render()); expect(store).toMatchInlineSnapshot(` [root] @@ -1742,7 +2084,7 @@ describe('Store', () => { `); // Render again to unmount it before it finishes loading - act(() => root.render()); + await act(() => root.render()); expect(store).toMatchInlineSnapshot(` [root] @@ -1753,15 +2095,15 @@ describe('Store', () => { describe('inline errors and warnings', () => { // @reactVersion >= 18.0 - it('during render are counted', () => { + it('during render are counted', async () => { function Example() { console.error('test-only: render error'); console.warn('test-only: render warning'); return null; } - withErrorsOrWarningsIgnored(['test-only:'], () => { - act(() => render()); + withErrorsOrWarningsIgnored(['test-only:'], async () => { + await act(() => render()); }); expect(store).toMatchInlineSnapshot(` @@ -1770,8 +2112,8 @@ describe('Store', () => { ✕⚠ `); - withErrorsOrWarningsIgnored(['test-only:'], () => { - act(() => render()); + withErrorsOrWarningsIgnored(['test-only:'], async () => { + await act(() => render()); }); expect(store).toMatchInlineSnapshot(` @@ -1782,7 +2124,7 @@ describe('Store', () => { }); // @reactVersion >= 18.0 - it('during layout get counted', () => { + it('during layout get counted', async () => { function Example() { React.useLayoutEffect(() => { console.error('test-only: layout error'); @@ -1791,8 +2133,8 @@ describe('Store', () => { return null; } - withErrorsOrWarningsIgnored(['test-only:'], () => { - act(() => render()); + withErrorsOrWarningsIgnored(['test-only:'], async () => { + await act(() => render()); }); expect(store).toMatchInlineSnapshot(` @@ -1801,8 +2143,8 @@ describe('Store', () => { ✕⚠ `); - withErrorsOrWarningsIgnored(['test-only:'], () => { - act(() => render()); + withErrorsOrWarningsIgnored(['test-only:'], async () => { + await act(() => render()); }); expect(store).toMatchInlineSnapshot(` @@ -1818,7 +2160,7 @@ describe('Store', () => { } // @reactVersion >= 18.0 - it('are counted (after no delay)', () => { + it('are counted (after no delay)', async () => { function Example() { React.useEffect(() => { console.error('test-only: passive error'); @@ -1827,8 +2169,8 @@ describe('Store', () => { return null; } - withErrorsOrWarningsIgnored(['test-only:'], () => { - act(() => { + withErrorsOrWarningsIgnored(['test-only:'], async () => { + await act(() => { render(); }, false); }); @@ -1839,12 +2181,12 @@ describe('Store', () => { ✕⚠ `); - act(() => unmount()); + await act(() => unmount()); expect(store).toMatchInlineSnapshot(``); }); // @reactVersion >= 18.0 - it('are flushed early when there is a new commit', () => { + it('are flushed early when there is a new commit', async () => { function Example() { React.useEffect(() => { console.error('test-only: passive error'); @@ -1890,7 +2232,7 @@ describe('Store', () => { `); }); - act(() => unmount()); + await act(() => unmount()); expect(store).toMatchInlineSnapshot(``); }); }); @@ -1898,7 +2240,7 @@ describe('Store', () => { // In React 19, JSX warnings were moved into the renderer - https://github.com/facebook/react/pull/29088 // The warning is moved to the Child instead of the Parent. // @reactVersion >= 19.0.1 - it('from react get counted [React >= 19.0.1]', () => { + it('from react get counted [React >= 19.0.1]', async () => { function Example() { return []; } @@ -1923,7 +2265,7 @@ describe('Store', () => { // @reactVersion >= 18.0 // @reactVersion < 19.0 - it('from react get counted [React 18.x]', () => { + it('from react get counted [React 18.x]', async () => { function Example() { return []; } @@ -1947,15 +2289,15 @@ describe('Store', () => { }); // @reactVersion >= 18.0 - it('can be cleared for the whole app', () => { + it('can be cleared for the whole app', async () => { function Example() { console.error('test-only: render error'); console.warn('test-only: render warning'); return null; } - withErrorsOrWarningsIgnored(['test-only:'], () => { - act(() => + withErrorsOrWarningsIgnored(['test-only:'], async () => { + await act(() => render( @@ -1988,15 +2330,15 @@ describe('Store', () => { }); // @reactVersion >= 18.0 - it('can be cleared for particular Fiber (only warnings)', () => { + it('can be cleared for particular Fiber (only warnings)', async () => { function Example() { console.error('test-only: render error'); console.warn('test-only: render warning'); return null; } - withErrorsOrWarningsIgnored(['test-only:'], () => { - act(() => + withErrorsOrWarningsIgnored(['test-only:'], async () => { + await act(() => render( @@ -2033,15 +2375,15 @@ describe('Store', () => { }); // @reactVersion >= 18.0 - it('can be cleared for a particular Fiber (only errors)', () => { + it('can be cleared for a particular Fiber (only errors)', async () => { function Example() { console.error('test-only: render error'); console.warn('test-only: render warning'); return null; } - withErrorsOrWarningsIgnored(['test-only:'], () => { - act(() => + withErrorsOrWarningsIgnored(['test-only:'], async () => { + await act(() => render( @@ -2078,7 +2420,7 @@ describe('Store', () => { }); // @reactVersion >= 18.0 - it('are updated when fibers are removed from the tree', () => { + it('are updated when fibers are removed from the tree', async () => { function ComponentWithWarning() { console.warn('test-only: render warning'); return null; @@ -2093,8 +2435,8 @@ describe('Store', () => { return null; } - withErrorsOrWarningsIgnored(['test-only:'], () => { - act(() => + withErrorsOrWarningsIgnored(['test-only:'], async () => { + await act(() => render( @@ -2112,8 +2454,8 @@ describe('Store', () => { ✕⚠ `); - withErrorsOrWarningsIgnored(['test-only:'], () => { - act(() => + withErrorsOrWarningsIgnored(['test-only:'], async () => { + await act(() => render( @@ -2129,8 +2471,8 @@ describe('Store', () => { ✕⚠ `); - withErrorsOrWarningsIgnored(['test-only:'], () => { - act(() => + withErrorsOrWarningsIgnored(['test-only:'], async () => { + await act(() => render( @@ -2144,8 +2486,8 @@ describe('Store', () => { ⚠ `); - withErrorsOrWarningsIgnored(['test-only:'], () => { - act(() => render()); + withErrorsOrWarningsIgnored(['test-only:'], async () => { + await act(() => render()); }); expect(store).toMatchInlineSnapshot(`[root]`); expect(store.componentWithErrorCount).toBe(0); @@ -2176,20 +2518,24 @@ describe('Store', () => { await actAsync(() => render()); expect(store).toMatchInlineSnapshot(` - [root] - ▾ - ▾ - - `); + [root] + ▾ + ▾ + + [shell] + + `); await actAsync(() => render()); expect(store).toMatchInlineSnapshot(` - [root] - ▾ - ▾ - - `); + [root] + ▾ + ▾ + + [shell] + + `); }); }); @@ -2466,4 +2812,272 @@ describe('Store', () => { `); }); + + // @reactVersion >= 18.0 + it('can reconcile Suspense in fallback positions', async () => { + let resolveFallback; + const fallbackPromise = new Promise(resolve => { + resolveFallback = resolve; + }); + let resolveContent; + const contentPromise = new Promise(resolve => { + resolveContent = resolve; + }); + + function Component({children, promise}) { + if (promise) { + React.use(promise); + } + return
    {children}
    ; + } + + await actAsync(() => + render( + + Loading fallback... +
    + }> + + Loading... + +
    + }> + + done + + , + ), + ); + + expect(store).toMatchInlineSnapshot(` + [root] + ▾ + ▾ + + [shell] + + + `); + + await actAsync(() => { + resolveFallback(); + }); + + expect(store).toMatchInlineSnapshot(` + [root] + ▾ + ▾ + + [shell] + + + `); + + await actAsync(() => { + resolveContent(); + }); + + expect(store).toMatchInlineSnapshot(` + [root] + ▾ + + [shell] + + `); + }); + + // @reactVersion >= 18.0 + it('can reconcile resuspended Suspense with Suspense in fallback positions', async () => { + let resolveHeadFallback; + let resolveHeadContent; + let resolveMainFallback; + let resolveMainContent; + + function Component({children, promise}) { + if (promise) { + React.use(promise); + } + return
    {children}
    ; + } + + function WithSuspenseInFallback({fallbackPromise, contentPromise, name}) { + return ( + + Loading fallback... +
    + }> + + Loading... + + + }> + + done + + + ); + } + + function App({ + headFallbackPromise, + headContentPromise, + mainContentPromise, + mainFallbackPromise, + tailContentPromise, + tailFallbackPromise, + }) { + return ( + <> + + + + ); + } + + const initialHeadContentPromise = new Promise(resolve => { + resolveHeadContent = resolve; + }); + const initialHeadFallbackPromise = new Promise(resolve => { + resolveHeadFallback = resolve; + }); + const initialMainContentPromise = new Promise(resolve => { + resolveMainContent = resolve; + }); + const initialMainFallbackPromise = new Promise(resolve => { + resolveMainFallback = resolve; + }); + await actAsync(() => + render( + , + ), + ); + + expect(store).toMatchInlineSnapshot(` + [root] + ▾ + ▾ + ▾ + ▾ + + ▾ + ▾ + ▾ + + [shell] + + + + + `); + + await actAsync(() => { + resolveHeadFallback(); + resolveMainFallback(); + resolveHeadContent(); + resolveMainContent(); + }); + + expect(store).toMatchInlineSnapshot(` + [root] + ▾ + ▾ + ▾ + + ▾ + ▾ + + [shell] + + + `); + + // Resuspend head content + const nextHeadContentPromise = new Promise(resolve => { + resolveHeadContent = resolve; + }); + await actAsync(() => + render( + , + ), + ); + + expect(store).toMatchInlineSnapshot(` + [root] + ▾ + ▾ + ▾ + ▾ + + ▾ + ▾ + + [shell] + + + + `); + + // Resuspend head fallback + const nextHeadFallbackPromise = new Promise(resolve => { + resolveHeadFallback = resolve; + }); + await actAsync(() => + render( + , + ), + ); + + expect(store).toMatchInlineSnapshot(` + [root] + ▾ + ▾ + ▾ + ▾ + + ▾ + ▾ + + [shell] + + + + `); + }); }); diff --git a/packages/react-devtools-shared/src/__tests__/storeComponentFilters-test.js b/packages/react-devtools-shared/src/__tests__/storeComponentFilters-test.js index 5f00af92bf5ae..c29bff05383c5 100644 --- a/packages/react-devtools-shared/src/__tests__/storeComponentFilters-test.js +++ b/packages/react-devtools-shared/src/__tests__/storeComponentFilters-test.js @@ -156,6 +156,9 @@ describe('Store component filters', () => {
    + [shell] + + `); await actAsync( @@ -171,6 +174,9 @@ describe('Store component filters', () => {
    + [shell] + + `); await actAsync( @@ -186,6 +192,9 @@ describe('Store component filters', () => {
    + [shell] + + `); }); @@ -207,12 +216,11 @@ describe('Store component filters', () => { ); expect(store).toMatchInlineSnapshot(` - [root] - ▾ -
    - ▾ -
    - `); + [root] + ▾ +
    + + `); await actAsync( async () => @@ -222,10 +230,9 @@ describe('Store component filters', () => { ); expect(store).toMatchInlineSnapshot(` - [root] -
    -
    - `); + [root] +
    + `); await actAsync( async () => @@ -235,12 +242,11 @@ describe('Store component filters', () => { ); expect(store).toMatchInlineSnapshot(` - [root] - ▾ -
    - ▾ -
    - `); + [root] + ▾ +
    + + `); } }); @@ -262,12 +268,12 @@ describe('Store component filters', () => { ); expect(store).toMatchInlineSnapshot(` - [root] - ▾ -
    - ▾ -
    - `); + [root] + ▾ +
    + ▾ +
    + `); await actAsync( async () => @@ -277,12 +283,12 @@ describe('Store component filters', () => { ); expect(store).toMatchInlineSnapshot(` - [root] - ▾ -
    - ▾ -
    - `); + [root] + ▾ +
    + ▾ +
    + `); await actAsync( async () => @@ -292,12 +298,12 @@ describe('Store component filters', () => { ); expect(store).toMatchInlineSnapshot(` - [root] - ▾ -
    - ▾ -
    - `); + [root] + ▾ +
    + ▾ +
    + `); } }); @@ -509,7 +515,11 @@ describe('Store component filters', () => { const Component = ({shouldSuspend}) => { if (shouldSuspend) { - throw promise; + if (React.use) { + React.use(promise); + } else { + throw promise; + } } return null; }; diff --git a/packages/react-devtools-shared/src/__tests__/storeStressSync-test.js b/packages/react-devtools-shared/src/__tests__/storeStressSync-test.js index 585499fd81a65..759ce79590371 100644 --- a/packages/react-devtools-shared/src/__tests__/storeStressSync-test.js +++ b/packages/react-devtools-shared/src/__tests__/storeStressSync-test.js @@ -522,7 +522,11 @@ describe('StoreStress (Legacy Mode)', () => { ]; const Never = () => { - throw new Promise(() => {}); + if (React.use) { + React.use(new Promise(() => {})); + } else { + throw new Promise(() => {}); + } }; const Root = ({children}) => { @@ -1144,7 +1148,11 @@ describe('StoreStress (Legacy Mode)', () => { ]; const Never = () => { - throw new Promise(() => {}); + if (React.use) { + React.use(new Promise(() => {})); + } else { + throw new Promise(() => {}); + } }; const MaybeSuspend = ({children, suspend}) => { diff --git a/packages/react-devtools-shared/src/__tests__/storeStressTestConcurrent-test.js b/packages/react-devtools-shared/src/__tests__/storeStressTestConcurrent-test.js index 8dd4ce428438f..e060cb3f06ba9 100644 --- a/packages/react-devtools-shared/src/__tests__/storeStressTestConcurrent-test.js +++ b/packages/react-devtools-shared/src/__tests__/storeStressTestConcurrent-test.js @@ -32,13 +32,13 @@ describe('StoreStressConcurrent', () => { // this helper with the real thing. actAsync = require('./utils').actAsync; - print = require('./__serializers__/storeSerializer').print; + print = require('./__serializers__/storeSerializer').printStore; }); // This is a stress test for the tree mount/update/unmount traversal. // It renders different trees that should produce the same output. // @reactVersion >= 18.0 - it('should handle a stress test with different tree operations (Concurrent Mode)', () => { + it('should handle a stress test with different tree operations (Concurrent Mode)', async () => { let setShowX; const A = () => 'a'; const B = () => 'b'; @@ -67,8 +67,7 @@ describe('StoreStressConcurrent', () => { let container = document.createElement('div'); let root = ReactDOMClient.createRoot(container); act(() => root.render({[a, b, c, d, e]})); - expect(store).toMatchInlineSnapshot( - ` + expect(store).toMatchInlineSnapshot(` [root] ▾ @@ -76,8 +75,7 @@ describe('StoreStressConcurrent', () => { - `, - ); + `); expect(container.textContent).toMatch('abcde'); const snapshotForABCDE = print(store); @@ -86,8 +84,7 @@ describe('StoreStressConcurrent', () => { act(() => { setShowX(true); }); - expect(store).toMatchInlineSnapshot( - ` + expect(store).toMatchInlineSnapshot(` [root] ▾ @@ -96,8 +93,7 @@ describe('StoreStressConcurrent', () => { - `, - ); + `); expect(container.textContent).toMatch('abxde'); const snapshotForABXDE = print(store); @@ -151,26 +147,26 @@ describe('StoreStressConcurrent', () => { root = ReactDOMClient.createRoot(container); // Verify mounting 'abcde'. - act(() => root.render({cases[i]})); + await act(() => root.render({cases[i]})); expect(container.textContent).toMatch('abcde'); expect(print(store)).toEqual(snapshotForABCDE); // Verify switching to 'abxde'. - act(() => { + await act(() => { setShowX(true); }); expect(container.textContent).toMatch('abxde'); expect(print(store)).toBe(snapshotForABXDE); // Verify switching back to 'abcde'. - act(() => { + await act(() => { setShowX(false); }); expect(container.textContent).toMatch('abcde'); expect(print(store)).toBe(snapshotForABCDE); // Clean up. - act(() => root.unmount()); + await act(() => root.unmount()); expect(print(store)).toBe(''); } @@ -180,19 +176,19 @@ describe('StoreStressConcurrent', () => { root = ReactDOMClient.createRoot(container); for (let i = 0; i < cases.length; i++) { // Verify mounting 'abcde'. - act(() => root.render({cases[i]})); + await act(() => root.render({cases[i]})); expect(container.textContent).toMatch('abcde'); expect(print(store)).toEqual(snapshotForABCDE); // Verify switching to 'abxde'. - act(() => { + await act(() => { setShowX(true); }); expect(container.textContent).toMatch('abxde'); expect(print(store)).toBe(snapshotForABXDE); // Verify switching back to 'abcde'. - act(() => { + await act(() => { setShowX(false); }); expect(container.textContent).toMatch('abcde'); @@ -204,7 +200,7 @@ describe('StoreStressConcurrent', () => { }); // @reactVersion >= 18.0 - it('should handle stress test with reordering (Concurrent Mode)', () => { + it('should handle stress test with reordering (Concurrent Mode)', async () => { const A = () => 'a'; const B = () => 'b'; const C = () => 'c'; @@ -245,10 +241,10 @@ describe('StoreStressConcurrent', () => { let container = document.createElement('div'); for (let i = 0; i < steps.length; i++) { const root = ReactDOMClient.createRoot(container); - act(() => root.render({steps[i]})); + await act(() => root.render({steps[i]})); // We snapshot each step once so it doesn't regress. snapshots.push(print(store)); - act(() => root.unmount()); + await act(() => root.unmount()); expect(print(store)).toBe(''); } @@ -316,13 +312,13 @@ describe('StoreStressConcurrent', () => { for (let j = 0; j < steps.length; j++) { container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); - act(() => root.render({steps[i]})); + await act(() => root.render({steps[i]})); expect(print(store)).toMatch(snapshots[i]); - act(() => root.render({steps[j]})); + await act(() => root.render({steps[j]})); expect(print(store)).toMatch(snapshots[j]); - act(() => root.render({steps[i]})); + await act(() => root.render({steps[i]})); expect(print(store)).toMatch(snapshots[i]); - act(() => root.unmount()); + await act(() => root.unmount()); expect(print(store)).toBe(''); } } @@ -332,7 +328,7 @@ describe('StoreStressConcurrent', () => { for (let j = 0; j < steps.length; j++) { container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); - act(() => + await act(() => root.render(
    {steps[i]}
    @@ -340,7 +336,7 @@ describe('StoreStressConcurrent', () => { ), ); expect(print(store)).toMatch(snapshots[i]); - act(() => + await act(() => root.render(
    {steps[j]}
    @@ -348,7 +344,7 @@ describe('StoreStressConcurrent', () => { ), ); expect(print(store)).toMatch(snapshots[j]); - act(() => + await act(() => root.render(
    {steps[i]}
    @@ -356,7 +352,7 @@ describe('StoreStressConcurrent', () => { ), ); expect(print(store)).toMatch(snapshots[i]); - act(() => root.unmount()); + await act(() => root.unmount()); expect(print(store)).toBe(''); } } @@ -392,7 +388,11 @@ describe('StoreStressConcurrent', () => { ]; const Never = () => { - throw new Promise(() => {}); + if (React.use) { + React.use(new Promise(() => {})); + } else { + throw new Promise(() => {}); + } }; const Root = ({children}) => { @@ -405,7 +405,7 @@ describe('StoreStressConcurrent', () => { let container = document.createElement('div'); for (let i = 0; i < steps.length; i++) { const root = ReactDOMClient.createRoot(container); - act(() => + await act(() => root.render( @@ -415,8 +415,8 @@ describe('StoreStressConcurrent', () => { ), ); // We snapshot each step once so it doesn't regress.d - snapshots.push(print(store)); - act(() => root.unmount()); + snapshots.push(print(store, false, null, false)); + await act(() => root.unmount()); expect(print(store)).toBe(''); } @@ -507,7 +507,7 @@ describe('StoreStressConcurrent', () => { // 2. Verify check Suspense can render same steps as initial fallback content. for (let i = 0; i < steps.length; i++) { const root = ReactDOMClient.createRoot(container); - act(() => + await act(() => root.render( @@ -520,8 +520,8 @@ describe('StoreStressConcurrent', () => { , ), ); - expect(print(store)).toEqual(snapshots[i]); - act(() => root.unmount()); + expect(print(store, false, null, false)).toEqual(snapshots[i]); + await act(() => root.unmount()); expect(print(store)).toBe(''); } @@ -531,7 +531,7 @@ describe('StoreStressConcurrent', () => { // Always start with a fresh container and steps[i]. container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); - act(() => + await act(() => root.render( @@ -540,9 +540,9 @@ describe('StoreStressConcurrent', () => { , ), ); - expect(print(store)).toEqual(snapshots[i]); + expect(print(store, false, null, false)).toEqual(snapshots[i]); // Re-render with steps[j]. - act(() => + await act(() => root.render( @@ -552,9 +552,9 @@ describe('StoreStressConcurrent', () => { ), ); // Verify the successful transition to steps[j]. - expect(print(store)).toEqual(snapshots[j]); + expect(print(store, false, null, false)).toEqual(snapshots[j]); // Check that we can transition back again. - act(() => + await act(() => root.render( @@ -563,9 +563,9 @@ describe('StoreStressConcurrent', () => { , ), ); - expect(print(store)).toEqual(snapshots[i]); + expect(print(store, false, null, false)).toEqual(snapshots[i]); // Clean up after every iteration. - act(() => root.unmount()); + await act(() => root.unmount()); expect(print(store)).toBe(''); } } @@ -576,7 +576,7 @@ describe('StoreStressConcurrent', () => { // Always start with a fresh container and steps[i]. container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); - act(() => + await act(() => root.render( @@ -589,9 +589,9 @@ describe('StoreStressConcurrent', () => { , ), ); - expect(print(store)).toEqual(snapshots[i]); + expect(print(store, false, null, false)).toEqual(snapshots[i]); // Re-render with steps[j]. - act(() => + await act(() => root.render( @@ -605,9 +605,9 @@ describe('StoreStressConcurrent', () => { ), ); // Verify the successful transition to steps[j]. - expect(print(store)).toEqual(snapshots[j]); + expect(print(store, false, null, false)).toEqual(snapshots[j]); // Check that we can transition back again. - act(() => + await act(() => root.render( @@ -620,9 +620,9 @@ describe('StoreStressConcurrent', () => { , ), ); - expect(print(store)).toEqual(snapshots[i]); + expect(print(store, false, null, false)).toEqual(snapshots[i]); // Clean up after every iteration. - act(() => root.unmount()); + await act(() => root.unmount()); expect(print(store)).toBe(''); } } @@ -633,7 +633,7 @@ describe('StoreStressConcurrent', () => { // Always start with a fresh container and steps[i]. container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); - act(() => + await act(() => root.render( @@ -642,9 +642,9 @@ describe('StoreStressConcurrent', () => { , ), ); - expect(print(store)).toEqual(snapshots[i]); + expect(print(store, false, null, false)).toEqual(snapshots[i]); // Re-render with steps[j]. - act(() => + await act(() => root.render( @@ -658,9 +658,9 @@ describe('StoreStressConcurrent', () => { ), ); // Verify the successful transition to steps[j]. - expect(print(store)).toEqual(snapshots[j]); + expect(print(store, false, null, false)).toEqual(snapshots[j]); // Check that we can transition back again. - act(() => + await act(() => root.render( @@ -669,9 +669,9 @@ describe('StoreStressConcurrent', () => { , ), ); - expect(print(store)).toEqual(snapshots[i]); + expect(print(store, false, null, false)).toEqual(snapshots[i]); // Clean up after every iteration. - act(() => root.unmount()); + await act(() => root.unmount()); expect(print(store)).toBe(''); } } @@ -682,7 +682,7 @@ describe('StoreStressConcurrent', () => { // Always start with a fresh container and steps[i]. container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); - act(() => + await act(() => root.render( @@ -695,9 +695,9 @@ describe('StoreStressConcurrent', () => { , ), ); - expect(print(store)).toEqual(snapshots[i]); + expect(print(store, false, null, false)).toEqual(snapshots[i]); // Re-render with steps[j]. - act(() => + await act(() => root.render( @@ -707,9 +707,9 @@ describe('StoreStressConcurrent', () => { ), ); // Verify the successful transition to steps[j]. - expect(print(store)).toEqual(snapshots[j]); + expect(print(store, false, null, false)).toEqual(snapshots[j]); // Check that we can transition back again. - act(() => + await act(() => root.render( @@ -722,9 +722,9 @@ describe('StoreStressConcurrent', () => { , ), ); - expect(print(store)).toEqual(snapshots[i]); + expect(print(store, false, null, false)).toEqual(snapshots[i]); // Clean up after every iteration. - act(() => root.unmount()); + await act(() => root.unmount()); expect(print(store)).toBe(''); } } @@ -735,7 +735,7 @@ describe('StoreStressConcurrent', () => { // Always start with a fresh container and steps[i]. container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); - act(() => + await act(() => root.render( @@ -751,7 +751,7 @@ describe('StoreStressConcurrent', () => { const suspenseID = store.getElementIDAtIndex(2); // Force fallback. - expect(print(store)).toEqual(snapshots[i]); + expect(print(store, false, null, false)).toEqual(snapshots[i]); await actAsync(async () => { bridge.send('overrideSuspense', { id: suspenseID, @@ -759,7 +759,7 @@ describe('StoreStressConcurrent', () => { forceFallback: true, }); }); - expect(print(store)).toEqual(snapshots[j]); + expect(print(store, false, null, false)).toEqual(snapshots[j]); // Stop forcing fallback. await actAsync(async () => { @@ -769,10 +769,10 @@ describe('StoreStressConcurrent', () => { forceFallback: false, }); }); - expect(print(store)).toEqual(snapshots[i]); + expect(print(store, false, null, false)).toEqual(snapshots[i]); // Trigger actual fallback. - act(() => + await act(() => root.render( @@ -785,10 +785,10 @@ describe('StoreStressConcurrent', () => { , ), ); - expect(print(store)).toEqual(snapshots[j]); + expect(print(store, false, null, false)).toEqual(snapshots[j]); // Force fallback while we're in fallback mode. - act(() => { + await act(() => { bridge.send('overrideSuspense', { id: suspenseID, rendererID: store.getRendererIDForElement(suspenseID), @@ -796,10 +796,10 @@ describe('StoreStressConcurrent', () => { }); }); // Keep seeing fallback content. - expect(print(store)).toEqual(snapshots[j]); + expect(print(store, false, null, false)).toEqual(snapshots[j]); // Switch to primary mode. - act(() => + await act(() => root.render( @@ -809,7 +809,7 @@ describe('StoreStressConcurrent', () => { ), ); // Fallback is still forced though. - expect(print(store)).toEqual(snapshots[j]); + expect(print(store, false, null, false)).toEqual(snapshots[j]); // Stop forcing fallback. This reverts to primary content. await actAsync(async () => { @@ -820,7 +820,7 @@ describe('StoreStressConcurrent', () => { }); }); // Now we see primary content. - expect(print(store)).toEqual(snapshots[i]); + expect(print(store, false, null, false)).toEqual(snapshots[i]); // Clean up after every iteration. await actAsync(async () => root.unmount()); @@ -859,7 +859,11 @@ describe('StoreStressConcurrent', () => { ]; const Never = () => { - throw new Promise(() => {}); + if (React.use) { + React.use(new Promise(() => {})); + } else { + throw new Promise(() => {}); + } }; const MaybeSuspend = ({children, suspend}) => { @@ -890,7 +894,7 @@ describe('StoreStressConcurrent', () => { let container = document.createElement('div'); for (let i = 0; i < steps.length; i++) { const root = ReactDOMClient.createRoot(container); - act(() => + await act(() => root.render( @@ -902,8 +906,8 @@ describe('StoreStressConcurrent', () => { ), ); // We snapshot each step once so it doesn't regress. - snapshots.push(print(store)); - act(() => root.unmount()); + snapshots.push(print(store, false, null, false)); + await act(() => root.unmount()); expect(print(store)).toBe(''); } @@ -913,7 +917,7 @@ describe('StoreStressConcurrent', () => { const fallbackSnapshots = []; for (let i = 0; i < steps.length; i++) { const root = ReactDOMClient.createRoot(container); - act(() => + await act(() => root.render( @@ -927,8 +931,8 @@ describe('StoreStressConcurrent', () => { ), ); // We snapshot each step once so it doesn't regress. - fallbackSnapshots.push(print(store)); - act(() => root.unmount()); + fallbackSnapshots.push(print(store, false, null, false)); + await act(() => root.unmount()); expect(print(store)).toBe(''); } @@ -1046,7 +1050,7 @@ describe('StoreStressConcurrent', () => { // Always start with a fresh container and steps[i]. container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); - act(() => + await act(() => root.render( @@ -1057,9 +1061,9 @@ describe('StoreStressConcurrent', () => { , ), ); - expect(print(store)).toEqual(snapshots[i]); + expect(print(store, false, null, false)).toEqual(snapshots[i]); // Re-render with steps[j]. - act(() => + await act(() => root.render( @@ -1071,9 +1075,9 @@ describe('StoreStressConcurrent', () => { ), ); // Verify the successful transition to steps[j]. - expect(print(store)).toEqual(snapshots[j]); + expect(print(store, false, null, false)).toEqual(snapshots[j]); // Check that we can transition back again. - act(() => + await act(() => root.render( @@ -1084,9 +1088,9 @@ describe('StoreStressConcurrent', () => { , ), ); - expect(print(store)).toEqual(snapshots[i]); + expect(print(store, false, null, false)).toEqual(snapshots[i]); // Clean up after every iteration. - act(() => root.unmount()); + await act(() => root.unmount()); expect(print(store)).toBe(''); } } @@ -1097,7 +1101,7 @@ describe('StoreStressConcurrent', () => { // Always start with a fresh container and steps[i]. container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); - act(() => + await act(() => root.render( @@ -1113,9 +1117,9 @@ describe('StoreStressConcurrent', () => { , ), ); - expect(print(store)).toEqual(fallbackSnapshots[i]); + expect(print(store, false, null, false)).toEqual(fallbackSnapshots[i]); // Re-render with steps[j]. - act(() => + await act(() => root.render( @@ -1132,9 +1136,9 @@ describe('StoreStressConcurrent', () => { ), ); // Verify the successful transition to steps[j]. - expect(print(store)).toEqual(fallbackSnapshots[j]); + expect(print(store, false, null, false)).toEqual(fallbackSnapshots[j]); // Check that we can transition back again. - act(() => + await act(() => root.render( @@ -1150,9 +1154,9 @@ describe('StoreStressConcurrent', () => { , ), ); - expect(print(store)).toEqual(fallbackSnapshots[i]); + expect(print(store, false, null, false)).toEqual(fallbackSnapshots[i]); // Clean up after every iteration. - act(() => root.unmount()); + await act(() => root.unmount()); expect(print(store)).toBe(''); } } @@ -1163,7 +1167,7 @@ describe('StoreStressConcurrent', () => { // Always start with a fresh container and steps[i]. container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); - act(() => + await act(() => root.render( @@ -1174,9 +1178,9 @@ describe('StoreStressConcurrent', () => { , ), ); - expect(print(store)).toEqual(snapshots[i]); + expect(print(store, false, null, false)).toEqual(snapshots[i]); // Re-render with steps[j]. - act(() => + await act(() => root.render( @@ -1188,9 +1192,9 @@ describe('StoreStressConcurrent', () => { ), ); // Verify the successful transition to steps[j]. - expect(print(store)).toEqual(fallbackSnapshots[j]); + expect(print(store, false, null, false)).toEqual(fallbackSnapshots[j]); // Check that we can transition back again. - act(() => + await act(() => root.render( @@ -1201,9 +1205,9 @@ describe('StoreStressConcurrent', () => { , ), ); - expect(print(store)).toEqual(snapshots[i]); + expect(print(store, false, null, false)).toEqual(snapshots[i]); // Clean up after every iteration. - act(() => root.unmount()); + await act(() => root.unmount()); expect(print(store)).toBe(''); } } @@ -1214,7 +1218,7 @@ describe('StoreStressConcurrent', () => { // Always start with a fresh container and steps[i]. container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); - act(() => + await act(() => root.render( @@ -1225,9 +1229,9 @@ describe('StoreStressConcurrent', () => { , ), ); - expect(print(store)).toEqual(fallbackSnapshots[i]); + expect(print(store, false, null, false)).toEqual(fallbackSnapshots[i]); // Re-render with steps[j]. - act(() => + await act(() => root.render( @@ -1239,9 +1243,9 @@ describe('StoreStressConcurrent', () => { ), ); // Verify the successful transition to steps[j]. - expect(print(store)).toEqual(snapshots[j]); + expect(print(store, false, null, false)).toEqual(snapshots[j]); // Check that we can transition back again. - act(() => + await act(() => root.render( @@ -1252,9 +1256,9 @@ describe('StoreStressConcurrent', () => { , ), ); - expect(print(store)).toEqual(fallbackSnapshots[i]); + expect(print(store, false, null, false)).toEqual(fallbackSnapshots[i]); // Clean up after every iteration. - act(() => root.unmount()); + await act(() => root.unmount()); expect(print(store)).toBe(''); } } @@ -1265,7 +1269,7 @@ describe('StoreStressConcurrent', () => { // Always start with a fresh container and steps[i]. container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); - act(() => + await act(() => root.render( @@ -1283,7 +1287,7 @@ describe('StoreStressConcurrent', () => { const suspenseID = store.getElementIDAtIndex(2); // Force fallback. - expect(print(store)).toEqual(snapshots[i]); + expect(print(store, false, null, false)).toEqual(snapshots[i]); await actAsync(async () => { bridge.send('overrideSuspense', { id: suspenseID, @@ -1291,7 +1295,7 @@ describe('StoreStressConcurrent', () => { forceFallback: true, }); }); - expect(print(store)).toEqual(fallbackSnapshots[j]); + expect(print(store, false, null, false)).toEqual(fallbackSnapshots[j]); // Stop forcing fallback. await actAsync(async () => { @@ -1301,10 +1305,10 @@ describe('StoreStressConcurrent', () => { forceFallback: false, }); }); - expect(print(store)).toEqual(snapshots[i]); + expect(print(store, false, null, false)).toEqual(snapshots[i]); // Trigger actual fallback. - act(() => + await act(() => root.render( @@ -1315,10 +1319,10 @@ describe('StoreStressConcurrent', () => { , ), ); - expect(print(store)).toEqual(fallbackSnapshots[j]); + expect(print(store, false, null, false)).toEqual(fallbackSnapshots[j]); // Force fallback while we're in fallback mode. - act(() => { + await act(() => { bridge.send('overrideSuspense', { id: suspenseID, rendererID: store.getRendererIDForElement(suspenseID), @@ -1326,10 +1330,10 @@ describe('StoreStressConcurrent', () => { }); }); // Keep seeing fallback content. - expect(print(store)).toEqual(fallbackSnapshots[j]); + expect(print(store, false, null, false)).toEqual(fallbackSnapshots[j]); // Switch to primary mode. - act(() => + await act(() => root.render( @@ -1341,7 +1345,7 @@ describe('StoreStressConcurrent', () => { ), ); // Fallback is still forced though. - expect(print(store)).toEqual(fallbackSnapshots[j]); + expect(print(store, false, null, false)).toEqual(fallbackSnapshots[j]); // Stop forcing fallback. This reverts to primary content. await actAsync(async () => { @@ -1352,10 +1356,10 @@ describe('StoreStressConcurrent', () => { }); }); // Now we see primary content. - expect(print(store)).toEqual(snapshots[i]); + expect(print(store, false, null, false)).toEqual(snapshots[i]); // Clean up after every iteration. - act(() => root.unmount()); + await act(() => root.unmount()); expect(print(store)).toBe(''); } } diff --git a/packages/react-devtools-shared/src/__tests__/treeContext-test.js b/packages/react-devtools-shared/src/__tests__/treeContext-test.js index fa2031c6b5c8d..e7042418053ea 100644 --- a/packages/react-devtools-shared/src/__tests__/treeContext-test.js +++ b/packages/react-devtools-shared/src/__tests__/treeContext-test.js @@ -1368,6 +1368,9 @@ describe('TreeListContext', () => { ▾ + [shell] + + `); const outerSuspenseID = ((store.getElementIDAtIndex(1): any): number); @@ -1407,6 +1410,9 @@ describe('TreeListContext', () => { ▾ + [shell] + + `); }); }); @@ -2361,16 +2367,20 @@ describe('TreeListContext', () => { jest.runAllTimers(); expect(state).toMatchInlineSnapshot(` - [root] - - `); + [root] + + [shell] + + `); selectNextErrorOrWarning(); expect(state).toMatchInlineSnapshot(` - [root] - - `); + [root] + + [shell] + + `); }); it('should properly handle errors/warnings from components that dont mount because of Suspense', async () => { @@ -2392,9 +2402,11 @@ describe('TreeListContext', () => { utils.act(() => TestRenderer.create()); expect(state).toMatchInlineSnapshot(` - [root] - - `); + [root] + + [shell] + + `); await Promise.resolve(); withErrorsOrWarningsIgnored(['test-only:'], () => @@ -2414,6 +2426,8 @@ describe('TreeListContext', () => { ▾ + [shell] + `); }); @@ -2442,6 +2456,8 @@ describe('TreeListContext', () => { ▾ ✕ + [shell] + `); await Promise.resolve(); @@ -2456,10 +2472,12 @@ describe('TreeListContext', () => { ); expect(state).toMatchInlineSnapshot(` - [root] - ▾ - - `); + [root] + ▾ + + [shell] + + `); }); }); diff --git a/packages/react-devtools-shared/src/__tests__/utils-test.js b/packages/react-devtools-shared/src/__tests__/utils-test.js index 876aaa99f1ea9..dcffd2a228b0c 100644 --- a/packages/react-devtools-shared/src/__tests__/utils-test.js +++ b/packages/react-devtools-shared/src/__tests__/utils-test.js @@ -12,15 +12,15 @@ import { getDisplayNameForReactElement, isPlainObject, } from 'react-devtools-shared/src/utils'; -import {stackToComponentSources} from 'react-devtools-shared/src/devtools/utils'; +import {stackToComponentLocations} from 'react-devtools-shared/src/devtools/utils'; import { formatConsoleArguments, formatConsoleArgumentsToSingleString, formatWithStyles, gt, gte, - parseSourceFromComponentStack, } from 'react-devtools-shared/src/backend/utils'; +import {extractLocationFromComponentStack} from 'react-devtools-shared/src/backend/utils/parseStackTrace'; import { REACT_SUSPENSE_LIST_TYPE as SuspenseList, REACT_STRICT_MODE_TYPE as StrictMode, @@ -63,14 +63,17 @@ describe('utils', () => { it('should parse a component stack trace', () => { expect( - stackToComponentSources(` + stackToComponentLocations(` at Foobar (http://localhost:3000/static/js/bundle.js:103:74) at a at header at div at App`), ).toEqual([ - ['Foobar', ['http://localhost:3000/static/js/bundle.js', 103, 74]], + [ + 'Foobar', + ['Foobar', 'http://localhost:3000/static/js/bundle.js', 103, 74], + ], ['a', null], ['header', null], ['div', null], @@ -303,29 +306,29 @@ describe('utils', () => { }); }); - describe('parseSourceFromComponentStack', () => { + describe('extractLocationFromComponentStack', () => { it('should return null if passed empty string', () => { - expect(parseSourceFromComponentStack('')).toEqual(null); + expect(extractLocationFromComponentStack('')).toEqual(null); }); it('should construct the source from the first frame if available', () => { expect( - parseSourceFromComponentStack( + extractLocationFromComponentStack( 'at l (https://react.dev/_next/static/chunks/main-78a3b4c2aa4e4850.js:1:10389)\n' + 'at f (https://react.dev/_next/static/chunks/pages/%5B%5B...markdownPath%5D%5D-af2ed613aedf1d57.js:1:8519)\n' + 'at r (https://react.dev/_next/static/chunks/pages/_app-dd0b77ea7bd5b246.js:1:498)\n', ), - ).toEqual({ - sourceURL: - 'https://react.dev/_next/static/chunks/main-78a3b4c2aa4e4850.js', - line: 1, - column: 10389, - }); + ).toEqual([ + 'l', + 'https://react.dev/_next/static/chunks/main-78a3b4c2aa4e4850.js', + 1, + 10389, + ]); }); it('should construct the source from highest available frame', () => { expect( - parseSourceFromComponentStack( + extractLocationFromComponentStack( ' at Q\n' + ' at a\n' + ' at m (https://react.dev/_next/static/chunks/848-122f91e9565d9ffa.js:5:9236)\n' + @@ -338,57 +341,57 @@ describe('utils', () => { ' at tt (https://react.dev/_next/static/chunks/363-3c5f1b553b6be118.js:1:165520)\n' + ' at f (https://react.dev/_next/static/chunks/pages/%5B%5B...markdownPath%5D%5D-af2ed613aedf1d57.js:1:8519)', ), - ).toEqual({ - sourceURL: - 'https://react.dev/_next/static/chunks/848-122f91e9565d9ffa.js', - line: 5, - column: 9236, - }); + ).toEqual([ + 'm', + 'https://react.dev/_next/static/chunks/848-122f91e9565d9ffa.js', + 5, + 9236, + ]); }); it('should construct the source from frame, which has only url specified', () => { expect( - parseSourceFromComponentStack( + extractLocationFromComponentStack( ' at Q\n' + ' at a\n' + ' at https://react.dev/_next/static/chunks/848-122f91e9565d9ffa.js:5:9236\n', ), - ).toEqual({ - sourceURL: - 'https://react.dev/_next/static/chunks/848-122f91e9565d9ffa.js', - line: 5, - column: 9236, - }); + ).toEqual([ + '', + 'https://react.dev/_next/static/chunks/848-122f91e9565d9ffa.js', + 5, + 9236, + ]); }); it('should parse sourceURL correctly if it includes parentheses', () => { expect( - parseSourceFromComponentStack( + extractLocationFromComponentStack( 'at HotReload (webpack-internal:///(app-pages-browser)/./node_modules/next/dist/client/components/react-dev-overlay/hot-reloader-client.js:307:11)\n' + ' at Router (webpack-internal:///(app-pages-browser)/./node_modules/next/dist/client/components/app-router.js:181:11)\n' + ' at ErrorBoundaryHandler (webpack-internal:///(app-pages-browser)/./node_modules/next/dist/client/components/error-boundary.js:114:9)', ), - ).toEqual({ - sourceURL: - 'webpack-internal:///(app-pages-browser)/./node_modules/next/dist/client/components/react-dev-overlay/hot-reloader-client.js', - line: 307, - column: 11, - }); + ).toEqual([ + 'HotReload', + 'webpack-internal:///(app-pages-browser)/./node_modules/next/dist/client/components/react-dev-overlay/hot-reloader-client.js', + 307, + 11, + ]); }); it('should support Firefox stack', () => { expect( - parseSourceFromComponentStack( + extractLocationFromComponentStack( 'tt@https://react.dev/_next/static/chunks/363-3c5f1b553b6be118.js:1:165558\n' + 'f@https://react.dev/_next/static/chunks/pages/%5B%5B...markdownPath%5D%5D-af2ed613aedf1d57.js:1:8535\n' + 'r@https://react.dev/_next/static/chunks/pages/_app-dd0b77ea7bd5b246.js:1:513', ), - ).toEqual({ - sourceURL: - 'https://react.dev/_next/static/chunks/363-3c5f1b553b6be118.js', - line: 1, - column: 165558, - }); + ).toEqual([ + 'tt', + 'https://react.dev/_next/static/chunks/363-3c5f1b553b6be118.js', + 1, + 165558, + ]); }); }); @@ -399,9 +402,8 @@ exports.f = f; function f() { } //# sourceMappingURL=`; const result = { - column: 16, - line: 1, - sourceURL: 'http://test/a.mts', + location: ['', 'http://test/a.mts', 1, 17], + ignored: false, }; const fs = { 'http://test/a.mts': `export function f() {}`, diff --git a/packages/react-devtools-shared/src/backend/NativeStyleEditor/setupNativeStyleEditor.js b/packages/react-devtools-shared/src/backend/NativeStyleEditor/setupNativeStyleEditor.js index 728f0e691c98b..7a8bfff43bf24 100644 --- a/packages/react-devtools-shared/src/backend/NativeStyleEditor/setupNativeStyleEditor.js +++ b/packages/react-devtools-shared/src/backend/NativeStyleEditor/setupNativeStyleEditor.js @@ -194,7 +194,7 @@ function renameStyle( const {instance, style} = data; const newStyle = newName - ? {[oldName]: undefined, [newName]: value} + ? {[oldName]: (undefined: string | void), [newName]: value} : {[oldName]: undefined}; let customStyle; diff --git a/packages/react-devtools-shared/src/backend/agent.js b/packages/react-devtools-shared/src/backend/agent.js index e883724f49765..98091a06d6a8f 100644 --- a/packages/react-devtools-shared/src/backend/agent.js +++ b/packages/react-devtools-shared/src/backend/agent.js @@ -130,6 +130,12 @@ type OverrideSuspenseParams = { forceFallback: boolean, }; +type OverrideSuspenseMilestoneParams = { + rendererID: number, + rootID: number, + suspendedSet: Array, +}; + type PersistedSelection = { rendererID: number, path: Array, @@ -198,6 +204,10 @@ export default class Agent extends EventEmitter<{ bridge.addListener('logElementToConsole', this.logElementToConsole); bridge.addListener('overrideError', this.overrideError); bridge.addListener('overrideSuspense', this.overrideSuspense); + bridge.addListener( + 'overrideSuspenseMilestone', + this.overrideSuspenseMilestone, + ); bridge.addListener('overrideValueAtPath', this.overrideValueAtPath); bridge.addListener('reloadAndProfile', this.reloadAndProfile); bridge.addListener('renamePath', this.renamePath); @@ -556,6 +566,21 @@ export default class Agent extends EventEmitter<{ } }; + overrideSuspenseMilestone: OverrideSuspenseMilestoneParams => void = ({ + rendererID, + rootID, + suspendedSet, + }) => { + const renderer = this._rendererInterfaces[rendererID]; + if (renderer == null) { + console.warn( + `Invalid renderer id "${rendererID}" to override suspense milestone`, + ); + } else { + renderer.overrideSuspenseMilestone(rootID, suspendedSet); + } + }; + overrideValueAtPath: OverrideValueAtPathParams => void = ({ hookID, id, @@ -710,6 +735,16 @@ export default class Agent extends EventEmitter<{ rendererInterface.setTraceUpdatesEnabled(this._traceUpdatesEnabled); + const renderer = rendererInterface.renderer; + if (renderer !== null) { + const devRenderer = renderer.bundleType === 1; + const enableSuspenseTab = + devRenderer && renderer.version.includes('-experimental-'); + if (enableSuspenseTab) { + this._bridge.send('enableSuspenseTab'); + } + } + // When the renderer is attached, we need to tell it whether // we remember the previous selection that we'd like to restore. // It'll start tracking mounts for matches to the last selection path. diff --git a/packages/react-devtools-shared/src/backend/fiber/DevToolsFiberComponentStack.js b/packages/react-devtools-shared/src/backend/fiber/DevToolsFiberComponentStack.js index 4f63f562e0b51..c938b6736323e 100644 --- a/packages/react-devtools-shared/src/backend/fiber/DevToolsFiberComponentStack.js +++ b/packages/react-devtools-shared/src/backend/fiber/DevToolsFiberComponentStack.js @@ -199,7 +199,7 @@ export function getOwnerStackByFiberInDev( if (typeof owner.tag === 'number') { const fiber: Fiber = (owner: any); owner = fiber._debugOwner; - let debugStack = fiber._debugStack; + let debugStack: void | null | string | Error = fiber._debugStack; // If we don't actually print the stack if there is no owner of this JSX element. // In a real app it's typically not useful since the root app is always controlled // by the framework. These also tend to have noisy stacks because they're not rooted diff --git a/packages/react-devtools-shared/src/backend/fiber/renderer.js b/packages/react-devtools-shared/src/backend/fiber/renderer.js index 711d76c92d075..545725526d04a 100644 --- a/packages/react-devtools-shared/src/backend/fiber/renderer.js +++ b/packages/react-devtools-shared/src/backend/fiber/renderer.js @@ -7,7 +7,18 @@ * @flow */ -import type {ReactComponentInfo, ReactDebugInfo} from 'shared/ReactTypes'; +import type { + Thenable, + ReactComponentInfo, + ReactDebugInfo, + ReactAsyncInfo, + ReactIOInfo, + ReactStackTrace, + ReactCallSite, + Wakeable, +} from 'shared/ReactTypes'; + +import type {HooksTree} from 'react-debug-tools/src/ReactDebugHooks'; import { ComponentFilterDisplayName, @@ -49,9 +60,13 @@ import { formatDurationToMicrosecondsGranularity, gt, gte, - parseSourceFromComponentStack, serializeToString, } from 'react-devtools-shared/src/backend/utils'; +import { + extractLocationFromComponentStack, + extractLocationFromOwnerStack, + parseStackTrace, +} from 'react-devtools-shared/src/backend/utils/parseStackTrace'; import { cleanForBridge, copyWithDelete, @@ -69,6 +84,14 @@ import { TREE_OPERATION_SET_SUBTREE_MODE, TREE_OPERATION_UPDATE_ERRORS_OR_WARNINGS, TREE_OPERATION_UPDATE_TREE_BASE_DURATION, + SUSPENSE_TREE_OPERATION_ADD, + SUSPENSE_TREE_OPERATION_REMOVE, + SUSPENSE_TREE_OPERATION_REORDER_CHILDREN, + SUSPENSE_TREE_OPERATION_RESIZE, + UNKNOWN_SUSPENDERS_NONE, + UNKNOWN_SUSPENDERS_REASON_PRODUCTION, + UNKNOWN_SUSPENDERS_REASON_OLD_VERSION, + UNKNOWN_SUSPENDERS_REASON_THROWN_PROMISE, } from '../../constants'; import {inspectHooksOfFiber} from 'react-debug-tools'; import { @@ -92,6 +115,7 @@ import { MEMO_NUMBER, MEMO_SYMBOL_STRING, SERVER_CONTEXT_SYMBOL_STRING, + LAZY_SYMBOL_STRING, } from '../shared/ReactSymbols'; import {enableStyleXFeatures} from 'react-devtools-feature-flags'; @@ -100,6 +124,8 @@ import {componentInfoToComponentLogsMap} from '../shared/DevToolsServerComponent import is from 'shared/objectIs'; import hasOwnProperty from 'shared/hasOwnProperty'; +import {getIODescription} from 'shared/ReactIODescription'; + import { getStackByFiberInDevAndProd, getOwnerStackByFiberInDev, @@ -134,6 +160,7 @@ import type { ReactRenderer, RendererInterface, SerializedElement, + SerializedAsyncInfo, WorkTagMap, CurrentDispatcherRef, LegacyDispatcherRef, @@ -144,7 +171,7 @@ import type { ElementType, Plugins, } from 'react-devtools-shared/src/frontend/types'; -import type {Source} from 'react-devtools-shared/src/shared/types'; +import type {ReactFunctionLocation} from 'shared/ReactTypes'; import {getSourceLocationByFiber} from './DevToolsFiberComponentStack'; import {formatOwnerStack} from '../shared/DevToolsOwnerStack'; @@ -161,9 +188,11 @@ type FiberInstance = { parent: null | DevToolsInstance, firstChild: null | DevToolsInstance, nextSibling: null | DevToolsInstance, - source: null | string | Error | Source, // source location of this component function, or owned child stack + source: null | string | Error | ReactFunctionLocation, // source location of this component function, or owned child stack logCount: number, // total number of errors/warnings last seen treeBaseDuration: number, // the profiled time of the last render of this subtree + suspendedBy: null | Array, // things that suspended in the children position of this component + suspenseNode: null | SuspenseNode, data: Fiber, // one of a Fiber pair }; @@ -177,6 +206,8 @@ function createFiberInstance(fiber: Fiber): FiberInstance { source: null, logCount: 0, treeBaseDuration: 0, + suspendedBy: null, + suspenseNode: null, data: fiber, }; } @@ -189,9 +220,11 @@ type FilteredFiberInstance = { parent: null | DevToolsInstance, firstChild: null | DevToolsInstance, nextSibling: null | DevToolsInstance, - source: null | string | Error | Source, // always null here. + source: null | string | Error | ReactFunctionLocation, // always null here. logCount: number, // total number of errors/warnings last seen treeBaseDuration: number, // the profiled time of the last render of this subtree + suspendedBy: null | Array, // only used at the root + suspenseNode: null | SuspenseNode, data: Fiber, // one of a Fiber pair }; @@ -206,6 +239,8 @@ function createFilteredFiberInstance(fiber: Fiber): FilteredFiberInstance { source: null, logCount: 0, treeBaseDuration: 0, + suspendedBy: null, + suspenseNode: null, data: fiber, }: any); } @@ -221,9 +256,11 @@ type VirtualInstance = { parent: null | DevToolsInstance, firstChild: null | DevToolsInstance, nextSibling: null | DevToolsInstance, - source: null | string | Error | Source, // source location of this server component, or owned child stack + source: null | string | Error | ReactFunctionLocation, // source location of this server component, or owned child stack logCount: number, // total number of errors/warnings last seen treeBaseDuration: number, // the profiled time of the last render of this subtree + suspendedBy: null | Array, // things that blocked the server component's child from rendering + suspenseNode: null, // The latest info for this instance. This can be updated over time and the // same info can appear in more than once ServerComponentInstance. data: ReactComponentInfo, @@ -241,12 +278,59 @@ function createVirtualInstance( source: null, logCount: 0, treeBaseDuration: 0, + suspendedBy: null, + suspenseNode: null, data: debugEntry, }; } type DevToolsInstance = FiberInstance | VirtualInstance | FilteredFiberInstance; +// A Generic Rect super type which can include DOMRect and other objects with similar shape like in React Native. +type Rect = {x: number, y: number, width: number, height: number, ...}; + +type SuspenseNode = { + // The Instance can be a Suspense boundary, a SuspenseList Row, or HostRoot. + // It can also be disconnected from the main tree if it's a Filtered Instance. + instance: FiberInstance | FilteredFiberInstance, + parent: null | SuspenseNode, + firstChild: null | SuspenseNode, + nextSibling: null | SuspenseNode, + rects: null | Array, // The bounding rects of content children. + suspendedBy: Map>, // Tracks which data we're suspended by and the children that suspend it. + // Track whether any of the items in suspendedBy are unique this this Suspense boundaries or if they're all + // also in the parent sets. This determine whether this could contribute in the loading sequence. + hasUniqueSuspenders: boolean, + // Track whether anything suspended in this boundary that we can't track either because it was using throw + // a promise, an older version of React or because we're inspecting prod. + hasUnknownSuspenders: boolean, +}; + +// Update flags need to be propagated up until the caller that put the corresponding +// node on the stack. +// If you push a new node, you need to handle ShouldResetChildren when you pop it. +// If you push a new Suspense node, you need to handle ShouldResetSuspenseChildren when you pop it. +type UpdateFlags = number; +const NoUpdate = /* */ 0b000; +const ShouldResetChildren = /* */ 0b001; +const ShouldResetSuspenseChildren = /* */ 0b010; +const ShouldResetParentSuspenseChildren = /* */ 0b100; + +function createSuspenseNode( + instance: FiberInstance | FilteredFiberInstance, +): SuspenseNode { + return (instance.suspenseNode = { + instance: instance, + parent: null, + firstChild: null, + nextSibling: null, + rects: null, + suspendedBy: new Map(), + hasUniqueSuspenders: false, + hasUnknownSuspenders: false, + }); +} + type getDisplayNameForFiberType = (fiber: Fiber) => string | null; type getTypeSymbolType = (type: any) => symbol | string | number; @@ -302,6 +386,7 @@ export function getInternalReactConstants(version: string): { ReactPriorityLevels: ReactPriorityLevelsType, ReactTypeOfWork: WorkTagMap, StrictModeBits: number, + SuspenseyImagesMode: number, } { // ********************************************************** // The section below is copied from files in React repo. @@ -342,6 +427,8 @@ export function getInternalReactConstants(version: string): { StrictModeBits = 0b10; } + const SuspenseyImagesMode = 0b0100000; + let ReactTypeOfWork: WorkTagMap = ((null: any): WorkTagMap); // ********************************************************** @@ -755,6 +842,7 @@ export function getInternalReactConstants(version: string): { ReactPriorityLevels, ReactTypeOfWork, StrictModeBits, + SuspenseyImagesMode, }; } @@ -769,8 +857,12 @@ const rootToFiberInstanceMap: Map = new Map(); // Map of id to FiberInstance or VirtualInstance. // This Map is used to e.g. get the display name for a Fiber or schedule an update, // operations that should be the same whether the current and work-in-progress Fiber is used. -const idToDevToolsInstanceMap: Map = - new Map(); +const idToDevToolsInstanceMap: Map< + FiberInstance['id'] | VirtualInstance['id'], + FiberInstance | VirtualInstance, +> = new Map(); + +const idToSuspenseNodeMap: Map = new Map(); // Map of canonical HostInstances to the nearest parent DevToolsInstance. const publicInstanceToDevToolsInstanceMap: Map = @@ -919,10 +1011,10 @@ export function attach( ReactPriorityLevels, ReactTypeOfWork, StrictModeBits, + SuspenseyImagesMode, } = getInternalReactConstants(version); const { ActivityComponent, - CacheComponent, ClassComponent, ContextConsumer, DehydratedSuspenseComponent, @@ -1225,9 +1317,9 @@ export function attach( if (componentLogsEntry === undefined) { componentLogsEntry = { errors: new Map(), - errorsCount: 0, + errorsCount: 0 as number, warnings: new Map(), - warningsCount: 0, + warningsCount: 0 as number, }; fiberToComponentLogsMap.set(fiber, componentLogsEntry); } @@ -1461,6 +1553,22 @@ export function attach( return Array.from(knownEnvironmentNames); } + function isFiberHydrated(fiber: Fiber): boolean { + if (OffscreenComponent === -1) { + throw new Error('not implemented for legacy suspense'); + } + switch (fiber.tag) { + case HostRoot: + const rootState = fiber.memoizedState; + return !rootState.isDehydrated; + case SuspenseComponent: + const suspenseState = fiber.memoizedState; + return suspenseState === null || suspenseState.dehydrated === null; + default: + throw new Error('not implemented for work tag ' + fiber.tag); + } + } + function shouldFilterVirtual( data: ReactComponentInfo, secondaryEnv: null | string, @@ -1906,11 +2014,12 @@ export function attach( }; const pendingOperations: OperationsArray = []; - const pendingRealUnmountedIDs: Array = []; + const pendingRealUnmountedIDs: Array = []; + const pendingRealUnmountedSuspenseIDs: Array = []; let pendingOperationsQueue: Array | null = []; const pendingStringTable: Map = new Map(); let pendingStringTableLength: number = 0; - let pendingUnmountedRootID: number | null = null; + let pendingUnmountedRootID: FiberInstance['id'] | null = null; function pushOperation(op: number): void { if (__DEV__) { @@ -1937,6 +2046,7 @@ export function attach( return ( pendingOperations.length === 0 && pendingRealUnmountedIDs.length === 0 && + pendingRealUnmountedSuspenseIDs.length === 0 && pendingUnmountedRootID === null ); } @@ -2002,6 +2112,7 @@ export function attach( const numUnmountIDs = pendingRealUnmountedIDs.length + (pendingUnmountedRootID === null ? 0 : 1); + const numUnmountSuspenseIDs = pendingRealUnmountedSuspenseIDs.length; const operations = new Array( // Identify which renderer this update is coming from. @@ -2010,6 +2121,9 @@ export function attach( 1 + // [stringTableLength] // Then goes the actual string table. pendingStringTableLength + + // All unmounts of Suspense boundaries are batched in a single message. + // [TREE_OPERATION_REMOVE_SUSPENSE, removedSuspenseIDLength, ...ids] + (numUnmountSuspenseIDs > 0 ? 2 + numUnmountSuspenseIDs : 0) + // All unmounts are batched in a single message. // [TREE_OPERATION_REMOVE, removedIDLength, ...ids] (numUnmountIDs > 0 ? 2 + numUnmountIDs : 0) + @@ -2047,6 +2161,19 @@ export function attach( i += length; }); + if (numUnmountSuspenseIDs > 0) { + // All unmounts of Suspense boundaries are batched in a single message. + operations[i++] = SUSPENSE_TREE_OPERATION_REMOVE; + // The first number is how many unmounted IDs we're gonna send. + operations[i++] = numUnmountSuspenseIDs; + // Fill in the real unmounts in the reverse order. + // They were inserted parents-first by React, but we want children-first. + // So we traverse our array backwards. + for (let j = 0; j < pendingRealUnmountedSuspenseIDs.length; j++) { + operations[i++] = pendingRealUnmountedSuspenseIDs[j]; + } + } + if (numUnmountIDs > 0) { // All unmounts except roots are batched in a single message. operations[i++] = TREE_OPERATION_REMOVE; @@ -2076,11 +2203,75 @@ export function attach( // Reset all of the pending state now that we've told the frontend about it. pendingOperations.length = 0; pendingRealUnmountedIDs.length = 0; + pendingRealUnmountedSuspenseIDs.length = 0; pendingUnmountedRootID = null; pendingStringTable.clear(); pendingStringTableLength = 0; } + function measureHostInstance(instance: HostInstance): null | Array { + // Feature detect measurement capabilities of this environment. + // TODO: Consider making this capability injected by the ReactRenderer. + if (typeof instance !== 'object' || instance === null) { + return null; + } + if (typeof instance.getClientRects === 'function') { + // DOM + const result: Array = []; + const doc = instance.ownerDocument; + const win = doc && doc.defaultView; + const scrollX = win ? win.scrollX : 0; + const scrollY = win ? win.scrollY : 0; + const rects = instance.getClientRects(); + for (let i = 0; i < rects.length; i++) { + const rect = rects[i]; + result.push({ + x: rect.x + scrollX, + y: rect.y + scrollY, + width: rect.width, + height: rect.height, + }); + } + return result; + } + if (instance.canonical) { + // Native + const publicInstance = instance.canonical.publicInstance; + if (!publicInstance) { + // The publicInstance may not have been initialized yet if there was no ref on this node. + // We can't initialize it from any existing Hook but we could fallback to this async form: + // renderer.extraDevToolsConfig.getInspectorDataForInstance(instance).hierarchy[last].getInspectorData().measure(callback) + return null; + } + if (typeof publicInstance.getBoundingClientRect === 'function') { + // enableAccessToHostTreeInFabric / ReadOnlyElement + return [publicInstance.getBoundingClientRect()]; + } + if (typeof publicInstance.unstable_getBoundingClientRect === 'function') { + // ReactFabricHostComponent + return [publicInstance.unstable_getBoundingClientRect()]; + } + } + return null; + } + + function measureInstance(instance: DevToolsInstance): null | Array { + // Synchronously return the client rects of the Host instances directly inside this Instance. + const hostInstances = findAllCurrentHostInstances(instance); + let result: null | Array = null; + for (let i = 0; i < hostInstances.length; i++) { + const childResult = measureHostInstance(hostInstances[i]); + if (childResult !== null) { + if (result === null) { + result = childResult; + } else { + result = result.concat(childResult); + } + } + } + return result; + } + function getStringID(string: string | null): number { if (string === null) { return 0; @@ -2108,6 +2299,8 @@ export function attach( return id; } + let isInDisconnectedSubtree = false; + function recordMount( fiber: Fiber, parentInstance: DevToolsInstance | null, @@ -2125,14 +2318,29 @@ export function attach( } idToDevToolsInstanceMap.set(fiberInstance.id, fiberInstance); - const id = fiberInstance.id; - if (__DEBUG__) { debug('recordMount()', fiberInstance, parentInstance); } + recordReconnect(fiberInstance, parentInstance); + return fiberInstance; + } + + function recordReconnect( + fiberInstance: FiberInstance, + parentInstance: DevToolsInstance | null, + ): void { + if (isInDisconnectedSubtree) { + // We're disconnected. We'll reconnect a hidden mount after the parent reappears. + return; + } + const id = fiberInstance.id; + const fiber = fiberInstance.data; + const isProfilingSupported = fiber.hasOwnProperty('treeBaseDuration'); + const isRoot = fiber.tag === HostRoot; + if (isRoot) { const hasOwnerMetadata = fiber.hasOwnProperty('_debugOwner'); @@ -2158,6 +2366,7 @@ export function attach( !isProductionBuildOfRenderer && StrictModeBits !== 0 ? 1 : 0, ); pushOperation(hasOwnerMetadata ? 1 : 0); + pushOperation(supportsTogglingSuspense ? 1 : 0); if (isProfiling) { if (displayNamesByRootID !== null) { @@ -2189,13 +2398,17 @@ export function attach( // the debugStack will be a stack frame inside the ownerInstance's source. ownerInstance.source = fiber._debugStack; } + + let unfilteredParent = parentInstance; + while ( + unfilteredParent !== null && + unfilteredParent.kind === FILTERED_FIBER_INSTANCE + ) { + unfilteredParent = unfilteredParent.parent; + } + const ownerID = ownerInstance === null ? 0 : ownerInstance.id; - const parentID = parentInstance - ? parentInstance.kind === FILTERED_FIBER_INSTANCE - ? // A Filtered Fiber Instance will always have a Virtual Instance as a parent. - ((parentInstance.parent: any): VirtualInstance).id - : parentInstance.id - : 0; + const parentID = unfilteredParent === null ? 0 : unfilteredParent.id; const displayNameStringID = getStringID(displayName); @@ -2204,6 +2417,15 @@ export function attach( const keyString = key === null ? null : String(key); const keyStringID = getStringID(keyString); + const nameProp = + fiber.tag === SuspenseComponent + ? fiber.memoizedProps.name + : fiber.tag === ActivityComponent + ? fiber.memoizedProps.name + : null; + const namePropString = nameProp == null ? null : String(nameProp); + const namePropStringID = getStringID(namePropString); + pushOperation(TREE_OPERATION_ADD); pushOperation(id); pushOperation(elementType); @@ -2211,6 +2433,7 @@ export function attach( pushOperation(ownerID); pushOperation(displayNameStringID); pushOperation(keyStringID); + pushOperation(namePropStringID); // If this subtree has a new mode, let the frontend know. if ((fiber.mode & StrictModeBits) !== 0) { @@ -2240,7 +2463,6 @@ export function attach( if (isProfilingSupported) { recordProfilingDurations(fiberInstance, null); } - return fiberInstance; } function recordVirtualMount( @@ -2252,6 +2474,18 @@ export function attach( idToDevToolsInstanceMap.set(id, instance); + recordVirtualReconnect(instance, parentInstance, secondaryEnv); + } + + function recordVirtualReconnect( + instance: VirtualInstance, + parentInstance: DevToolsInstance | null, + secondaryEnv: null | string, + ): void { + if (isInDisconnectedSubtree) { + // We're disconnected. We'll reconnect a hidden mount after the parent reappears. + return; + } const componentInfo = instance.data; const key = @@ -2284,13 +2518,17 @@ export function attach( // the debugStack will be a stack frame inside the ownerInstance's source. ownerInstance.source = componentInfo.debugStack; } + + let unfilteredParent = parentInstance; + while ( + unfilteredParent !== null && + unfilteredParent.kind === FILTERED_FIBER_INSTANCE + ) { + unfilteredParent = unfilteredParent.parent; + } + const ownerID = ownerInstance === null ? 0 : ownerInstance.id; - const parentID = parentInstance - ? parentInstance.kind === FILTERED_FIBER_INSTANCE - ? // A Filtered Fiber Instance will always have a Virtual Instance as a parent. - ((parentInstance.parent: any): VirtualInstance).id - : parentInstance.id - : 0; + const parentID = unfilteredParent === null ? 0 : unfilteredParent.id; const displayNameStringID = getStringID(displayName); @@ -2298,6 +2536,9 @@ export function attach( // in such a way as to bypass the default stringification of the "key" property. const keyString = key === null ? null : String(key); const keyStringID = getStringID(keyString); + const namePropStringID = getStringID(null); + + const id = instance.id; pushOperation(TREE_OPERATION_ADD); pushOperation(id); @@ -2306,21 +2547,102 @@ export function attach( pushOperation(ownerID); pushOperation(displayNameStringID); pushOperation(keyStringID); + pushOperation(namePropStringID); const componentLogsEntry = componentInfoToComponentLogsMap.get(componentInfo); recordConsoleLogs(instance, componentLogsEntry); } - function recordUnmount(fiberInstance: FiberInstance): void { + function recordSuspenseMount( + suspenseInstance: SuspenseNode, + parentSuspenseInstance: SuspenseNode | null, + ): void { + const fiberInstance = suspenseInstance.instance; + if (fiberInstance.kind === FILTERED_FIBER_INSTANCE) { + throw new Error('Cannot record a mount for a filtered Fiber instance.'); + } + const fiberID = fiberInstance.id; + + let unfilteredParent = parentSuspenseInstance; + while ( + unfilteredParent !== null && + unfilteredParent.instance.kind === FILTERED_FIBER_INSTANCE + ) { + unfilteredParent = unfilteredParent.parent; + } + const unfilteredParentInstance = + unfilteredParent !== null ? unfilteredParent.instance : null; + if ( + unfilteredParentInstance !== null && + unfilteredParentInstance.kind === FILTERED_FIBER_INSTANCE + ) { + throw new Error( + 'Should not have a filtered instance at this point. This is a bug.', + ); + } + const parentID = + unfilteredParentInstance === null ? 0 : unfilteredParentInstance.id; + const fiber = fiberInstance.data; + const props = fiber.memoizedProps; + // TODO: Compute a fallback name based on Owner, key etc. + const name = props === null ? null : props.name || null; + const nameStringID = getStringID(name); + + if (__DEBUG__) { + console.log('recordSuspenseMount()', suspenseInstance); + } + + idToSuspenseNodeMap.set(fiberID, suspenseInstance); + + pushOperation(SUSPENSE_TREE_OPERATION_ADD); + pushOperation(fiberID); + pushOperation(parentID); + pushOperation(nameStringID); + + const rects = suspenseInstance.rects; + if (rects === null) { + pushOperation(-1); + } else { + pushOperation(rects.length); + for (let i = 0; i < rects.length; ++i) { + const rect = rects[i]; + pushOperation(Math.round(rect.x)); + pushOperation(Math.round(rect.y)); + pushOperation(Math.round(rect.width)); + pushOperation(Math.round(rect.height)); + } + } + } + + function recordUnmount(fiberInstance: FiberInstance): void { if (__DEBUG__) { debug('recordUnmount()', fiberInstance, reconcilingParent); } + recordDisconnect(fiberInstance); + + const suspenseNode = fiberInstance.suspenseNode; + if (suspenseNode !== null) { + recordSuspenseUnmount(suspenseNode); + } + + idToDevToolsInstanceMap.delete(fiberInstance.id); + + untrackFiber(fiberInstance, fiberInstance.data); + } + + function recordDisconnect(fiberInstance: FiberInstance): void { + if (isInDisconnectedSubtree) { + // Already disconnected. + return; + } + const fiber = fiberInstance.data; + if (trackedPathMatchInstance === fiberInstance) { // We're in the process of trying to restore previous selection. - // If this fiber matched but is being unmounted, there's no use trying. + // If this fiber matched but is being hidden, there's no use trying. // Reset the state so we don't keep holding onto it. setTrackedPath(null); } @@ -2337,10 +2659,57 @@ export function attach( // and later arrange them in the correct order. pendingRealUnmountedIDs.push(id); } + } - idToDevToolsInstanceMap.delete(fiberInstance.id); + function recordSuspenseResize(suspenseNode: SuspenseNode): void { + if (__DEBUG__) { + console.log('recordSuspenseResize()', suspenseNode); + } + const fiberInstance = suspenseNode.instance; + if (fiberInstance.kind !== FIBER_INSTANCE) { + // TODO: Resizes of filtered Suspense nodes are currently dropped. + return; + } - untrackFiber(fiberInstance, fiber); + pushOperation(SUSPENSE_TREE_OPERATION_RESIZE); + pushOperation(fiberInstance.id); + const rects = suspenseNode.rects; + if (rects === null) { + pushOperation(-1); + } else { + pushOperation(rects.length); + for (let i = 0; i < rects.length; ++i) { + const rect = rects[i]; + pushOperation(Math.round(rect.x)); + pushOperation(Math.round(rect.y)); + pushOperation(Math.round(rect.width)); + pushOperation(Math.round(rect.height)); + } + } + } + + function recordSuspenseUnmount(suspenseInstance: SuspenseNode): void { + if (__DEBUG__) { + console.log( + 'recordSuspenseUnmount()', + suspenseInstance, + reconcilingParentSuspenseNode, + ); + } + + const devtoolsInstance = suspenseInstance.instance; + if (devtoolsInstance.kind !== FIBER_INSTANCE) { + throw new Error("Can't unmount a filtered SuspenseNode. This is a bug."); + } + const fiberInstance = devtoolsInstance; + const id = fiberInstance.id; + + // To maintain child-first ordering, + // we'll push it into one of these queues, + // and later arrange them in the correct order. + pendingRealUnmountedSuspenseIDs.push(id); + + idToSuspenseNodeMap.delete(id); } // Running state of the remaining children from the previous version of this parent that @@ -2353,6 +2722,167 @@ export function attach( // the current parent here as well. let reconcilingParent: null | DevToolsInstance = null; + let remainingReconcilingChildrenSuspenseNodes: null | SuspenseNode = null; + // The previously placed child. + let previouslyReconciledSiblingSuspenseNode: null | SuspenseNode = null; + // To save on stack allocation and ensure that they are updated as a pair, we also store + // the current parent here as well. + let reconcilingParentSuspenseNode: null | SuspenseNode = null; + + function ioExistsInSuspenseAncestor( + suspenseNode: SuspenseNode, + ioInfo: ReactIOInfo, + ): boolean { + let ancestor = suspenseNode.parent; + while (ancestor !== null) { + if (ancestor.suspendedBy.has(ioInfo)) { + return true; + } + ancestor = ancestor.parent; + } + return false; + } + + function insertSuspendedBy(asyncInfo: ReactAsyncInfo): void { + if (reconcilingParent === null || reconcilingParentSuspenseNode === null) { + throw new Error( + 'It should not be possible to have suspended data outside the root. ' + + 'Even suspending at the first position is still a child of the root.', + ); + } + const parentSuspenseNode = reconcilingParentSuspenseNode; + // Use the nearest unfiltered parent so that there's always some component that has + // the entry on it even if you filter, or the root if all are filtered. + let parentInstance = reconcilingParent; + while ( + parentInstance.kind === FILTERED_FIBER_INSTANCE && + parentInstance.parent !== null + ) { + parentInstance = parentInstance.parent; + } + + const suspenseNodeSuspendedBy = parentSuspenseNode.suspendedBy; + const ioInfo = asyncInfo.awaited; + let suspendedBySet = suspenseNodeSuspendedBy.get(ioInfo); + if (suspendedBySet === undefined) { + suspendedBySet = new Set(); + suspenseNodeSuspendedBy.set(asyncInfo.awaited, suspendedBySet); + } + // The child of the Suspense boundary that was suspended on this, or null if suspended at the root. + // This is used to keep track of how many dependents are still alive and also to get information + // like owner instances to link down into the tree. + if (!suspendedBySet.has(parentInstance)) { + suspendedBySet.add(parentInstance); + if ( + !parentSuspenseNode.hasUniqueSuspenders && + !ioExistsInSuspenseAncestor(parentSuspenseNode, ioInfo) + ) { + // This didn't exist in the parent before, so let's mark this boundary as having a unique suspender. + parentSuspenseNode.hasUniqueSuspenders = true; + } + } + // We have observed at least one known reason this might have been suspended. + parentSuspenseNode.hasUnknownSuspenders = false; + // Suspending right below the root is not attributed to any particular component in UI + // other than the SuspenseNode and the HostRoot's FiberInstance. + const suspendedBy = parentInstance.suspendedBy; + if (suspendedBy === null) { + parentInstance.suspendedBy = [asyncInfo]; + } else if (suspendedBy.indexOf(asyncInfo) === -1) { + suspendedBy.push(asyncInfo); + } + } + + function getAwaitInSuspendedByFromIO( + suspensedBy: Array, + ioInfo: ReactIOInfo, + ): null | ReactAsyncInfo { + for (let i = 0; i < suspensedBy.length; i++) { + const asyncInfo = suspensedBy[i]; + if (asyncInfo.awaited === ioInfo) { + return asyncInfo; + } + } + return null; + } + + function unblockSuspendedBy( + parentSuspenseNode: SuspenseNode, + ioInfo: ReactIOInfo, + ): void { + const firstChild = parentSuspenseNode.firstChild; + if (firstChild === null) { + return; + } + let node: SuspenseNode = firstChild; + while (node !== null) { + if (node.suspendedBy.has(ioInfo)) { + // We have found a child boundary that depended on the unblocked I/O. + // It can now be marked as having unique suspenders. We can skip its children + // since they'll still be blocked by this one. + node.hasUniqueSuspenders = true; + node.hasUnknownSuspenders = false; + } else if (node.firstChild !== null) { + node = node.firstChild; + continue; + } + while (node.nextSibling === null) { + if (node.parent === null || node.parent === parentSuspenseNode) { + return; + } + node = node.parent; + } + node = node.nextSibling; + } + } + + function removePreviousSuspendedBy( + instance: DevToolsInstance, + previousSuspendedBy: null | Array, + parentSuspenseNode: null | SuspenseNode, + ): void { + // Remove any async info from the parent, if they were in the previous set but + // is no longer in the new set. + if (previousSuspendedBy !== null && parentSuspenseNode !== null) { + const nextSuspendedBy = instance.suspendedBy; + for (let i = 0; i < previousSuspendedBy.length; i++) { + const asyncInfo = previousSuspendedBy[i]; + if ( + nextSuspendedBy === null || + (nextSuspendedBy.indexOf(asyncInfo) === -1 && + getAwaitInSuspendedByFromIO(nextSuspendedBy, asyncInfo.awaited) === + null) + ) { + // This IO entry is no longer blocking the current tree. + // Let's remove it from the parent SuspenseNode. + const ioInfo = asyncInfo.awaited; + const suspendedBySet = parentSuspenseNode.suspendedBy.get(ioInfo); + if ( + suspendedBySet === undefined || + !suspendedBySet.delete(instance) + ) { + throw new Error( + 'We are cleaning up async info that was not on the parent Suspense boundary. ' + + 'This is a bug in React.', + ); + } + if (suspendedBySet.size === 0) { + parentSuspenseNode.suspendedBy.delete(asyncInfo.awaited); + } + if ( + parentSuspenseNode.hasUniqueSuspenders && + !ioExistsInSuspenseAncestor(parentSuspenseNode, ioInfo) + ) { + // This entry wasn't in any ancestor and is no longer in this suspense boundary. + // This means that a child might now be the unique suspender for this IO. + // Search the child boundaries to see if we can reveal any of them. + unblockSuspendedBy(parentSuspenseNode, ioInfo); + } + } + } + } + } + function insertChild(instance: DevToolsInstance): void { const parentInstance = reconcilingParent; if (parentInstance === null) { @@ -2369,6 +2899,22 @@ export function attach( previouslyReconciledSibling = instance; } instance.nextSibling = null; + // Insert any SuspenseNode into its parent Node. + const suspenseNode = instance.suspenseNode; + if (suspenseNode !== null) { + const parentNode = reconcilingParentSuspenseNode; + if (parentNode !== null) { + suspenseNode.parent = parentNode; + if (previouslyReconciledSiblingSuspenseNode === null) { + previouslyReconciledSiblingSuspenseNode = suspenseNode; + parentNode.firstChild = suspenseNode; + } else { + previouslyReconciledSiblingSuspenseNode.nextSibling = suspenseNode; + previouslyReconciledSiblingSuspenseNode = suspenseNode; + } + suspenseNode.nextSibling = null; + } + } } function moveChild( @@ -2418,40 +2964,237 @@ export function attach( } instance.nextSibling = null; instance.parent = null; + + // Remove any SuspenseNode from its parent. + const suspenseNode = instance.suspenseNode; + if (suspenseNode !== null && suspenseNode.parent !== null) { + const parentNode = reconcilingParentSuspenseNode; + if (parentNode === null) { + throw new Error('Should not have a parent if we are at the root'); + } + if (suspenseNode.parent !== parentNode) { + throw new Error( + 'Cannot remove a Suspense node from a different parent than is being reconciled.', + ); + } + let previousSuspenseSibling = remainingReconcilingChildrenSuspenseNodes; + if (previousSuspenseSibling === suspenseNode) { + // We're first in the remaining set. Remove us. + remainingReconcilingChildrenSuspenseNodes = suspenseNode.nextSibling; + } else { + // Search for our previous sibling and remove us. + while (previousSuspenseSibling !== null) { + if (previousSuspenseSibling.nextSibling === suspenseNode) { + previousSuspenseSibling.nextSibling = suspenseNode.nextSibling; + break; + } + previousSuspenseSibling = previousSuspenseSibling.nextSibling; + } + } + suspenseNode.nextSibling = null; + suspenseNode.parent = null; + } } function unmountRemainingChildren() { - let child = remainingReconcilingChildren; - while (child !== null) { - unmountInstanceRecursively(child); - child = remainingReconcilingChildren; + if ( + reconcilingParent !== null && + (reconcilingParent.kind === FIBER_INSTANCE || + reconcilingParent.kind === FILTERED_FIBER_INSTANCE) && + reconcilingParent.data.tag === OffscreenComponent && + reconcilingParent.data.memoizedState !== null && + !isInDisconnectedSubtree + ) { + // This is a hidden offscreen, we need to execute this in the context of a disconnected subtree. + isInDisconnectedSubtree = true; + try { + let child = remainingReconcilingChildren; + while (child !== null) { + unmountInstanceRecursively(child); + child = remainingReconcilingChildren; + } + } finally { + isInDisconnectedSubtree = false; + } + } else { + let child = remainingReconcilingChildren; + while (child !== null) { + unmountInstanceRecursively(child); + child = remainingReconcilingChildren; + } } } - function mountVirtualInstanceRecursively( - virtualInstance: VirtualInstance, - firstChild: Fiber, - lastChild: null | Fiber, // non-inclusive - traceNearestHostComponentUpdate: boolean, - virtualLevel: number, // the nth level of virtual instances - ): void { - // If we have the tree selection from previous reload, try to match this Instance. - // Also remember whether to do the same for siblings. - const mightSiblingsBeOnTrackedPath = - updateVirtualTrackedPathStateBeforeMount( - virtualInstance, - reconcilingParent, - ); + function isChildOf( + parentInstance: DevToolsInstance, + childInstance: DevToolsInstance, + grandParent: DevToolsInstance, + ): boolean { + let instance = childInstance.parent; + while (instance !== null) { + if (parentInstance === instance) { + return true; + } + if (instance === parentInstance.parent || instance === grandParent) { + // This was a sibling but not inside the FiberInstance. We can bail out. + break; + } + instance = instance.parent; + } + return false; + } - const stashedParent = reconcilingParent; - const stashedPrevious = previouslyReconciledSibling; - const stashedRemaining = remainingReconcilingChildren; - // Push a new DevTools instance parent while reconciling this subtree. - reconcilingParent = virtualInstance; - previouslyReconciledSibling = null; - remainingReconcilingChildren = null; - try { - mountVirtualChildrenRecursively( + function areEqualRects( + a: null | Array, + b: null | Array, + ): boolean { + if (a === null) { + return b === null; + } + if (b === null) { + return false; + } + if (a.length !== b.length) { + return false; + } + for (let i = 0; i < a.length; i++) { + const aRect = a[i]; + const bRect = b[i]; + if ( + aRect.x !== bRect.x || + aRect.y !== bRect.y || + aRect.width !== bRect.width || + aRect.height !== bRect.height + ) { + return false; + } + } + return true; + } + + function measureUnchangedSuspenseNodesRecursively( + suspenseNode: SuspenseNode, + ): void { + if (isInDisconnectedSubtree) { + // We don't update rects inside disconnected subtrees. + return; + } + const nextRects = measureInstance(suspenseNode.instance); + const prevRects = suspenseNode.rects; + if (areEqualRects(prevRects, nextRects)) { + return; // Unchanged + } + // The rect has changed. While the bailed out root wasn't in a disconnected subtree, + // it's possible that this node was in one. So we need to check if we're offscreen. + let parent = suspenseNode.instance.parent; + while (parent !== null) { + if ( + (parent.kind === FIBER_INSTANCE || + parent.kind === FILTERED_FIBER_INSTANCE) && + parent.data.tag === OffscreenComponent && + parent.data.memoizedState !== null + ) { + // We're inside a hidden offscreen Fiber. We're in a disconnected tree. + return; + } + if (parent.suspenseNode !== null) { + // Found our parent SuspenseNode. We can bail out now. + break; + } + parent = parent.parent; + } + // We changed inside a visible tree. + // Since this boundary changed, it's possible it also affected its children so lets + // measure them as well. + for ( + let child = suspenseNode.firstChild; + child !== null; + child = child.nextSibling + ) { + measureUnchangedSuspenseNodesRecursively(child); + } + suspenseNode.rects = nextRects; + recordSuspenseResize(suspenseNode); + } + + function consumeSuspenseNodesOfExistingInstance( + instance: DevToolsInstance, + ): void { + // We need to also consume any unchanged Suspense boundaries. + let suspenseNode = remainingReconcilingChildrenSuspenseNodes; + if (suspenseNode === null) { + return; + } + const parentSuspenseNode = reconcilingParentSuspenseNode; + if (parentSuspenseNode === null) { + throw new Error( + 'The should not be any remaining suspense node children if there is no parent.', + ); + } + let foundOne = false; + let previousSkippedSibling = null; + while (suspenseNode !== null) { + // Check if this SuspenseNode was a child of the bailed out FiberInstance. + if ( + isChildOf(instance, suspenseNode.instance, parentSuspenseNode.instance) + ) { + foundOne = true; + // The suspenseNode was child of the bailed out Fiber. + // First, remove it from the remaining children set. + const nextRemainingSibling = suspenseNode.nextSibling; + if (previousSkippedSibling === null) { + remainingReconcilingChildrenSuspenseNodes = nextRemainingSibling; + } else { + previousSkippedSibling.nextSibling = nextRemainingSibling; + } + suspenseNode.nextSibling = null; + // Then, re-insert it into the newly reconciled set. + if (previouslyReconciledSiblingSuspenseNode === null) { + parentSuspenseNode.firstChild = suspenseNode; + } else { + previouslyReconciledSiblingSuspenseNode.nextSibling = suspenseNode; + } + previouslyReconciledSiblingSuspenseNode = suspenseNode; + // While React didn't rerender this node, it's possible that it was affected by + // layout due to mutation of a parent or sibling. Check if it changed size. + measureUnchangedSuspenseNodesRecursively(suspenseNode); + // Continue + suspenseNode = nextRemainingSibling; + } else if (foundOne) { + // If we found one and then hit a miss, we assume that we're passed the sequence because + // they should've all been consecutive. + break; + } else { + previousSkippedSibling = suspenseNode; + suspenseNode = suspenseNode.nextSibling; + } + } + } + + function mountVirtualInstanceRecursively( + virtualInstance: VirtualInstance, + firstChild: Fiber, + lastChild: null | Fiber, // non-inclusive + traceNearestHostComponentUpdate: boolean, + virtualLevel: number, // the nth level of virtual instances + ): void { + // If we have the tree selection from previous reload, try to match this Instance. + // Also remember whether to do the same for siblings. + const mightSiblingsBeOnTrackedPath = + updateVirtualTrackedPathStateBeforeMount( + virtualInstance, + reconcilingParent, + ); + + const stashedParent = reconcilingParent; + const stashedPrevious = previouslyReconciledSibling; + const stashedRemaining = remainingReconcilingChildren; + // Push a new DevTools instance parent while reconciling this subtree. + reconcilingParent = virtualInstance; + previouslyReconciledSibling = null; + remainingReconcilingChildren = null; + try { + mountVirtualChildrenRecursively( firstChild, lastChild, traceNearestHostComponentUpdate, @@ -2468,6 +3211,14 @@ export function attach( } function recordVirtualUnmount(instance: VirtualInstance) { + recordVirtualDisconnect(instance); + idToDevToolsInstanceMap.delete(instance.id); + } + + function recordVirtualDisconnect(instance: VirtualInstance) { + if (isInDisconnectedSubtree) { + return; + } if (trackedPathMatchInstance === instance) { // We're in the process of trying to restore previous selection. // If this fiber matched but is being unmounted, there's no use trying. @@ -2498,6 +3249,287 @@ export function attach( return null; } + function trackDebugInfoFromLazyType(fiber: Fiber): void { + // The debugInfo from a Lazy isn't propagated onto _debugInfo of the parent Fiber the way + // it is when used in child position. So we need to pick it up explicitly. + const type = fiber.elementType; + const typeSymbol = getTypeSymbol(type); // The elementType might be have been a LazyComponent. + if (typeSymbol === LAZY_SYMBOL_STRING) { + const debugInfo: ?ReactDebugInfo = type._debugInfo; + if (debugInfo) { + for (let i = 0; i < debugInfo.length; i++) { + const debugEntry = debugInfo[i]; + if (debugEntry.awaited) { + const asyncInfo: ReactAsyncInfo = (debugEntry: any); + insertSuspendedBy(asyncInfo); + } + } + } + } + } + + function trackDebugInfoFromUsedThenables(fiber: Fiber): void { + // If a Fiber called use() in DEV mode then we may have collected _debugThenableState on + // the dependencies. If so, then this will contain the thenables passed to use(). + // These won't have their debug info picked up by fiber._debugInfo since that just + // contains things suspending the children. We have to collect use() separately. + const dependencies = fiber.dependencies; + if (dependencies == null) { + return; + } + const thenableState = dependencies._debugThenableState; + if (thenableState == null) { + return; + } + // In DEV the thenableState is an inner object. + const usedThenables: any = thenableState.thenables || thenableState; + if (!Array.isArray(usedThenables)) { + return; + } + for (let i = 0; i < usedThenables.length; i++) { + const thenable: Thenable = usedThenables[i]; + const debugInfo = thenable._debugInfo; + if (debugInfo) { + for (let j = 0; j < debugInfo.length; j++) { + const debugEntry = debugInfo[j]; + if (debugEntry.awaited) { + const asyncInfo: ReactAsyncInfo = (debugEntry: any); + insertSuspendedBy(asyncInfo); + } + } + } + } + } + + const hostAsyncInfoCache: WeakMap<{...}, ReactAsyncInfo> = new WeakMap(); + + function trackDebugInfoFromHostResource( + devtoolsInstance: DevToolsInstance, + fiber: Fiber, + ): void { + const resource: ?{ + type: 'stylesheet' | 'style' | 'script' | 'void', + instance?: null | HostInstance, + ... + } = fiber.memoizedState; + if (resource == null) { + return; + } + + // Use a cached entry based on the resource. This ensures that if we use the same + // resource in multiple places, it gets deduped and inner boundaries don't consider it + // as contributing to those boundaries. + const existingEntry = hostAsyncInfoCache.get(resource); + if (existingEntry !== undefined) { + insertSuspendedBy(existingEntry); + return; + } + + const props: { + href?: string, + media?: string, + ... + } = fiber.memoizedProps; + + // Stylesheet resources may suspend. We need to track that. + const mayResourceSuspendCommit = + resource.type === 'stylesheet' && + // If it doesn't match the currently debugged media, then it doesn't count. + (typeof props.media !== 'string' || + typeof matchMedia !== 'function' || + matchMedia(props.media)); + if (!mayResourceSuspendCommit) { + return; + } + + const instance = resource.instance; + if (instance == null) { + return; + } + + // Unlike props.href, this href will be fully qualified which we need for comparison below. + const href = instance.href; + if (typeof href !== 'string') { + return; + } + let start = -1; + let end = -1; + let byteSize = 0; + // $FlowFixMe[method-unbinding] + if (typeof performance.getEntriesByType === 'function') { + // We may be able to collect the start and end time of this resource from Performance Observer. + const resourceEntries = performance.getEntriesByType('resource'); + for (let i = 0; i < resourceEntries.length; i++) { + const resourceEntry = resourceEntries[i]; + if (resourceEntry.name === href) { + start = resourceEntry.startTime; + end = start + resourceEntry.duration; + // $FlowFixMe[prop-missing] + byteSize = (resourceEntry.transferSize: any) || 0; + } + } + } + const value = instance.sheet; + const promise = Promise.resolve(value); + (promise: any).status = 'fulfilled'; + (promise: any).value = value; + const ioInfo: ReactIOInfo = { + name: 'stylesheet', + start, + end, + value: promise, + // $FlowFixMe: This field doesn't usually take a Fiber but we're only using inside this file. + owner: fiber, // Allow linking to the if it's not filtered. + }; + if (byteSize > 0) { + // $FlowFixMe[cannot-write] + ioInfo.byteSize = byteSize; + } + const asyncInfo: ReactAsyncInfo = { + awaited: ioInfo, + // $FlowFixMe: This field doesn't usually take a Fiber but we're only using inside this file. + owner: fiber._debugOwner == null ? null : fiber._debugOwner, + debugStack: fiber._debugStack == null ? null : fiber._debugStack, + debugTask: fiber._debugTask == null ? null : fiber._debugTask, + }; + hostAsyncInfoCache.set(resource, asyncInfo); + insertSuspendedBy(asyncInfo); + } + + function trackDebugInfoFromHostComponent( + devtoolsInstance: DevToolsInstance, + fiber: Fiber, + ): void { + if (fiber.tag !== HostComponent) { + return; + } + if ((fiber.mode & SuspenseyImagesMode) === 0) { + // In any released version, Suspensey Images are only enabled inside a ViewTransition + // subtree, which is enabled by the SuspenseyImagesMode. + // TODO: If we ever enable the enableSuspenseyImages flag then it would be enabled for + // all images and we'd need some other check for if the version of React has that enabled. + return; + } + + const type = fiber.type; + const props: { + src?: string, + onLoad?: (event: any) => void, + loading?: 'eager' | 'lazy', + ... + } = fiber.memoizedProps; + + const maySuspendCommit = + type === 'img' && + props.src != null && + props.src !== '' && + props.onLoad == null && + props.loading !== 'lazy'; + + // Note: We don't track "maySuspendCommitOnUpdate" separately because it doesn't matter if + // it didn't suspend this particular update if it would've suspended if it mounted in this + // state, since we're tracking the dependencies inside the current state. + + if (!maySuspendCommit) { + return; + } + + const instance = fiber.stateNode; + if (instance == null) { + // Should never happen. + return; + } + + // Unlike props.src, currentSrc will be fully qualified which we need for comparison below. + // Unlike instance.src it will be resolved into the media queries currently matching which is + // the state we're inspecting. + const src = instance.currentSrc; + if (typeof src !== 'string' || src === '') { + return; + } + let start = -1; + let end = -1; + let byteSize = 0; + let fileSize = 0; + // $FlowFixMe[method-unbinding] + if (typeof performance.getEntriesByType === 'function') { + // We may be able to collect the start and end time of this resource from Performance Observer. + const resourceEntries = performance.getEntriesByType('resource'); + for (let i = 0; i < resourceEntries.length; i++) { + const resourceEntry = resourceEntries[i]; + if (resourceEntry.name === src) { + start = resourceEntry.startTime; + end = start + resourceEntry.duration; + // $FlowFixMe[prop-missing] + fileSize = (resourceEntry.decodedBodySize: any) || 0; + // $FlowFixMe[prop-missing] + byteSize = (resourceEntry.transferSize: any) || 0; + } + } + } + // A representation of the image data itself. + // TODO: We could render a little preview in the front end from the resource API. + const value: { + currentSrc: string, + naturalWidth?: number, + naturalHeight?: number, + fileSize?: number, + } = { + currentSrc: src, + }; + if (instance.naturalWidth > 0 && instance.naturalHeight > 0) { + // The intrinsic size of the file value itself, if it's loaded + value.naturalWidth = instance.naturalWidth; + value.naturalHeight = instance.naturalHeight; + } + if (fileSize > 0) { + // Cross-origin images won't have a file size that we can access. + value.fileSize = fileSize; + } + const promise = Promise.resolve(value); + (promise: any).status = 'fulfilled'; + (promise: any).value = value; + const ioInfo: ReactIOInfo = { + name: 'img', + start, + end, + value: promise, + // $FlowFixMe: This field doesn't usually take a Fiber but we're only using inside this file. + owner: fiber, // Allow linking to the if it's not filtered. + }; + if (byteSize > 0) { + // $FlowFixMe[cannot-write] + ioInfo.byteSize = byteSize; + } + const asyncInfo: ReactAsyncInfo = { + awaited: ioInfo, + // $FlowFixMe: This field doesn't usually take a Fiber but we're only using inside this file. + owner: fiber._debugOwner == null ? null : fiber._debugOwner, + debugStack: fiber._debugStack == null ? null : fiber._debugStack, + debugTask: fiber._debugTask == null ? null : fiber._debugTask, + }; + insertSuspendedBy(asyncInfo); + } + + function trackThrownPromisesFromRetryCache( + suspenseNode: SuspenseNode, + retryCache: ?WeakSet, + ): void { + if (retryCache != null) { + // If a Suspense boundary ever committed in fallback state with a retryCache, that + // suggests that something unique to that boundary was suspensey since otherwise + // it wouldn't have thrown and so never created the retryCache. + // Unfortunately if we don't have any DEV time debug info or debug thenables then + // we have no meta data to show. However, we still mark this Suspense boundary as + // participating in the loading sequence since apparently it can suspend. + suspenseNode.hasUniqueSuspenders = true; + // We have not seen any reason yet for why this suspense node might have been + // suspended but it clearly has been at some point. If we later discover a reason + // we'll clear this flag again. + suspenseNode.hasUnknownSuspenders = true; + } + } + function mountVirtualChildrenRecursively( firstChild: Fiber, lastChild: null | Fiber, // non-inclusive @@ -2514,6 +3546,17 @@ export function attach( if (fiber._debugInfo) { for (let i = 0; i < fiber._debugInfo.length; i++) { const debugEntry = fiber._debugInfo[i]; + if (debugEntry.awaited) { + // Async Info + const asyncInfo: ReactAsyncInfo = (debugEntry: any); + if (level === virtualLevel) { + // Track any async info between the previous virtual instance up until to this + // instance and add it to the parent. This can add the same set multiple times + // so we assume insertSuspendedBy dedupes. + insertSuspendedBy(asyncInfo); + } + continue; + } if (typeof debugEntry.name !== 'string') { // Not a Component. Some other Debug Info. continue; @@ -2608,26 +3651,96 @@ export function attach( ); } + function mountSuspenseChildrenRecursively( + contentFiber: Fiber, + traceNearestHostComponentUpdate: boolean, + stashedSuspenseParent: SuspenseNode | null, + stashedSuspensePrevious: SuspenseNode | null, + stashedSuspenseRemaining: SuspenseNode | null, + ) { + const fallbackFiber = contentFiber.sibling; + + // First update only the Offscreen boundary. I.e. the main content. + mountVirtualChildrenRecursively( + contentFiber, + fallbackFiber, + traceNearestHostComponentUpdate, + 0, // first level + ); + + // Next, we'll pop back out of the SuspenseNode that we added above and now we'll + // reconcile the fallback, reconciling anything by inserting into the parent SuspenseNode. + // Since the fallback conceptually blocks the parent. + reconcilingParentSuspenseNode = stashedSuspenseParent; + previouslyReconciledSiblingSuspenseNode = stashedSuspensePrevious; + remainingReconcilingChildrenSuspenseNodes = stashedSuspenseRemaining; + if (fallbackFiber !== null) { + mountVirtualChildrenRecursively( + fallbackFiber, + null, + traceNearestHostComponentUpdate, + 0, // first level + ); + } + } + function mountFiberRecursively( fiber: Fiber, traceNearestHostComponentUpdate: boolean, ): void { const shouldIncludeInTree = !shouldFilterFiber(fiber); let newInstance = null; + let newSuspenseNode = null; if (shouldIncludeInTree) { newInstance = recordMount(fiber, reconcilingParent); + if (fiber.tag === SuspenseComponent || fiber.tag === HostRoot) { + newSuspenseNode = createSuspenseNode(newInstance); + // Measure this Suspense node. In general we shouldn't do this until we have + // inserted the new children but since we know this is a FiberInstance we'll + // just use the Fiber anyway. + // Fallbacks get attributed to the parent so we only measure if we're + // showing primary content. + if (OffscreenComponent === -1) { + const isTimedOut = fiber.memoizedState !== null; + if (!isTimedOut) { + newSuspenseNode.rects = measureInstance(newInstance); + } + } else { + const hydrated = isFiberHydrated(fiber); + if (hydrated) { + const contentFiber = fiber.child; + if (contentFiber === null) { + throw new Error( + 'There should always be an Offscreen Fiber child in a hydrated Suspense boundary.', + ); + } + } else { + // This Suspense Fiber is still dehydrated. It won't have any children + // until hydration. + } + const isTimedOut = fiber.memoizedState !== null; + if (!isTimedOut) { + newSuspenseNode.rects = measureInstance(newInstance); + } + } + recordSuspenseMount(newSuspenseNode, reconcilingParentSuspenseNode); + } insertChild(newInstance); if (__DEBUG__) { debug('mountFiberRecursively()', newInstance, reconcilingParent); } } else if ( - reconcilingParent !== null && - reconcilingParent.kind === VIRTUAL_INSTANCE + (reconcilingParent !== null && + reconcilingParent.kind === VIRTUAL_INSTANCE) || + fiber.tag === SuspenseComponent || + fiber.tag === OffscreenComponent // Use to keep resuspended instances alive inside a SuspenseComponent. ) { // If the parent is a Virtual Instance and we filtered this Fiber we include a - // hidden node. - + // hidden node. We also include this if it's a Suspense boundary so we can track those + // in the Suspense tree. if ( + reconcilingParent !== null && + reconcilingParent.kind === VIRTUAL_INSTANCE && reconcilingParent.data === fiber._debugOwner && fiber._debugStack != null && reconcilingParent.source === null @@ -2638,6 +3751,38 @@ export function attach( } newInstance = createFilteredFiberInstance(fiber); + if (fiber.tag === SuspenseComponent) { + newSuspenseNode = createSuspenseNode(newInstance); + // Measure this Suspense node. In general we shouldn't do this until we have + // inserted the new children but since we know this is a FiberInstance we'll + // just use the Fiber anyway. + // Fallbacks get attributed to the parent so we only measure if we're + // showing primary content. + if (OffscreenComponent === -1) { + const isTimedOut = fiber.memoizedState !== null; + if (!isTimedOut) { + newSuspenseNode.rects = measureInstance(newInstance); + } + } else { + const hydrated = isFiberHydrated(fiber); + if (hydrated) { + const contentFiber = fiber.child; + if (contentFiber === null) { + throw new Error( + 'There should always be an Offscreen Fiber child in a hydrated Suspense boundary.', + ); + } + } else { + // This Suspense Fiber is still dehydrated. It won't have any children + // until hydration. + } + const suspenseState = fiber.memoizedState; + const isTimedOut = suspenseState !== null; + if (!isTimedOut) { + newSuspenseNode.rects = measureInstance(newInstance); + } + } + } insertChild(newInstance); if (__DEBUG__) { debug('mountFiberRecursively()', newInstance, reconcilingParent); @@ -2654,12 +3799,22 @@ export function attach( const stashedParent = reconcilingParent; const stashedPrevious = previouslyReconciledSibling; const stashedRemaining = remainingReconcilingChildren; + const stashedSuspenseParent = reconcilingParentSuspenseNode; + const stashedSuspensePrevious = previouslyReconciledSiblingSuspenseNode; + const stashedSuspenseRemaining = remainingReconcilingChildrenSuspenseNodes; if (newInstance !== null) { // Push a new DevTools instance parent while reconciling this subtree. reconcilingParent = newInstance; previouslyReconciledSibling = null; remainingReconcilingChildren = null; } + let shouldPopSuspenseNode = false; + if (newSuspenseNode !== null) { + reconcilingParentSuspenseNode = newSuspenseNode; + previouslyReconciledSiblingSuspenseNode = null; + remainingReconcilingChildrenSuspenseNodes = null; + shouldPopSuspenseNode = true; + } try { if (traceUpdatesEnabled) { if (traceNearestHostComponentUpdate) { @@ -2675,12 +3830,16 @@ export function attach( // because we don't want to highlight every host node inside of a newly mounted subtree. } + trackDebugInfoFromLazyType(fiber); + trackDebugInfoFromUsedThenables(fiber); + if (fiber.tag === HostHoistable) { const nearestInstance = reconcilingParent; if (nearestInstance === null) { throw new Error('Did not expect a host hoistable to be the root'); } aquireHostResource(nearestInstance, fiber.memoizedState); + trackDebugInfoFromHostResource(nearestInstance, fiber); } else if ( fiber.tag === HostComponent || fiber.tag === HostText || @@ -2691,9 +3850,26 @@ export function attach( throw new Error('Did not expect a host hoistable to be the root'); } aquireHostInstance(nearestInstance, fiber.stateNode); + trackDebugInfoFromHostComponent(nearestInstance, fiber); } - if (fiber.tag === SuspenseComponent) { + if (fiber.tag === OffscreenComponent && fiber.memoizedState !== null) { + // If an Offscreen component is hidden, mount its children as disconnected. + const stashedDisconnected = isInDisconnectedSubtree; + isInDisconnectedSubtree = true; + try { + if (fiber.child !== null) { + mountChildrenRecursively(fiber.child, false); + } + } finally { + isInDisconnectedSubtree = stashedDisconnected; + } + } else if (fiber.tag === SuspenseComponent && OffscreenComponent === -1) { + // Legacy Suspense without the Offscreen wrapper. For the modern Suspense we just handle the + // Offscreen wrapper itself specially. + if (newSuspenseNode !== null) { + trackThrownPromisesFromRetryCache(newSuspenseNode, fiber.stateNode); + } const isTimedOut = fiber.memoizedState !== null; if (isTimedOut) { // Special case: if Suspense mounts in a timed-out state, @@ -2713,16 +3889,9 @@ export function attach( ); } } + // TODO: Track SuspenseNode in resuspended trees. } else { - let primaryChild: Fiber | null = null; - const areSuspenseChildrenConditionallyWrapped = - OffscreenComponent === -1; - if (areSuspenseChildrenConditionallyWrapped) { - primaryChild = fiber.child; - } else if (fiber.child !== null) { - primaryChild = fiber.child.child; - updateTrackedPathStateBeforeMount(fiber.child, null); - } + const primaryChild: Fiber | null = fiber.child; if (primaryChild !== null) { mountChildrenRecursively( primaryChild, @@ -2730,6 +3899,37 @@ export function attach( ); } } + } else if ( + fiber.tag === SuspenseComponent && + OffscreenComponent !== -1 && + newInstance !== null && + newSuspenseNode !== null + ) { + // Modern Suspense path + const contentFiber = fiber.child; + const hydrated = isFiberHydrated(fiber); + if (hydrated) { + if (contentFiber === null) { + throw new Error( + 'There should always be an Offscreen Fiber child in a hydrated Suspense boundary.', + ); + } + + trackThrownPromisesFromRetryCache(newSuspenseNode, fiber.stateNode); + + mountSuspenseChildrenRecursively( + contentFiber, + traceNearestHostComponentUpdate, + stashedSuspenseParent, + stashedSuspensePrevious, + stashedSuspenseRemaining, + ); + // mountSuspenseChildrenRecursively popped already + shouldPopSuspenseNode = false; + } else { + // This Suspense Fiber is still dehydrated. It won't have any children + // until hydration. + } } else { if (fiber.child !== null) { mountChildrenRecursively( @@ -2744,6 +3944,11 @@ export function attach( previouslyReconciledSibling = stashedPrevious; remainingReconcilingChildren = stashedRemaining; } + if (shouldPopSuspenseNode) { + reconcilingParentSuspenseNode = stashedSuspenseParent; + previouslyReconciledSiblingSuspenseNode = stashedSuspensePrevious; + remainingReconcilingChildrenSuspenseNodes = stashedSuspenseRemaining; + } } // We're exiting this Fiber now, and entering its siblings. @@ -2761,19 +3966,42 @@ export function attach( const stashedParent = reconcilingParent; const stashedPrevious = previouslyReconciledSibling; const stashedRemaining = remainingReconcilingChildren; + const stashedSuspenseParent = reconcilingParentSuspenseNode; + const stashedSuspensePrevious = previouslyReconciledSiblingSuspenseNode; + const stashedSuspenseRemaining = remainingReconcilingChildrenSuspenseNodes; + const previousSuspendedBy = instance.suspendedBy; // Push a new DevTools instance parent while reconciling this subtree. reconcilingParent = instance; previouslyReconciledSibling = null; // Move all the children of this instance to the remaining set. remainingReconcilingChildren = instance.firstChild; instance.firstChild = null; + instance.suspendedBy = null; + + if (instance.suspenseNode !== null) { + reconcilingParentSuspenseNode = instance.suspenseNode; + previouslyReconciledSiblingSuspenseNode = null; + remainingReconcilingChildrenSuspenseNodes = + instance.suspenseNode.firstChild; + } + try { // Unmount the remaining set. unmountRemainingChildren(); + removePreviousSuspendedBy( + instance, + previousSuspendedBy, + reconcilingParentSuspenseNode, + ); } finally { reconcilingParent = stashedParent; previouslyReconciledSibling = stashedPrevious; remainingReconcilingChildren = stashedRemaining; + if (instance.suspenseNode !== null) { + reconcilingParentSuspenseNode = stashedSuspenseParent; + previouslyReconciledSiblingSuspenseNode = stashedSuspensePrevious; + remainingReconcilingChildrenSuspenseNodes = stashedSuspenseRemaining; + } } if (instance.kind === FIBER_INSTANCE) { recordUnmount(instance); @@ -2903,6 +4131,26 @@ export function attach( virtualInstance.treeBaseDuration = treeBaseDuration; } + function addUnfilteredChildrenIDs( + parentInstance: DevToolsInstance, + nextChildren: Array, + ): void { + let child: null | DevToolsInstance = parentInstance.firstChild; + while (child !== null) { + if (child.kind === FILTERED_FIBER_INSTANCE) { + const fiber = child.data; + if (fiber.tag === OffscreenComponent && fiber.memoizedState !== null) { + // The children of this Offscreen are hidden so they don't get added. + } else { + addUnfilteredChildrenIDs(child, nextChildren); + } + } else { + nextChildren.push(child.id); + } + child = child.nextSibling; + } + } + function recordResetChildren( parentInstance: FiberInstance | VirtualInstance, ) { @@ -2920,29 +4168,61 @@ export function attach( // This is trickier than a simple comparison though, since certain types of fibers are filtered. const nextChildren: Array = []; - let child: null | DevToolsInstance = parentInstance.firstChild; + addUnfilteredChildrenIDs(parentInstance, nextChildren); + + const numChildren = nextChildren.length; + if (numChildren < 2) { + // No need to reorder. + return; + } + pushOperation(TREE_OPERATION_REORDER_CHILDREN); + pushOperation(parentInstance.id); + pushOperation(numChildren); + for (let i = 0; i < nextChildren.length; i++) { + pushOperation(nextChildren[i]); + } + } + + function addUnfilteredSuspenseChildrenIDs( + parentInstance: SuspenseNode, + nextChildren: Array, + ): void { + let child: null | SuspenseNode = parentInstance.firstChild; while (child !== null) { - if (child.kind === FILTERED_FIBER_INSTANCE) { - for ( - let innerChild: null | DevToolsInstance = parentInstance.firstChild; - innerChild !== null; - innerChild = innerChild.nextSibling - ) { - nextChildren.push((innerChild: any).id); - } + if (child.instance.kind === FILTERED_FIBER_INSTANCE) { + addUnfilteredSuspenseChildrenIDs(child, nextChildren); } else { - nextChildren.push(child.id); + nextChildren.push(child.instance.id); } child = child.nextSibling; } + } + + function recordResetSuspenseChildren(parentInstance: SuspenseNode) { + if (__DEBUG__) { + if (parentInstance.firstChild !== null) { + console.log( + 'recordResetSuspenseChildren()', + parentInstance.firstChild, + parentInstance, + ); + } + } + // The frontend only really cares about the name, and children. + // The first two don't really change, so we are only concerned with the order of children here. + // This is trickier than a simple comparison though, since certain types of fibers are filtered. + const nextChildren: Array = []; + + addUnfilteredSuspenseChildrenIDs(parentInstance, nextChildren); const numChildren = nextChildren.length; if (numChildren < 2) { // No need to reorder. return; } - pushOperation(TREE_OPERATION_REORDER_CHILDREN); - pushOperation(parentInstance.id); + pushOperation(SUSPENSE_TREE_OPERATION_REORDER_CHILDREN); + // $FlowFixMe[incompatible-call] TODO: Allow filtering SuspenseNode + pushOperation(parentInstance.instance.id); pushOperation(numChildren); for (let i = 0; i < nextChildren.length; i++) { pushOperation(nextChildren[i]); @@ -2956,10 +4236,11 @@ export function attach( prevFirstChild: null | Fiber, traceNearestHostComponentUpdate: boolean, virtualLevel: number, // the nth level of virtual instances - ): void { + ): UpdateFlags { const stashedParent = reconcilingParent; const stashedPrevious = previouslyReconciledSibling; const stashedRemaining = remainingReconcilingChildren; + const previousSuspendedBy = virtualInstance.suspendedBy; // Push a new DevTools instance parent while reconciling this subtree. reconcilingParent = virtualInstance; previouslyReconciledSibling = null; @@ -2967,18 +4248,24 @@ export function attach( // We'll move them back one by one, and anything that remains is deleted. remainingReconcilingChildren = virtualInstance.firstChild; virtualInstance.firstChild = null; + virtualInstance.suspendedBy = null; try { - if ( - updateVirtualChildrenRecursively( - nextFirstChild, - nextLastChild, - prevFirstChild, - traceNearestHostComponentUpdate, - virtualLevel + 1, - ) - ) { + let updateFlags = updateVirtualChildrenRecursively( + nextFirstChild, + nextLastChild, + prevFirstChild, + traceNearestHostComponentUpdate, + virtualLevel + 1, + ); + if ((updateFlags & ShouldResetChildren) !== NoUpdate) { recordResetChildren(virtualInstance); + updateFlags &= ~ShouldResetChildren; } + removePreviousSuspendedBy( + virtualInstance, + previousSuspendedBy, + reconcilingParentSuspenseNode, + ); // Update the errors/warnings count. If this Instance has switched to a different // ReactComponentInfo instance, such as when refreshing Server Components, then // we replace all the previous logs with the ones associated with the new ones rather @@ -2989,6 +4276,8 @@ export function attach( recordConsoleLogs(virtualInstance, componentLogsEntry); // Must be called after all children have been appended. recordVirtualProfilingDurations(virtualInstance); + + return updateFlags; } finally { unmountRemainingChildren(); reconcilingParent = stashedParent; @@ -3003,8 +4292,8 @@ export function attach( prevFirstChild: null | Fiber, traceNearestHostComponentUpdate: boolean, virtualLevel: number, // the nth level of virtual instances - ): boolean { - let shouldResetChildren = false; + ): UpdateFlags { + let updateFlags = NoUpdate; // If the first child is different, we need to traverse them. // Each next child will be either a new child (mount) or an alternate (update). let nextChild: null | Fiber = nextFirstChild; @@ -3018,6 +4307,17 @@ export function attach( if (nextChild._debugInfo) { for (let i = 0; i < nextChild._debugInfo.length; i++) { const debugEntry = nextChild._debugInfo[i]; + if (debugEntry.awaited) { + // Async Info + const asyncInfo: ReactAsyncInfo = (debugEntry: any); + if (level === virtualLevel) { + // Track any async info between the previous virtual instance up until to this + // instance and add it to the parent. This can add the same set multiple times + // so we assume insertSuspendedBy dedupes. + insertSuspendedBy(asyncInfo); + } + continue; + } if (typeof debugEntry.name !== 'string') { // Not a Component. Some other Debug Info. continue; @@ -3053,8 +4353,10 @@ export function attach( traceNearestHostComponentUpdate, virtualLevel, ); + updateFlags |= + ShouldResetChildren | ShouldResetSuspenseChildren; } else { - updateVirtualInstanceRecursively( + updateFlags |= updateVirtualInstanceRecursively( previousVirtualInstance, previousVirtualInstanceNextFirstFiber, nextChild, @@ -3105,7 +4407,7 @@ export function attach( insertChild(newVirtualInstance); previousVirtualInstance = newVirtualInstance; previousVirtualInstanceWasMount = true; - shouldResetChildren = true; + updateFlags |= ShouldResetChildren; } // Existing children might be reparented into this new virtual instance. // TODO: This will cause the front end to error which needs to be fixed. @@ -3132,8 +4434,9 @@ export function attach( traceNearestHostComponentUpdate, virtualLevel, ); + updateFlags |= ShouldResetChildren | ShouldResetSuspenseChildren; } else { - updateVirtualInstanceRecursively( + updateFlags |= updateVirtualInstanceRecursively( previousVirtualInstance, previousVirtualInstanceNextFirstFiber, nextChild, @@ -3176,45 +4479,43 @@ export function attach( } if (existingInstance !== null) { // Common case. Match in the same parent. - const fiberInstance: FiberInstance = (existingInstance: any); // Only matches if it's a Fiber. + const fiberInstance: FiberInstance | FilteredFiberInstance = + (existingInstance: any); // Only matches if it's a Fiber. // We keep track if the order of the children matches the previous order. // They are always different referentially, but if the instances line up // conceptually we'll want to know that. if (prevChild !== prevChildAtSameIndex) { - shouldResetChildren = true; + updateFlags |= ShouldResetChildren | ShouldResetSuspenseChildren; } moveChild(fiberInstance, previousSiblingOfExistingInstance); - if ( - updateFiberRecursively( - fiberInstance, - nextChild, - (prevChild: any), - traceNearestHostComponentUpdate, - ) - ) { - // If a nested tree child order changed but it can't handle its own - // child order invalidation (e.g. because it's filtered out like host nodes), - // propagate the need to reset child order upwards to this Fiber. - shouldResetChildren = true; - } + // If a nested tree child order changed but it can't handle its own + // child order invalidation (e.g. because it's filtered out like host nodes), + // propagate the need to reset child order upwards to this Fiber. + updateFlags |= updateFiberRecursively( + fiberInstance, + nextChild, + (prevChild: any), + traceNearestHostComponentUpdate, + ); } else if (prevChild !== null && shouldFilterFiber(nextChild)) { + // The filtered instance could've reordered. + if (prevChild !== prevChildAtSameIndex) { + updateFlags |= ShouldResetChildren | ShouldResetSuspenseChildren; + } + // If this Fiber should be filtered, we need to still update its children. // This relies on an alternate since we don't have an Instance with the previous // child on it. Ideally, the reconciliation wouldn't need previous Fibers that // are filtered from the tree. - if ( - updateFiberRecursively( - null, - nextChild, - prevChild, - traceNearestHostComponentUpdate, - ) - ) { - shouldResetChildren = true; - } + updateFlags |= updateFiberRecursively( + null, + nextChild, + prevChild, + traceNearestHostComponentUpdate, + ); } else { // It's possible for a FiberInstance to be reparented when virtual parents // get their sequence split or change structure with the same render result. @@ -3226,14 +4527,17 @@ export function attach( mountFiberRecursively(nextChild, traceNearestHostComponentUpdate); // Need to mark the parent set to remount the new instance. - shouldResetChildren = true; + updateFlags |= ShouldResetChildren | ShouldResetSuspenseChildren; } } // Try the next child. nextChild = nextChild.sibling; // Advance the pointer in the previous list so that we can // keep comparing if they line up. - if (!shouldResetChildren && prevChildAtSameIndex !== null) { + if ( + (updateFlags & ShouldResetChildren) === NoUpdate && + prevChildAtSameIndex !== null + ) { prevChildAtSameIndex = prevChildAtSameIndex.sibling; } } @@ -3246,8 +4550,9 @@ export function attach( traceNearestHostComponentUpdate, virtualLevel, ); + updateFlags |= ShouldResetChildren | ShouldResetSuspenseChildren; } else { - updateVirtualInstanceRecursively( + updateFlags |= updateVirtualInstanceRecursively( previousVirtualInstance, previousVirtualInstanceNextFirstFiber, null, @@ -3259,9 +4564,9 @@ export function attach( } // If we have no more children, but used to, they don't line up. if (prevChildAtSameIndex !== null) { - shouldResetChildren = true; + updateFlags |= ShouldResetChildren | ShouldResetSuspenseChildren; } - return shouldResetChildren; + return updateFlags; } // Returns whether closest unfiltered fiber parent needs to reset its child list. @@ -3269,9 +4574,9 @@ export function attach( nextFirstChild: null | Fiber, prevFirstChild: null | Fiber, traceNearestHostComponentUpdate: boolean, - ): boolean { + ): UpdateFlags { if (nextFirstChild === null) { - return prevFirstChild !== null; + return prevFirstChild !== null ? ShouldResetChildren : NoUpdate; } return updateVirtualChildrenRecursively( nextFirstChild, @@ -3282,13 +4587,62 @@ export function attach( ); } + function updateSuspenseChildrenRecursively( + nextContentFiber: Fiber, + prevContentFiber: Fiber, + traceNearestHostComponentUpdate: boolean, + stashedSuspenseParent: null | SuspenseNode, + stashedSuspensePrevious: null | SuspenseNode, + stashedSuspenseRemaining: null | SuspenseNode, + ): UpdateFlags { + let updateFlags = NoUpdate; + const prevFallbackFiber = prevContentFiber.sibling; + const nextFallbackFiber = nextContentFiber.sibling; + + // First update only the Offscreen boundary. I.e. the main content. + updateFlags |= updateVirtualChildrenRecursively( + nextContentFiber, + nextFallbackFiber, + prevContentFiber, + traceNearestHostComponentUpdate, + 0, + ); + + // Next, we'll pop back out of the SuspenseNode that we added above and now we'll + // reconcile the fallback, reconciling anything in the context of the parent SuspenseNode. + // Since the fallback conceptually blocks the parent. + reconcilingParentSuspenseNode = stashedSuspenseParent; + previouslyReconciledSiblingSuspenseNode = stashedSuspensePrevious; + remainingReconcilingChildrenSuspenseNodes = stashedSuspenseRemaining; + if (prevFallbackFiber !== null || nextFallbackFiber !== null) { + if (nextFallbackFiber === null) { + unmountRemainingChildren(); + } else { + updateFlags |= updateVirtualChildrenRecursively( + nextFallbackFiber, + null, + prevFallbackFiber, + traceNearestHostComponentUpdate, + 0, + ); + + if ((updateFlags & ShouldResetSuspenseChildren) !== NoUpdate) { + updateFlags |= ShouldResetParentSuspenseChildren; + updateFlags &= ~ShouldResetSuspenseChildren; + } + } + } + + return updateFlags; + } + // Returns whether closest unfiltered fiber parent needs to reset its child list. function updateFiberRecursively( - fiberInstance: null | FiberInstance, // null if this should be filtered + fiberInstance: null | FiberInstance | FilteredFiberInstance, // null if this should be filtered nextFiber: Fiber, prevFiber: Fiber, traceNearestHostComponentUpdate: boolean, - ): boolean { + ): UpdateFlags { if (__DEBUG__) { if (fiberInstance !== null) { debug('updateFiberRecursively()', fiberInstance, reconcilingParent); @@ -3323,7 +4677,15 @@ export function attach( const stashedParent = reconcilingParent; const stashedPrevious = previouslyReconciledSibling; const stashedRemaining = remainingReconcilingChildren; + const stashedSuspenseParent = reconcilingParentSuspenseNode; + const stashedSuspensePrevious = previouslyReconciledSiblingSuspenseNode; + const stashedSuspenseRemaining = remainingReconcilingChildrenSuspenseNodes; + let updateFlags = NoUpdate; + let shouldMeasureSuspenseNode = false; + let shouldPopSuspenseNode = false; + let previousSuspendedBy = null; if (fiberInstance !== null) { + previousSuspendedBy = fiberInstance.suspendedBy; // Update the Fiber so we that we always keep the current Fiber on the data. fiberInstance.data = nextFiber; if ( @@ -3342,38 +4704,52 @@ export function attach( // We'll move them back one by one, and anything that remains is deleted. remainingReconcilingChildren = fiberInstance.firstChild; fiberInstance.firstChild = null; + fiberInstance.suspendedBy = null; + + const suspenseNode = fiberInstance.suspenseNode; + if (suspenseNode !== null) { + reconcilingParentSuspenseNode = suspenseNode; + previouslyReconciledSiblingSuspenseNode = null; + remainingReconcilingChildrenSuspenseNodes = suspenseNode.firstChild; + suspenseNode.firstChild = null; + shouldMeasureSuspenseNode = true; + shouldPopSuspenseNode = true; + } } try { - if ( - nextFiber.tag === HostHoistable && - prevFiber.memoizedState !== nextFiber.memoizedState - ) { + trackDebugInfoFromLazyType(nextFiber); + trackDebugInfoFromUsedThenables(nextFiber); + + if (nextFiber.tag === HostHoistable) { const nearestInstance = reconcilingParent; if (nearestInstance === null) { throw new Error('Did not expect a host hoistable to be the root'); } - releaseHostResource(nearestInstance, prevFiber.memoizedState); - aquireHostResource(nearestInstance, nextFiber.memoizedState); + if (prevFiber.memoizedState !== nextFiber.memoizedState) { + releaseHostResource(nearestInstance, prevFiber.memoizedState); + aquireHostResource(nearestInstance, nextFiber.memoizedState); + } + trackDebugInfoFromHostResource(nearestInstance, nextFiber); } else if ( - (nextFiber.tag === HostComponent || - nextFiber.tag === HostText || - nextFiber.tag === HostSingleton) && - prevFiber.stateNode !== nextFiber.stateNode + nextFiber.tag === HostComponent || + nextFiber.tag === HostText || + nextFiber.tag === HostSingleton ) { - // In persistent mode, it's possible for the stateNode to update with - // a new clone. In that case we need to release the old one and aquire - // new one instead. const nearestInstance = reconcilingParent; if (nearestInstance === null) { throw new Error('Did not expect a host hoistable to be the root'); } - releaseHostInstance(nearestInstance, prevFiber.stateNode); - aquireHostInstance(nearestInstance, nextFiber.stateNode); + if (prevFiber.stateNode !== nextFiber.stateNode) { + // In persistent mode, it's possible for the stateNode to update with + // a new clone. In that case we need to release the old one and aquire + // new one instead. + releaseHostInstance(nearestInstance, prevFiber.stateNode); + aquireHostInstance(nearestInstance, nextFiber.stateNode); + } + trackDebugInfoFromHostComponent(nearestInstance, nextFiber); } - const isSuspense = nextFiber.tag === SuspenseComponent; - let shouldResetChildren = false; - // The behavior of timed-out Suspense trees is unique. + // The behavior of timed-out legacy Suspense trees is unique. Without the Offscreen wrapper. // Rather than unmount the timed out content (and possibly lose important state), // React re-parents this content within a hidden Fragment while the fallback is showing. // This behavior doesn't need to be observable in the DevTools though. @@ -3381,8 +4757,29 @@ export function attach( // The easiest fix is to strip out the intermediate Fragment fibers, // so the Elements panel and Profiler don't need to special case them. // Suspense components only have a non-null memoizedState if they're timed-out. - const prevDidTimeout = isSuspense && prevFiber.memoizedState !== null; - const nextDidTimeOut = isSuspense && nextFiber.memoizedState !== null; + const isLegacySuspense = + nextFiber.tag === SuspenseComponent && OffscreenComponent === -1; + const prevDidTimeout = + isLegacySuspense && prevFiber.memoizedState !== null; + const nextDidTimeOut = + isLegacySuspense && nextFiber.memoizedState !== null; + + const isOffscreen = nextFiber.tag === OffscreenComponent; + const prevWasHidden = isOffscreen && prevFiber.memoizedState !== null; + const nextIsHidden = isOffscreen && nextFiber.memoizedState !== null; + + if (isLegacySuspense) { + if ( + fiberInstance !== null && + fiberInstance.suspenseNode !== null && + (prevFiber.stateNode === null) !== (nextFiber.stateNode === null) + ) { + trackThrownPromisesFromRetryCache( + fiberInstance.suspenseNode, + nextFiber.stateNode, + ); + } + } // The logic below is inspired by the code paths in updateSuspenseComponent() // inside ReactFiberBeginWork in the React source code. if (prevDidTimeout && nextDidTimeOut) { @@ -3405,20 +4802,18 @@ export function attach( traceNearestHostComponentUpdate, ); - shouldResetChildren = true; + updateFlags |= ShouldResetChildren | ShouldResetSuspenseChildren; } - if ( - nextFallbackChildSet != null && - prevFallbackChildSet != null && - updateChildrenRecursively( - nextFallbackChildSet, - prevFallbackChildSet, - traceNearestHostComponentUpdate, - ) - ) { - shouldResetChildren = true; - } + const childrenUpdateFlags = + nextFallbackChildSet != null && prevFallbackChildSet != null + ? updateChildrenRecursively( + nextFallbackChildSet, + prevFallbackChildSet, + traceNearestHostComponentUpdate, + ) + : NoUpdate; + updateFlags |= childrenUpdateFlags; } else if (prevDidTimeout && !nextDidTimeOut) { // Fallback -> Primary: // 1. Unmount fallback set @@ -3430,8 +4825,8 @@ export function attach( nextPrimaryChildSet, traceNearestHostComponentUpdate, ); + updateFlags |= ShouldResetChildren | ShouldResetSuspenseChildren; } - shouldResetChildren = true; } else if (!prevDidTimeout && nextDidTimeOut) { // Primary -> Fallback: // 1. Hide primary set @@ -3447,21 +4842,135 @@ export function attach( nextFallbackChildSet, traceNearestHostComponentUpdate, ); - shouldResetChildren = true; + updateFlags |= ShouldResetChildren | ShouldResetSuspenseChildren; } - } else { - // Common case: Primary -> Primary. - // This is the same code path as for non-Suspense fibers. - if (nextFiber.child !== prevFiber.child) { - if ( - updateChildrenRecursively( + } else if (nextIsHidden) { + if (!prevWasHidden) { + // We're hiding the children. Disconnect them from the front end but keep state. + if (fiberInstance !== null && !isInDisconnectedSubtree) { + disconnectChildrenRecursively(remainingReconcilingChildren); + } + } + // Update children inside the hidden tree if they committed with a new updates. + const stashedDisconnected = isInDisconnectedSubtree; + isInDisconnectedSubtree = true; + try { + updateFlags |= updateChildrenRecursively( + nextFiber.child, + prevFiber.child, + false, + ); + } finally { + isInDisconnectedSubtree = stashedDisconnected; + } + } else if (prevWasHidden && !nextIsHidden) { + // We're revealing the hidden children. We now need to update them to the latest state. + // We do this while still in the disconnected state and then we reconnect the new ones. + // This avoids reconnecting things that are about to be removed anyway. + const stashedDisconnected = isInDisconnectedSubtree; + isInDisconnectedSubtree = true; + try { + if (nextFiber.child !== null) { + updateFlags |= updateChildrenRecursively( nextFiber.child, prevFiber.child, - traceNearestHostComponentUpdate, - ) + false, + ); + } + // Ensure we unmount any remaining children inside the isInDisconnectedSubtree flag + // since they should not trigger real deletions. + unmountRemainingChildren(); + remainingReconcilingChildren = null; + } finally { + isInDisconnectedSubtree = stashedDisconnected; + } + if (fiberInstance !== null && !isInDisconnectedSubtree) { + reconnectChildrenRecursively(fiberInstance); + // Children may have reordered while they were hidden. + updateFlags |= ShouldResetChildren | ShouldResetSuspenseChildren; + } + } else if ( + nextFiber.tag === SuspenseComponent && + OffscreenComponent !== -1 && + fiberInstance !== null && + fiberInstance.suspenseNode !== null + ) { + // Modern Suspense path + const suspenseNode = fiberInstance.suspenseNode; + const prevContentFiber = prevFiber.child; + const nextContentFiber = nextFiber.child; + const previousHydrated = isFiberHydrated(prevFiber); + const nextHydrated = isFiberHydrated(nextFiber); + if (previousHydrated && nextHydrated) { + if (nextContentFiber === null || prevContentFiber === null) { + throw new Error( + 'There should always be an Offscreen Fiber child in a hydrated Suspense boundary.', + ); + } + + if ( + (prevFiber.stateNode === null) !== + (nextFiber.stateNode === null) ) { - shouldResetChildren = true; + trackThrownPromisesFromRetryCache( + suspenseNode, + nextFiber.stateNode, + ); } + + shouldMeasureSuspenseNode = false; + updateFlags |= updateSuspenseChildrenRecursively( + nextContentFiber, + prevContentFiber, + traceNearestHostComponentUpdate, + stashedSuspenseParent, + stashedSuspensePrevious, + stashedSuspenseRemaining, + ); + // updateSuspenseChildrenRecursively popped already + shouldPopSuspenseNode = false; + if (nextFiber.memoizedState === null) { + // Measure this Suspense node in case it changed. We don't update the rect while + // we're inside a disconnected subtree nor if we are the Suspense boundary that + // is suspended. This lets us keep the rectangle of the displayed content while + // we're suspended to visualize the resulting state. + shouldMeasureSuspenseNode = !isInDisconnectedSubtree; + } + } else if (!previousHydrated && nextHydrated) { + if (nextContentFiber === null) { + throw new Error( + 'There should always be an Offscreen Fiber child in a hydrated Suspense boundary.', + ); + } + + trackThrownPromisesFromRetryCache(suspenseNode, nextFiber.stateNode); + + mountSuspenseChildrenRecursively( + nextContentFiber, + traceNearestHostComponentUpdate, + stashedSuspenseParent, + stashedSuspensePrevious, + stashedSuspenseRemaining, + ); + // mountSuspenseChildrenRecursively popped already + shouldPopSuspenseNode = false; + } else if (previousHydrated && !nextHydrated) { + throw new Error( + 'Encountered a dehydrated Suspense boundary that was previously hydrated.', + ); + } else { + // This Suspense Fiber is still dehydrated. It won't have any children + // until hydration. + } + } else { + // Common case: Primary -> Primary. + // This is the same code path as for non-Suspense fibers. + if (nextFiber.child !== prevFiber.child) { + updateFlags |= updateChildrenRecursively( + nextFiber.child, + prevFiber.child, + traceNearestHostComponentUpdate, + ); } else { // Children are unchanged. if (fiberInstance !== null) { @@ -3470,6 +4979,8 @@ export function attach( fiberInstance.firstChild = remainingReconcilingChildren; remainingReconcilingChildren = null; + consumeSuspenseNodesOfExistingInstance(fiberInstance); + if (traceUpdatesEnabled) { // If we're tracing updates and we've bailed out before reaching a host node, // we should fall back to recursively marking the nearest host descendants for highlight. @@ -3482,57 +4993,168 @@ export function attach( } } } else { + const childrenUpdateFlags = updateChildrenRecursively( + nextFiber.child, + prevFiber.child, + false, + ); // If this fiber is filtered there might be changes to this set elsewhere so we have // to visit each child to place it back in the set. We let the child bail out instead. - if ( - updateChildrenRecursively(nextFiber.child, prevFiber.child, false) - ) { + if ((childrenUpdateFlags & ShouldResetChildren) !== NoUpdate) { throw new Error( 'The children should not have changed if we pass in the same set.', ); } + updateFlags |= childrenUpdateFlags; } } } if (fiberInstance !== null) { - let componentLogsEntry = fiberToComponentLogsMap.get( - fiberInstance.data, + removePreviousSuspendedBy( + fiberInstance, + previousSuspendedBy, + shouldPopSuspenseNode + ? reconcilingParentSuspenseNode + : stashedSuspenseParent, ); - if (componentLogsEntry === undefined && fiberInstance.data.alternate) { - componentLogsEntry = fiberToComponentLogsMap.get( - fiberInstance.data.alternate, + + if (fiberInstance.kind === FIBER_INSTANCE) { + let componentLogsEntry = fiberToComponentLogsMap.get( + fiberInstance.data, ); - } - recordConsoleLogs(fiberInstance, componentLogsEntry); + if ( + componentLogsEntry === undefined && + fiberInstance.data.alternate + ) { + componentLogsEntry = fiberToComponentLogsMap.get( + fiberInstance.data.alternate, + ); + } + recordConsoleLogs(fiberInstance, componentLogsEntry); - const isProfilingSupported = - nextFiber.hasOwnProperty('treeBaseDuration'); - if (isProfilingSupported) { - recordProfilingDurations(fiberInstance, prevFiber); + const isProfilingSupported = + nextFiber.hasOwnProperty('treeBaseDuration'); + if (isProfilingSupported) { + recordProfilingDurations(fiberInstance, prevFiber); + } } } - if (shouldResetChildren) { + + if ((updateFlags & ShouldResetChildren) !== NoUpdate) { // We need to crawl the subtree for closest non-filtered Fibers // so that we can display them in a flat children set. - if (fiberInstance !== null) { + if (fiberInstance !== null && fiberInstance.kind === FIBER_INSTANCE) { recordResetChildren(fiberInstance); + // We've handled the child order change for this Fiber. // Since it's included, there's no need to invalidate parent child order. - return false; + updateFlags &= ~ShouldResetChildren; } else { // Let the closest unfiltered parent Fiber reset its child order instead. - return true; } } else { - return false; } + + if ((updateFlags & ShouldResetSuspenseChildren) !== NoUpdate) { + if (fiberInstance !== null && fiberInstance.kind === FIBER_INSTANCE) { + const suspenseNode = fiberInstance.suspenseNode; + if (suspenseNode !== null) { + recordResetSuspenseChildren(suspenseNode); + updateFlags &= ~ShouldResetSuspenseChildren; + } + } else { + // Let the closest unfiltered parent Fiber reset its child order instead. + } + } + if ((updateFlags & ShouldResetParentSuspenseChildren) !== NoUpdate) { + if (fiberInstance !== null && fiberInstance.kind === FIBER_INSTANCE) { + const suspenseNode = fiberInstance.suspenseNode; + if (suspenseNode !== null) { + updateFlags &= ~ShouldResetParentSuspenseChildren; + updateFlags |= ShouldResetSuspenseChildren; + } + } else { + // Let the closest unfiltered parent Fiber reset its child order instead. + } + } + + return updateFlags; } finally { if (fiberInstance !== null) { unmountRemainingChildren(); reconcilingParent = stashedParent; previouslyReconciledSibling = stashedPrevious; remainingReconcilingChildren = stashedRemaining; + if (shouldMeasureSuspenseNode) { + if (!isInDisconnectedSubtree) { + // Measure this Suspense node in case it changed. We don't update the rect + // while we're inside a disconnected subtree so that we keep the outline + // as it was before we hid the parent. + const suspenseNode = fiberInstance.suspenseNode; + if (suspenseNode === null) { + throw new Error( + 'Attempted to measure a Suspense node that does not exist.', + ); + } + const prevRects = suspenseNode.rects; + const nextRects = measureInstance(fiberInstance); + if (!areEqualRects(prevRects, nextRects)) { + suspenseNode.rects = nextRects; + recordSuspenseResize(suspenseNode); + } + } + } + if (shouldPopSuspenseNode) { + reconcilingParentSuspenseNode = stashedSuspenseParent; + previouslyReconciledSiblingSuspenseNode = stashedSuspensePrevious; + remainingReconcilingChildrenSuspenseNodes = stashedSuspenseRemaining; + } + } + } + } + + function disconnectChildrenRecursively(firstChild: null | DevToolsInstance) { + for (let child = firstChild; child !== null; child = child.nextSibling) { + if ( + (child.kind === FIBER_INSTANCE || + child.kind === FILTERED_FIBER_INSTANCE) && + child.data.tag === OffscreenComponent && + child.data.memoizedState !== null + ) { + // This instance's children are already disconnected. + } else { + disconnectChildrenRecursively(child.firstChild); + } + if (child.kind === FIBER_INSTANCE) { + recordDisconnect(child); + } else if (child.kind === VIRTUAL_INSTANCE) { + recordVirtualDisconnect(child); + } + } + } + + function reconnectChildrenRecursively(parentInstance: DevToolsInstance) { + for ( + let child = parentInstance.firstChild; + child !== null; + child = child.nextSibling + ) { + if (child.kind === FIBER_INSTANCE) { + recordReconnect(child, parentInstance); + } else if (child.kind === VIRTUAL_INSTANCE) { + const secondaryEnv = null; // TODO: We don't have this data anywhere. We could just stash it somewhere. + recordVirtualReconnect(child, parentInstance, secondaryEnv); + } + if ( + (child.kind === FIBER_INSTANCE || + child.kind === FILTERED_FIBER_INSTANCE) && + child.data.tag === OffscreenComponent && + child.data.memoizedState !== null + ) { + // This instance's children should remain disconnected. + } else { + reconnectChildrenRecursively(child); } } } @@ -3692,14 +5314,9 @@ export function attach( // TODO: relying on this seems a bit fishy. const wasMounted = prevFiber.memoizedState != null && - prevFiber.memoizedState.element != null && - // A dehydrated root is not considered mounted - prevFiber.memoizedState.isDehydrated !== true; + prevFiber.memoizedState.element != null; const isMounted = - current.memoizedState != null && - current.memoizedState.element != null && - // A dehydrated root is not considered mounted - current.memoizedState.isDehydrated !== true; + current.memoizedState != null && current.memoizedState.element != null; if (!wasMounted && isMounted) { // Mount a new root. setRootPseudoKey(currentRoot.id, current); @@ -3857,6 +5474,18 @@ export function attach( } } + function getNearestSuspenseNode(instance: DevToolsInstance): SuspenseNode { + while (instance.suspenseNode === null) { + if (instance.parent === null) { + throw new Error( + 'There should always be a SuspenseNode parent on a mounted instance.', + ); + } + instance = instance.parent; + } + return instance.suspenseNode; + } + function getNearestMountedDOMNode(publicInstance: Element): null | Element { let domNode: null | Element = publicInstance; while (domNode && !publicInstanceToDevToolsInstanceMap.has(domNode)) { @@ -3935,6 +5564,11 @@ export function attach( displayName: getDisplayNameForFiber(fiber) || 'Anonymous', id: instance.id, key: fiber.key, + env: null, + stack: + fiber._debugOwner == null || fiber._debugStack == null + ? null + : parseStackTrace(fiber._debugStack, 1), type: getElementTypeForFiber(fiber), }; } else { @@ -3943,6 +5577,11 @@ export function attach( displayName: componentInfo.name || 'Anonymous', id: instance.id, key: componentInfo.key == null ? null : componentInfo.key, + env: componentInfo.env == null ? null : componentInfo.env, + stack: + componentInfo.owner == null || componentInfo.debugStack == null + ? null + : parseStackTrace(componentInfo.debugStack, 1), type: ElementTypeVirtual, }; } @@ -4050,6 +5689,325 @@ export function attach( return null; } + function inspectHooks(fiber: Fiber): HooksTree { + const originalConsoleMethods: {[string]: $FlowFixMe} = {}; + + // 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] = () => {}; + } catch (error) {} + } + + try { + return inspectHooksOfFiber(fiber, getDispatcherRef(renderer)); + } finally { + // Restore original console functionality. + for (const method in originalConsoleMethods) { + try { + // $FlowFixMe[prop-missing] + console[method] = originalConsoleMethods[method]; + } catch (error) {} + } + } + } + + function getSuspendedByOfSuspenseNode( + suspenseNode: SuspenseNode, + ): Array { + // Collect all ReactAsyncInfo that was suspending this SuspenseNode but + // isn't also in any parent set. + const result: Array = []; + if (!suspenseNode.hasUniqueSuspenders) { + return result; + } + // Cache the inspection of Hooks in case we need it for multiple entries. + // We don't need a full map here since it's likely that every ioInfo that's unique + // to a specific instance will have those appear in order of when that instance was discovered. + let hooksCacheKey: null | DevToolsInstance = null; + let hooksCache: null | HooksTree = null; + suspenseNode.suspendedBy.forEach((set, ioInfo) => { + let parentNode = suspenseNode.parent; + while (parentNode !== null) { + if (parentNode.suspendedBy.has(ioInfo)) { + return; + } + parentNode = parentNode.parent; + } + // We have the ioInfo but we need to find at least one corresponding await + // to go along with it. We don't really need to show every child that awaits the same + // thing so we just pick the first one that is still alive. + if (set.size === 0) { + return; + } + const firstInstance: DevToolsInstance = (set.values().next().value: any); + if (firstInstance.suspendedBy !== null) { + const asyncInfo = getAwaitInSuspendedByFromIO( + firstInstance.suspendedBy, + ioInfo, + ); + if (asyncInfo !== null) { + let hooks: null | HooksTree = null; + if (asyncInfo.stack == null && asyncInfo.owner == null) { + if (hooksCacheKey === firstInstance) { + hooks = hooksCache; + } else if (firstInstance.kind !== VIRTUAL_INSTANCE) { + const fiber = firstInstance.data; + if ( + fiber.dependencies && + fiber.dependencies._debugThenableState + ) { + // This entry had no stack nor owner but this Fiber used Hooks so we might + // be able to get the stack from the Hook. + hooksCacheKey = firstInstance; + hooksCache = hooks = inspectHooks(fiber); + } + } + } + result.push(serializeAsyncInfo(asyncInfo, firstInstance, hooks)); + } + } + }); + return result; + } + + const FALLBACK_THROTTLE_MS: number = 300; + + function getSuspendedByRange( + suspenseNode: SuspenseNode, + ): null | [number, number] { + let min = Infinity; + let max = -Infinity; + suspenseNode.suspendedBy.forEach((_, ioInfo) => { + if (ioInfo.end > max) { + max = ioInfo.end; + } + if (ioInfo.start < min) { + min = ioInfo.start; + } + }); + const parentSuspenseNode = suspenseNode.parent; + if (parentSuspenseNode !== null) { + let parentMax = -Infinity; + parentSuspenseNode.suspendedBy.forEach((_, ioInfo) => { + if (ioInfo.end > parentMax) { + parentMax = ioInfo.end; + } + }); + // The parent max is theoretically the earlier the parent could've committed. + // Therefore, the theoretical max that the child could be throttled is that plus 300ms. + const throttleTime = parentMax + FALLBACK_THROTTLE_MS; + if (throttleTime > max) { + // If the theoretical throttle time is later than the earliest reveal then we extend + // the max time to show that this is timespan could possibly get throttled. + max = throttleTime; + } + + // We use the end of the previous boundary as the start time for this boundary unless, + // that's earlier than we'd need to expand to the full fallback throttle range. It + // suggests that the parent was loaded earlier than this one. + let startTime = max - FALLBACK_THROTTLE_MS; + if (parentMax > startTime) { + startTime = parentMax; + } + // If the first fetch of this boundary starts before that, then we use that as the start. + if (startTime < min) { + min = startTime; + } + } + if (min < Infinity && max > -Infinity) { + return [min, max]; + } + return null; + } + + function getAwaitStackFromHooks( + hooks: HooksTree, + asyncInfo: ReactAsyncInfo, + ): null | ReactStackTrace { + // TODO: We search through the hooks tree generated by inspectHooksOfFiber so that we can + // use the information already extracted but ideally this search would be faster since we + // could know which index to extract from the debug state. + for (let i = 0; i < hooks.length; i++) { + const node = hooks[i]; + const debugInfo = node.debugInfo; + if (debugInfo != null && debugInfo.indexOf(asyncInfo) !== -1) { + // Found a matching Hook. We'll now use its source location to construct a stack. + const source = node.hookSource; + if ( + source != null && + source.functionName !== null && + source.fileName !== null && + source.lineNumber !== null && + source.columnNumber !== null + ) { + // Unfortunately this is in a slightly different format. TODO: Unify HookNode with ReactCallSite. + const callSite: ReactCallSite = [ + source.functionName, + source.fileName, + source.lineNumber, + source.columnNumber, + 0, + 0, + false, + ]; + // As we return we'll add any custom hooks parent stacks to the array. + return [callSite]; + } else { + return []; + } + } + // Otherwise, search the sub hooks of any custom hook. + const matchedStack = getAwaitStackFromHooks(node.subHooks, asyncInfo); + if (matchedStack !== null) { + // Append this custom hook to the stack trace since it must have been called inside of it. + const source = node.hookSource; + if ( + source != null && + source.functionName !== null && + source.fileName !== null && + source.lineNumber !== null && + source.columnNumber !== null + ) { + // Unfortunately this is in a slightly different format. TODO: Unify HookNode with ReactCallSite. + const callSite: ReactCallSite = [ + source.functionName, + source.fileName, + source.lineNumber, + source.columnNumber, + 0, + 0, + false, + ]; + matchedStack.push(callSite); + } + return matchedStack; + } + } + return null; + } + + function serializeAsyncInfo( + asyncInfo: ReactAsyncInfo, + parentInstance: DevToolsInstance, + hooks: null | HooksTree, + ): SerializedAsyncInfo { + const ioInfo = asyncInfo.awaited; + const ioOwnerInstance = findNearestOwnerInstance( + parentInstance, + ioInfo.owner, + ); + let awaitStack = + asyncInfo.debugStack == null + ? null + : // While we have a ReactStackTrace on ioInfo.stack, that will point to the location on + // the server. We need a location that points to the virtual source on the client which + // we can then use to source map to the original location. + parseStackTrace(asyncInfo.debugStack, 1); + let awaitOwnerInstance: null | FiberInstance | VirtualInstance; + if ( + asyncInfo.owner == null && + (awaitStack === null || awaitStack.length === 0) + ) { + // We had no owner nor stack for the await. This can happen if you render it as a child + // or throw a Promise. Replace it with the parent as the await. + awaitStack = null; + awaitOwnerInstance = + parentInstance.kind === FILTERED_FIBER_INSTANCE ? null : parentInstance; + if ( + parentInstance.kind === FIBER_INSTANCE || + parentInstance.kind === FILTERED_FIBER_INSTANCE + ) { + const fiber = parentInstance.data; + switch (fiber.tag) { + case ClassComponent: + case FunctionComponent: + case IncompleteClassComponent: + case IncompleteFunctionComponent: + case IndeterminateComponent: + case MemoComponent: + case SimpleMemoComponent: + // If we awaited in the child position of a component, then the best stack would be the + // return callsite but we don't have that available so instead we skip. The callsite of + // the JSX would be misleading in this case. The same thing happens with throw-a-Promise. + if (hooks !== null) { + // If this component used Hooks we might be able to instead infer the stack from the + // use() callsite if this async info came from a hook. Let's search the tree to find it. + awaitStack = getAwaitStackFromHooks(hooks, asyncInfo); + } + break; + default: + // If we awaited by passing a Promise to a built-in element, then the JSX callsite is a + // good stack trace to use for the await. + if ( + fiber._debugOwner != null && + fiber._debugStack != null && + typeof fiber._debugStack !== 'string' + ) { + awaitStack = parseStackTrace(fiber._debugStack, 1); + awaitOwnerInstance = findNearestOwnerInstance( + parentInstance, + fiber._debugOwner, + ); + } + } + } + } else { + awaitOwnerInstance = findNearestOwnerInstance( + parentInstance, + asyncInfo.owner, + ); + } + + const value: any = ioInfo.value; + let resolvedValue = undefined; + if ( + typeof value === 'object' && + value !== null && + typeof value.then === 'function' + ) { + switch (value.status) { + case 'fulfilled': + resolvedValue = value.value; + break; + case 'rejected': + resolvedValue = value.reason; + break; + } + } + return { + awaited: { + name: ioInfo.name, + description: getIODescription(resolvedValue), + start: ioInfo.start, + end: ioInfo.end, + byteSize: ioInfo.byteSize == null ? null : ioInfo.byteSize, + value: ioInfo.value == null ? null : ioInfo.value, + env: ioInfo.env == null ? null : ioInfo.env, + owner: + ioOwnerInstance === null + ? null + : instanceToSerializedElement(ioOwnerInstance), + stack: + ioInfo.debugStack == null + ? null + : // While we have a ReactStackTrace on ioInfo.stack, that will point to the location on + // the server. We need a location that points to the virtual source on the client which + // we can then use to source map to the original location. + parseStackTrace(ioInfo.debugStack, 1), + }, + env: asyncInfo.env == null ? null : asyncInfo.env, + owner: + awaitOwnerInstance === null + ? null + : instanceToSerializedElement(awaitOwnerInstance), + stack: awaitStack, + }; + } + // Fast path props lookup for React Native style editor. // Could use inspectElementRaw() but that would require shallow rendering hooks components, // and could also mess with memoization. @@ -4140,7 +6098,8 @@ export function attach( // TODO Show custom UI for Cache like we do for Suspense // For now, just hide state data entirely since it's not meant to be inspected. - const showState = !usesHooks && tag !== CacheComponent; + const showState = + tag === ClassComponent || tag === IncompleteClassComponent; const typeSymbol = getTypeSymbol(type); @@ -4250,31 +6209,9 @@ export function attach( const owners: null | Array = getOwnersListFromInstance(fiberInstance); - let hooks = null; + let hooks: null | HooksTree = null; if (usesHooks) { - const originalConsoleMethods: {[string]: $FlowFixMe} = {}; - - // 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] = () => {}; - } catch (error) {} - } - - try { - hooks = inspectHooksOfFiber(fiber, getDispatcherRef(renderer)); - } finally { - // Restore original console functionality. - for (const method in originalConsoleMethods) { - try { - // $FlowFixMe[prop-missing] - console[method] = originalConsoleMethods[method]; - } catch (error) {} - } - } + hooks = inspectHooks(fiber); } let rootType = null; @@ -4341,6 +6278,47 @@ export function attach( nativeTag = getNativeTag(fiber.stateNode); } + let isSuspended: boolean | null = null; + if (tag === SuspenseComponent) { + isSuspended = memoizedState !== null; + } + + const suspendedBy = + fiberInstance.suspenseNode !== null + ? // If this is a Suspense boundary, then we include everything in the subtree that might suspend + // this boundary down to the next Suspense boundary. + getSuspendedByOfSuspenseNode(fiberInstance.suspenseNode) + : // This set is an edge case where if you pass a promise to a Client Component into a children + // position without a Server Component as the direct parent. E.g.
    {promise}
    + // In this case, this becomes associated with the Client/Host Component where as normally + // you'd expect these to be associated with the Server Component that awaited the data. + // TODO: Prepend other suspense sources like css, images and use(). + fiberInstance.suspendedBy === null + ? [] + : fiberInstance.suspendedBy.map(info => + serializeAsyncInfo(info, fiberInstance, hooks), + ); + const suspendedByRange = getSuspendedByRange( + getNearestSuspenseNode(fiberInstance), + ); + + let unknownSuspenders = UNKNOWN_SUSPENDERS_NONE; + if ( + fiberInstance.suspenseNode !== null && + fiberInstance.suspenseNode.hasUnknownSuspenders && + !isTimedOutSuspense + ) { + // Something unknown threw to suspended this boundary. Let's figure out why that might be. + if (renderer.bundleType === 0) { + unknownSuspenders = UNKNOWN_SUSPENDERS_REASON_PRODUCTION; + } else if (!('_debugInfo' in fiber)) { + // TODO: We really should detect _debugThenable and the auto-instrumentation for lazy/thenables too. + unknownSuspenders = UNKNOWN_SUSPENDERS_REASON_OLD_VERSION; + } else { + unknownSuspenders = UNKNOWN_SUSPENDERS_REASON_THROWN_PROMISE; + } + } + return { id: fiberInstance.id, @@ -4372,11 +6350,15 @@ export function attach( forceFallbackForFibers.has(fiber) || (fiber.alternate !== null && forceFallbackForFibers.has(fiber.alternate))), + isSuspended: isSuspended, - // Can view component source location. - canViewSource, source, + stack: + fiber._debugOwner == null || fiber._debugStack == null + ? null + : parseStackTrace(fiber._debugStack, 1), + // Does the component have legacy context attached to it. hasLegacyContext, @@ -4399,9 +6381,15 @@ export function attach( ? [] : Array.from(componentLogsEntry.warnings.entries()), + suspendedBy: suspendedBy, + suspendedByRange: suspendedByRange, + unknownSuspenders: unknownSuspenders, + // List of owners owners, + env: null, + rootType, rendererPackageName: renderer.rendererPackageName, rendererVersion: renderer.version, @@ -4415,7 +6403,6 @@ export function attach( function inspectVirtualInstanceRaw( virtualInstance: VirtualInstance, ): InspectedElement | null { - const canViewSource = true; const source = getSourceForInstance(virtualInstance); const componentInfo = virtualInstance.data; @@ -4453,6 +6440,13 @@ export function attach( const componentLogsEntry = componentInfoToComponentLogsMap.get(componentInfo); + const isSuspended = null; + // Things that Suspended this Server Component (use(), awaits and direct child promises) + const suspendedBy = virtualInstance.suspendedBy; + const suspendedByRange = getSuspendedByRange( + getNearestSuspenseNode(virtualInstance), + ); + return { id: virtualInstance.id, @@ -4468,11 +6462,15 @@ export function attach( isErrored: false, canToggleSuspense: supportsTogglingSuspense && hasSuspenseBoundary, + isSuspended: isSuspended, - // Can view component source location. - canViewSource, source, + stack: + componentInfo.owner == null || componentInfo.debugStack == null + ? null + : parseStackTrace(componentInfo.debugStack, 1), + // Does the component have legacy context attached to it. hasLegacyContext: false, @@ -4494,9 +6492,21 @@ export function attach( componentLogsEntry === undefined ? [] : Array.from(componentLogsEntry.warnings.entries()), + + suspendedBy: + suspendedBy === null + ? [] + : suspendedBy.map(info => + serializeAsyncInfo(info, virtualInstance, null), + ), + suspendedByRange: suspendedByRange, + unknownSuspenders: UNKNOWN_SUSPENDERS_NONE, + // List of owners owners, + env: componentInfo.env == null ? null : componentInfo.env, + rootType, rendererPackageName: renderer.rendererPackageName, rendererVersion: renderer.version, @@ -4538,7 +6548,7 @@ export function attach( function createIsPathAllowed( key: string | null, - secondaryCategory: 'hooks' | null, + secondaryCategory: 'suspendedBy' | 'hooks' | null, ) { // This function helps prevent previously-inspected paths from being dehydrated in updates. // This is important to avoid a bad user experience where expanded toggles collapse on update. @@ -4570,6 +6580,13 @@ export function attach( return true; } break; + case 'suspendedBy': + if (path.length < 5) { + // Never dehydrate anything above suspendedBy[index].awaited.value + // Those are part of the internal meta data. We only dehydrate inside the Promise. + return true; + } + break; default: break; } @@ -4689,7 +6706,7 @@ export function attach( if (isMostRecentlyInspectedElement(id) && !forceFullData) { if (!hasElementUpdatedSinceLastInspected) { if (path !== null) { - let secondaryCategory = null; + let secondaryCategory: 'suspendedBy' | 'hooks' | null = null; if (path[0] === 'hooks') { secondaryCategory = 'hooks'; } @@ -4793,36 +6810,42 @@ export function attach( type: 'not-found', }; } + const inspectedElement = mostRecentlyInspectedElement; // Any time an inspected element has an update, // we should update the selected $r value as wel. // Do this before dehydration (cleanForBridge). - updateSelectedElement(mostRecentlyInspectedElement); + updateSelectedElement(inspectedElement); // Clone before cleaning so that we preserve the full data. // This will enable us to send patches without re-inspecting if hydrated paths are requested. // (Reducing how often we shallow-render is a better DX for function components that use hooks.) - const cleanedInspectedElement = {...mostRecentlyInspectedElement}; + const cleanedInspectedElement = {...inspectedElement}; // $FlowFixMe[prop-missing] found when upgrading Flow cleanedInspectedElement.context = cleanForBridge( - cleanedInspectedElement.context, + inspectedElement.context, createIsPathAllowed('context', null), ); // $FlowFixMe[prop-missing] found when upgrading Flow cleanedInspectedElement.hooks = cleanForBridge( - cleanedInspectedElement.hooks, + inspectedElement.hooks, createIsPathAllowed('hooks', 'hooks'), ); // $FlowFixMe[prop-missing] found when upgrading Flow cleanedInspectedElement.props = cleanForBridge( - cleanedInspectedElement.props, + inspectedElement.props, createIsPathAllowed('props', null), ); // $FlowFixMe[prop-missing] found when upgrading Flow cleanedInspectedElement.state = cleanForBridge( - cleanedInspectedElement.state, + inspectedElement.state, createIsPathAllowed('state', null), ); + // $FlowFixMe[prop-missing] found when upgrading Flow + cleanedInspectedElement.suspendedBy = cleanForBridge( + inspectedElement.suspendedBy, + createIsPathAllowed('suspendedBy', 'suspendedBy'), + ); return { id, @@ -5477,6 +7500,61 @@ export function attach( scheduleUpdate(fiber); } + /** + * Resets the all other roots of this renderer. + * @param rootID The root that contains this milestone + * @param suspendedSet List of IDs of SuspenseComponent Fibers + */ + function overrideSuspenseMilestone( + rootID: FiberInstance['id'], + suspendedSet: Array, + ) { + if ( + typeof setSuspenseHandler !== 'function' || + typeof scheduleUpdate !== 'function' + ) { + throw new Error( + 'Expected overrideSuspenseMilestone() to not get called for earlier React versions.', + ); + } + + // TODO: Allow overriding the timeline for the specified root. + forceFallbackForFibers.forEach(fiber => { + scheduleUpdate(fiber); + }); + forceFallbackForFibers.clear(); + + for (let i = 0; i < suspendedSet.length; ++i) { + const instance = idToDevToolsInstanceMap.get(suspendedSet[i]); + if (instance === undefined) { + console.warn( + `Could not suspend ID '${suspendedSet[i]}' since the instance can't be found.`, + ); + continue; + } + + if (instance.kind === FIBER_INSTANCE) { + const fiber = instance.data; + forceFallbackForFibers.add(fiber); + // We could find a minimal set that covers all the Fibers in this suspended set. + // For now we rely on React's batching of updates. + scheduleUpdate(fiber); + } else { + console.warn(`Cannot not suspend ID '${suspendedSet[i]}'.`); + } + } + + if (forceFallbackForFibers.size > 0) { + // First override is added. Switch React to slower path. + // TODO: Semantics for suspending a timeline are different. We want a suspended + // timeline to act like a first reveal which is relevant for SuspenseList. + // Resuspending would not affect rows in SuspenseList + setSuspenseHandler(shouldSuspendFiberAccordingToSet); + } else { + setSuspenseHandler(shouldSuspendFiberAlwaysFalse); + } + } + // Remember if we're trying to restore the selection after reload. // In that case, we'll do some extra checks for matching mounts. let trackedPath: Array | null = null; @@ -5804,16 +7882,14 @@ export function attach( function getSourceForFiberInstance( fiberInstance: FiberInstance, - ): Source | null { - const unresolvedSource = fiberInstance.source; - if ( - unresolvedSource !== null && - typeof unresolvedSource === 'object' && - !isError(unresolvedSource) - ) { - // $FlowFixMe: isError should have refined it. - return unresolvedSource; + ): ReactFunctionLocation | null { + // Favor the owner source if we have one. + const ownerSource = getSourceForInstance(fiberInstance); + if (ownerSource !== null) { + return ownerSource; } + + // Otherwise fallback to the throwing trick. const dispatcherRef = getDispatcherRef(renderer); const stackFrame = dispatcherRef == null @@ -5824,17 +7900,16 @@ export function attach( dispatcherRef, ); if (stackFrame === null) { - // If we don't find a source location by throwing, try to get one - // from an owned child if possible. This is the same branch as - // for virtual instances. - return getSourceForInstance(fiberInstance); + return null; } - const source = parseSourceFromComponentStack(stackFrame); + const source = extractLocationFromComponentStack(stackFrame); fiberInstance.source = source; return source; } - function getSourceForInstance(instance: DevToolsInstance): Source | null { + function getSourceForInstance( + instance: DevToolsInstance, + ): ReactFunctionLocation | null { let unresolvedSource = instance.source; if (unresolvedSource === null) { // We don't have any source yet. We can try again later in case an owned child mounts later. @@ -5842,25 +7917,115 @@ export function attach( return null; } + if (instance.kind === VIRTUAL_INSTANCE) { + // We might have found one on the virtual instance. + const debugLocation = instance.data.debugLocation; + if (debugLocation != null) { + unresolvedSource = debugLocation; + } + } + // If we have the debug stack (the creation stack of the JSX) for any owned child of this // component, then at the bottom of that stack will be a stack frame that is somewhere within // the component's function body. Typically it would be the callsite of the JSX unless there's // any intermediate utility functions. This won't point to the top of the component function // but it's at least somewhere within it. if (isError(unresolvedSource)) { - unresolvedSource = formatOwnerStack((unresolvedSource: any)); + return (instance.source = extractLocationFromOwnerStack( + (unresolvedSource: any), + )); } if (typeof unresolvedSource === 'string') { const idx = unresolvedSource.lastIndexOf('\n'); const lastLine = idx === -1 ? unresolvedSource : unresolvedSource.slice(idx + 1); - return (instance.source = parseSourceFromComponentStack(lastLine)); + return (instance.source = extractLocationFromComponentStack(lastLine)); } // $FlowFixMe: refined. return unresolvedSource; } + type InternalMcpFunctions = { + __internal_only_getComponentTree?: Function, + }; + + const internalMcpFunctions: InternalMcpFunctions = {}; + if (__IS_INTERNAL_MCP_BUILD__) { + // eslint-disable-next-line no-inner-declarations + function __internal_only_getComponentTree(): string { + let treeString = ''; + + function buildTreeString( + instance: DevToolsInstance, + prefix: string = '', + isLastChild: boolean = true, + ): void { + if (!instance) return; + + const name = + (instance.kind !== VIRTUAL_INSTANCE + ? getDisplayNameForFiber(instance.data) + : instance.data.name) || 'Unknown'; + + const id = instance.id !== undefined ? instance.id : 'unknown'; + + if (name !== 'createRoot()') { + treeString += + prefix + + (isLastChild ? '└── ' : '├── ') + + name + + ' (id: ' + + id + + ')\n'; + } + + const childPrefix = prefix + (isLastChild ? ' ' : '│ '); + + let childCount = 0; + let tempChild = instance.firstChild; + while (tempChild !== null) { + childCount++; + tempChild = tempChild.nextSibling; + } + + let child = instance.firstChild; + let currentChildIndex = 0; + + while (child !== null) { + currentChildIndex++; + const isLastSibling = currentChildIndex === childCount; + buildTreeString(child, childPrefix, isLastSibling); + child = child.nextSibling; + } + } + + const rootInstances: Array = []; + idToDevToolsInstanceMap.forEach(instance => { + if (instance.parent === null || instance.parent.parent === null) { + rootInstances.push(instance); + } + }); + + if (rootInstances.length > 0) { + for (let i = 0; i < rootInstances.length; i++) { + const isLast = i === rootInstances.length - 1; + buildTreeString(rootInstances[i], '', isLast); + if (!isLast) { + treeString += '\n'; + } + } + } else { + treeString = 'No component tree found.'; + } + + return treeString; + } + + internalMcpFunctions.__internal_only_getComponentTree = + __internal_only_getComponentTree; + } + return { cleanup, clearErrorsAndWarnings, @@ -5890,6 +8055,7 @@ export function attach( onErrorOrWarning, overrideError, overrideSuspense, + overrideSuspenseMilestone, overrideValueAtPath, renamePath, renderer, @@ -5898,7 +8064,9 @@ export function attach( startProfiling, stopProfiling, storeAsGlobal, + supportsTogglingSuspense, updateComponentFilters, getEnvironmentNames, + ...internalMcpFunctions, }; } diff --git a/packages/react-devtools-shared/src/backend/flight/renderer.js b/packages/react-devtools-shared/src/backend/flight/renderer.js index 3d8befd4215e7..75763b1f18499 100644 --- a/packages/react-devtools-shared/src/backend/flight/renderer.js +++ b/packages/react-devtools-shared/src/backend/flight/renderer.js @@ -118,9 +118,9 @@ export function attach( if (componentLogsEntry === undefined) { componentLogsEntry = { errors: new Map(), - errorsCount: 0, + errorsCount: 0 as number, warnings: new Map(), - warningsCount: 0, + warningsCount: 0 as number, }; componentInfoToComponentLogsMap.set(componentInfo, componentLogsEntry); } @@ -140,6 +140,8 @@ export function attach( // The changes will be flushed later when we commit this tree to Fiber. } + const supportsTogglingSuspense = false; + return { cleanup() {}, clearErrorsAndWarnings() {}, @@ -202,6 +204,7 @@ export function attach( onErrorOrWarning, overrideError() {}, overrideSuspense() {}, + overrideSuspenseMilestone() {}, overrideValueAtPath() {}, renamePath() {}, renderer, @@ -210,6 +213,7 @@ export function attach( startProfiling() {}, stopProfiling() {}, storeAsGlobal() {}, + supportsTogglingSuspense, updateComponentFilters() {}, getEnvironmentNames() { return []; diff --git a/packages/react-devtools-shared/src/backend/legacy/renderer.js b/packages/react-devtools-shared/src/backend/legacy/renderer.js index cbcc37e319ee6..2915d2cd30554 100644 --- a/packages/react-devtools-shared/src/backend/legacy/renderer.js +++ b/packages/react-devtools-shared/src/backend/legacy/renderer.js @@ -34,6 +34,7 @@ import { TREE_OPERATION_ADD, TREE_OPERATION_REMOVE, TREE_OPERATION_REORDER_CHILDREN, + UNKNOWN_SUSPENDERS_NONE, } from '../../constants'; import {decorateMany, forceUpdate, restoreMany} from './utils'; @@ -179,6 +180,8 @@ export function attach( }; } + const supportsTogglingSuspense = false; + function getDisplayNameForElementID(id: number): string | null { const internalInstance = idToInternalInstanceMap.get(id); return internalInstance ? getData(internalInstance).displayName : null; @@ -407,6 +410,7 @@ export function attach( pushOperation(0); // Profiling flag pushOperation(0); // StrictMode supported? pushOperation(hasOwnerMetadata ? 1 : 0); + pushOperation(supportsTogglingSuspense ? 1 : 0); } else { const type = getElementType(internalInstance); const {displayName, key} = getData(internalInstance); @@ -426,6 +430,7 @@ export function attach( pushOperation(ownerID); pushOperation(displayNameStringID); pushOperation(keyStringID); + pushOperation(getStringID(null)); // name prop } } @@ -755,6 +760,10 @@ export function attach( inspectedElement.state, createIsPathAllowed('state'), ); + inspectedElement.suspendedBy = cleanForBridge( + inspectedElement.suspendedBy, + createIsPathAllowed('suspendedBy'), + ); return { id, @@ -791,6 +800,8 @@ export function attach( displayName: getData(owner).displayName || 'Unknown', id: getID(owner), key: element.key, + env: null, + stack: null, type: getElementType(owner), }); if (owner._currentElement) { @@ -829,11 +840,12 @@ export function attach( // Suspense did not exist in legacy versions canToggleSuspense: false, + isSuspended: null, - // Can view component source location. - canViewSource: type === ElementTypeClass || type === ElementTypeFunction, source: null, + stack: null, + // Only legacy context exists in legacy versions. hasLegacyContext: true, @@ -849,9 +861,16 @@ export function attach( errors, warnings, + // Not supported in legacy renderers. + suspendedBy: [], + suspendedByRange: null, + unknownSuspenders: UNKNOWN_SUSPENDERS_NONE, + // List of owners owners, + env: null, + rootType: null, rendererPackageName: null, rendererVersion: null, @@ -1054,6 +1073,9 @@ export function attach( const overrideSuspense = () => { throw new Error('overrideSuspense not supported by this renderer'); }; + const overrideSuspenseMilestone = () => { + throw new Error('overrideSuspenseMilestone not supported by this renderer'); + }; const startProfiling = () => { // Do not throw, since this would break a multi-root scenario where v15 and v16 were both present. }; @@ -1137,6 +1159,7 @@ export function attach( logElementToConsole, overrideError, overrideSuspense, + overrideSuspenseMilestone, overrideValueAtPath, renamePath, getElementAttributeByPath, @@ -1147,6 +1170,7 @@ export function attach( startProfiling, stopProfiling, storeAsGlobal, + supportsTogglingSuspense, updateComponentFilters, getEnvironmentNames, }; diff --git a/packages/react-devtools-shared/src/backend/profilingHooks.js b/packages/react-devtools-shared/src/backend/profilingHooks.js index c84bb88250375..a3feb1748801c 100644 --- a/packages/react-devtools-shared/src/backend/profilingHooks.js +++ b/packages/react-devtools-shared/src/backend/profilingHooks.js @@ -278,6 +278,7 @@ export function createProfilingHooks({ const top = currentReactMeasuresStack.pop(); // $FlowFixMe[incompatible-type] + // $FlowFixMe[incompatible-use] if (top.type !== type) { console.error( 'Unexpected type "%s" completed at %sms before "%s" completed.', @@ -298,14 +299,16 @@ export function createProfilingHooks({ } function markCommitStarted(lanes: Lanes): void { - if (isProfiling) { - recordReactMeasureStarted('commit', lanes); - - // TODO (timeline) Re-think this approach to "batching"; I don't think it works for Suspense or pre-rendering. - // This issue applies to the User Timing data also. - nextRenderShouldStartNewBatch = true; + if (!isProfiling) { + return; } + recordReactMeasureStarted('commit', lanes); + + // TODO (timeline) Re-think this approach to "batching"; I don't think it works for Suspense or pre-rendering. + // This issue applies to the User Timing data also. + nextRenderShouldStartNewBatch = true; + if (supportsUserTimingV3) { markAndClear(`--commit-start-${lanes}`); @@ -318,54 +321,55 @@ export function createProfilingHooks({ } function markCommitStopped(): void { - if (isProfiling) { - recordReactMeasureCompleted('commit'); - recordReactMeasureCompleted('render-idle'); + if (!isProfiling) { + return; } + recordReactMeasureCompleted('commit'); + recordReactMeasureCompleted('render-idle'); if (supportsUserTimingV3) { markAndClear('--commit-stop'); } } function markComponentRenderStarted(fiber: Fiber): void { - if (isProfiling || supportsUserTimingV3) { - const componentName = getDisplayNameForFiber(fiber) || 'Unknown'; + if (!isProfiling) { + return; + } - if (isProfiling) { - // TODO (timeline) Record and cache component stack - if (isProfiling) { - currentReactComponentMeasure = { - componentName, - duration: 0, - timestamp: getRelativeTime(), - type: 'render', - warning: null, - }; - } - } + const componentName = getDisplayNameForFiber(fiber) || 'Unknown'; - if (supportsUserTimingV3) { - markAndClear(`--component-render-start-${componentName}`); - } + // TODO (timeline) Record and cache component stack + currentReactComponentMeasure = { + componentName, + duration: 0, + timestamp: getRelativeTime(), + type: 'render', + warning: null, + }; + + if (supportsUserTimingV3) { + markAndClear(`--component-render-start-${componentName}`); } } function markComponentRenderStopped(): void { - if (isProfiling) { - if (currentReactComponentMeasure) { - if (currentTimelineData) { - currentTimelineData.componentMeasures.push( - currentReactComponentMeasure, - ); - } + if (!isProfiling) { + return; + } - // $FlowFixMe[incompatible-use] found when upgrading Flow - currentReactComponentMeasure.duration = - // $FlowFixMe[incompatible-use] found when upgrading Flow - getRelativeTime() - currentReactComponentMeasure.timestamp; - currentReactComponentMeasure = null; + if (currentReactComponentMeasure) { + if (currentTimelineData) { + currentTimelineData.componentMeasures.push( + currentReactComponentMeasure, + ); } + + // $FlowFixMe[incompatible-use] found when upgrading Flow + currentReactComponentMeasure.duration = + // $FlowFixMe[incompatible-use] found when upgrading Flow + getRelativeTime() - currentReactComponentMeasure.timestamp; + currentReactComponentMeasure = null; } if (supportsUserTimingV3) { @@ -374,43 +378,43 @@ export function createProfilingHooks({ } function markComponentLayoutEffectMountStarted(fiber: Fiber): void { - if (isProfiling || supportsUserTimingV3) { - const componentName = getDisplayNameForFiber(fiber) || 'Unknown'; + if (!isProfiling) { + return; + } - if (isProfiling) { - // TODO (timeline) Record and cache component stack - if (isProfiling) { - currentReactComponentMeasure = { - componentName, - duration: 0, - timestamp: getRelativeTime(), - type: 'layout-effect-mount', - warning: null, - }; - } - } + const componentName = getDisplayNameForFiber(fiber) || 'Unknown'; - if (supportsUserTimingV3) { - markAndClear(`--component-layout-effect-mount-start-${componentName}`); - } + // TODO (timeline) Record and cache component stack + currentReactComponentMeasure = { + componentName, + duration: 0, + timestamp: getRelativeTime(), + type: 'layout-effect-mount', + warning: null, + }; + + if (supportsUserTimingV3) { + markAndClear(`--component-layout-effect-mount-start-${componentName}`); } } function markComponentLayoutEffectMountStopped(): void { - if (isProfiling) { - if (currentReactComponentMeasure) { - if (currentTimelineData) { - currentTimelineData.componentMeasures.push( - currentReactComponentMeasure, - ); - } + if (!isProfiling) { + return; + } - // $FlowFixMe[incompatible-use] found when upgrading Flow - currentReactComponentMeasure.duration = - // $FlowFixMe[incompatible-use] found when upgrading Flow - getRelativeTime() - currentReactComponentMeasure.timestamp; - currentReactComponentMeasure = null; + if (currentReactComponentMeasure) { + if (currentTimelineData) { + currentTimelineData.componentMeasures.push( + currentReactComponentMeasure, + ); } + + // $FlowFixMe[incompatible-use] found when upgrading Flow + currentReactComponentMeasure.duration = + // $FlowFixMe[incompatible-use] found when upgrading Flow + getRelativeTime() - currentReactComponentMeasure.timestamp; + currentReactComponentMeasure = null; } if (supportsUserTimingV3) { @@ -419,45 +423,43 @@ export function createProfilingHooks({ } function markComponentLayoutEffectUnmountStarted(fiber: Fiber): void { - if (isProfiling || supportsUserTimingV3) { - const componentName = getDisplayNameForFiber(fiber) || 'Unknown'; + if (!isProfiling) { + return; + } - if (isProfiling) { - // TODO (timeline) Record and cache component stack - if (isProfiling) { - currentReactComponentMeasure = { - componentName, - duration: 0, - timestamp: getRelativeTime(), - type: 'layout-effect-unmount', - warning: null, - }; - } - } + const componentName = getDisplayNameForFiber(fiber) || 'Unknown'; - if (supportsUserTimingV3) { - markAndClear( - `--component-layout-effect-unmount-start-${componentName}`, - ); - } + // TODO (timeline) Record and cache component stack + currentReactComponentMeasure = { + componentName, + duration: 0, + timestamp: getRelativeTime(), + type: 'layout-effect-unmount', + warning: null, + }; + + if (supportsUserTimingV3) { + markAndClear(`--component-layout-effect-unmount-start-${componentName}`); } } function markComponentLayoutEffectUnmountStopped(): void { - if (isProfiling) { - if (currentReactComponentMeasure) { - if (currentTimelineData) { - currentTimelineData.componentMeasures.push( - currentReactComponentMeasure, - ); - } + if (!isProfiling) { + return; + } - // $FlowFixMe[incompatible-use] found when upgrading Flow - currentReactComponentMeasure.duration = - // $FlowFixMe[incompatible-use] found when upgrading Flow - getRelativeTime() - currentReactComponentMeasure.timestamp; - currentReactComponentMeasure = null; + if (currentReactComponentMeasure) { + if (currentTimelineData) { + currentTimelineData.componentMeasures.push( + currentReactComponentMeasure, + ); } + + // $FlowFixMe[incompatible-use] found when upgrading Flow + currentReactComponentMeasure.duration = + // $FlowFixMe[incompatible-use] found when upgrading Flow + getRelativeTime() - currentReactComponentMeasure.timestamp; + currentReactComponentMeasure = null; } if (supportsUserTimingV3) { @@ -466,43 +468,43 @@ export function createProfilingHooks({ } function markComponentPassiveEffectMountStarted(fiber: Fiber): void { - if (isProfiling || supportsUserTimingV3) { - const componentName = getDisplayNameForFiber(fiber) || 'Unknown'; + if (!isProfiling) { + return; + } - if (isProfiling) { - // TODO (timeline) Record and cache component stack - if (isProfiling) { - currentReactComponentMeasure = { - componentName, - duration: 0, - timestamp: getRelativeTime(), - type: 'passive-effect-mount', - warning: null, - }; - } - } + const componentName = getDisplayNameForFiber(fiber) || 'Unknown'; - if (supportsUserTimingV3) { - markAndClear(`--component-passive-effect-mount-start-${componentName}`); - } + // TODO (timeline) Record and cache component stack + currentReactComponentMeasure = { + componentName, + duration: 0, + timestamp: getRelativeTime(), + type: 'passive-effect-mount', + warning: null, + }; + + if (supportsUserTimingV3) { + markAndClear(`--component-passive-effect-mount-start-${componentName}`); } } function markComponentPassiveEffectMountStopped(): void { - if (isProfiling) { - if (currentReactComponentMeasure) { - if (currentTimelineData) { - currentTimelineData.componentMeasures.push( - currentReactComponentMeasure, - ); - } + if (!isProfiling) { + return; + } - // $FlowFixMe[incompatible-use] found when upgrading Flow - currentReactComponentMeasure.duration = - // $FlowFixMe[incompatible-use] found when upgrading Flow - getRelativeTime() - currentReactComponentMeasure.timestamp; - currentReactComponentMeasure = null; + if (currentReactComponentMeasure) { + if (currentTimelineData) { + currentTimelineData.componentMeasures.push( + currentReactComponentMeasure, + ); } + + // $FlowFixMe[incompatible-use] found when upgrading Flow + currentReactComponentMeasure.duration = + // $FlowFixMe[incompatible-use] found when upgrading Flow + getRelativeTime() - currentReactComponentMeasure.timestamp; + currentReactComponentMeasure = null; } if (supportsUserTimingV3) { @@ -511,45 +513,43 @@ export function createProfilingHooks({ } function markComponentPassiveEffectUnmountStarted(fiber: Fiber): void { - if (isProfiling || supportsUserTimingV3) { - const componentName = getDisplayNameForFiber(fiber) || 'Unknown'; + if (!isProfiling) { + return; + } - if (isProfiling) { - // TODO (timeline) Record and cache component stack - if (isProfiling) { - currentReactComponentMeasure = { - componentName, - duration: 0, - timestamp: getRelativeTime(), - type: 'passive-effect-unmount', - warning: null, - }; - } - } + const componentName = getDisplayNameForFiber(fiber) || 'Unknown'; - if (supportsUserTimingV3) { - markAndClear( - `--component-passive-effect-unmount-start-${componentName}`, - ); - } + // TODO (timeline) Record and cache component stack + currentReactComponentMeasure = { + componentName, + duration: 0, + timestamp: getRelativeTime(), + type: 'passive-effect-unmount', + warning: null, + }; + + if (supportsUserTimingV3) { + markAndClear(`--component-passive-effect-unmount-start-${componentName}`); } } function markComponentPassiveEffectUnmountStopped(): void { - if (isProfiling) { - if (currentReactComponentMeasure) { - if (currentTimelineData) { - currentTimelineData.componentMeasures.push( - currentReactComponentMeasure, - ); - } + if (!isProfiling) { + return; + } - // $FlowFixMe[incompatible-use] found when upgrading Flow - currentReactComponentMeasure.duration = - // $FlowFixMe[incompatible-use] found when upgrading Flow - getRelativeTime() - currentReactComponentMeasure.timestamp; - currentReactComponentMeasure = null; + if (currentReactComponentMeasure) { + if (currentTimelineData) { + currentTimelineData.componentMeasures.push( + currentReactComponentMeasure, + ); } + + // $FlowFixMe[incompatible-use] found when upgrading Flow + currentReactComponentMeasure.duration = + // $FlowFixMe[incompatible-use] found when upgrading Flow + getRelativeTime() - currentReactComponentMeasure.timestamp; + currentReactComponentMeasure = null; } if (supportsUserTimingV3) { @@ -562,37 +562,37 @@ export function createProfilingHooks({ thrownValue: mixed, lanes: Lanes, ): void { - if (isProfiling || supportsUserTimingV3) { - const componentName = getDisplayNameForFiber(fiber) || 'Unknown'; - const phase = fiber.alternate === null ? 'mount' : 'update'; - - let message = ''; - if ( - thrownValue !== null && - typeof thrownValue === 'object' && - typeof thrownValue.message === 'string' - ) { - message = thrownValue.message; - } else if (typeof thrownValue === 'string') { - message = thrownValue; - } + if (!isProfiling) { + return; + } - if (isProfiling) { - // TODO (timeline) Record and cache component stack - if (currentTimelineData) { - currentTimelineData.thrownErrors.push({ - componentName, - message, - phase, - timestamp: getRelativeTime(), - type: 'thrown-error', - }); - } - } + const componentName = getDisplayNameForFiber(fiber) || 'Unknown'; + const phase = fiber.alternate === null ? 'mount' : 'update'; - if (supportsUserTimingV3) { - markAndClear(`--error-${componentName}-${phase}-${message}`); - } + let message = ''; + if ( + thrownValue !== null && + typeof thrownValue === 'object' && + typeof thrownValue.message === 'string' + ) { + message = thrownValue.message; + } else if (typeof thrownValue === 'string') { + message = thrownValue; + } + + // TODO (timeline) Record and cache component stack + if (currentTimelineData) { + currentTimelineData.thrownErrors.push({ + componentName, + message, + phase, + timestamp: getRelativeTime(), + type: 'thrown-error', + }); + } + + if (supportsUserTimingV3) { + markAndClear(`--error-${componentName}-${phase}-${message}`); } } @@ -613,44 +613,44 @@ export function createProfilingHooks({ wakeable: Wakeable, lanes: Lanes, ): void { - if (isProfiling || supportsUserTimingV3) { - const eventType = wakeableIDs.has(wakeable) ? 'resuspend' : 'suspend'; - const id = getWakeableID(wakeable); - const componentName = getDisplayNameForFiber(fiber) || 'Unknown'; - const phase = fiber.alternate === null ? 'mount' : 'update'; - - // Following the non-standard fn.displayName convention, - // frameworks like Relay may also annotate Promises with a displayName, - // describing what operation/data the thrown Promise is related to. - // When this is available we should pass it along to the Timeline. - const displayName = (wakeable: any).displayName || ''; - - let suspenseEvent: SuspenseEvent | null = null; - if (isProfiling) { - // TODO (timeline) Record and cache component stack - suspenseEvent = { - componentName, - depth: 0, - duration: 0, - id: `${id}`, - phase, - promiseName: displayName, - resolution: 'unresolved', - timestamp: getRelativeTime(), - type: 'suspense', - warning: null, - }; - - if (currentTimelineData) { - currentTimelineData.suspenseEvents.push(suspenseEvent); - } - } + if (!isProfiling) { + return; + } - if (supportsUserTimingV3) { - markAndClear( - `--suspense-${eventType}-${id}-${componentName}-${phase}-${lanes}-${displayName}`, - ); - } + const eventType = wakeableIDs.has(wakeable) ? 'resuspend' : 'suspend'; + const id = getWakeableID(wakeable); + const componentName = getDisplayNameForFiber(fiber) || 'Unknown'; + const phase = fiber.alternate === null ? 'mount' : 'update'; + + // Following the non-standard fn.displayName convention, + // frameworks like Relay may also annotate Promises with a displayName, + // describing what operation/data the thrown Promise is related to. + // When this is available we should pass it along to the Timeline. + const displayName = (wakeable: any).displayName || ''; + + let suspenseEvent: SuspenseEvent | null = null; + // TODO (timeline) Record and cache component stack + suspenseEvent = { + componentName, + depth: 0, + duration: 0, + id: `${id}`, + phase, + promiseName: displayName, + resolution: 'unresolved', + timestamp: getRelativeTime(), + type: 'suspense', + warning: null, + }; + + if (currentTimelineData) { + currentTimelineData.suspenseEvents.push(suspenseEvent); + } + + if (supportsUserTimingV3) { + markAndClear( + `--suspense-${eventType}-${id}-${componentName}-${phase}-${lanes}-${displayName}`, + ); wakeable.then( () => { @@ -680,100 +680,109 @@ export function createProfilingHooks({ } function markLayoutEffectsStarted(lanes: Lanes): void { - if (isProfiling) { - recordReactMeasureStarted('layout-effects', lanes); + if (!isProfiling) { + return; } + recordReactMeasureStarted('layout-effects', lanes); if (supportsUserTimingV3) { markAndClear(`--layout-effects-start-${lanes}`); } } function markLayoutEffectsStopped(): void { - if (isProfiling) { - recordReactMeasureCompleted('layout-effects'); + if (!isProfiling) { + return; } + recordReactMeasureCompleted('layout-effects'); if (supportsUserTimingV3) { markAndClear('--layout-effects-stop'); } } function markPassiveEffectsStarted(lanes: Lanes): void { - if (isProfiling) { - recordReactMeasureStarted('passive-effects', lanes); + if (!isProfiling) { + return; } + recordReactMeasureStarted('passive-effects', lanes); if (supportsUserTimingV3) { markAndClear(`--passive-effects-start-${lanes}`); } } function markPassiveEffectsStopped(): void { - if (isProfiling) { - recordReactMeasureCompleted('passive-effects'); + if (!isProfiling) { + return; } + recordReactMeasureCompleted('passive-effects'); if (supportsUserTimingV3) { markAndClear('--passive-effects-stop'); } } function markRenderStarted(lanes: Lanes): void { - if (isProfiling) { - if (nextRenderShouldStartNewBatch) { - nextRenderShouldStartNewBatch = false; - currentBatchUID++; - } + if (!isProfiling) { + return; + } - // If this is a new batch of work, wrap an "idle" measure around it. - // Log it before the "render" measure to preserve the stack ordering. - if ( - currentReactMeasuresStack.length === 0 || - currentReactMeasuresStack[currentReactMeasuresStack.length - 1].type !== - 'render-idle' - ) { - recordReactMeasureStarted('render-idle', lanes); - } + if (nextRenderShouldStartNewBatch) { + nextRenderShouldStartNewBatch = false; + currentBatchUID++; + } - recordReactMeasureStarted('render', lanes); + // If this is a new batch of work, wrap an "idle" measure around it. + // Log it before the "render" measure to preserve the stack ordering. + if ( + currentReactMeasuresStack.length === 0 || + currentReactMeasuresStack[currentReactMeasuresStack.length - 1].type !== + 'render-idle' + ) { + recordReactMeasureStarted('render-idle', lanes); } + recordReactMeasureStarted('render', lanes); if (supportsUserTimingV3) { markAndClear(`--render-start-${lanes}`); } } function markRenderYielded(): void { - if (isProfiling) { - recordReactMeasureCompleted('render'); + if (!isProfiling) { + return; } + recordReactMeasureCompleted('render'); if (supportsUserTimingV3) { markAndClear('--render-yield'); } } function markRenderStopped(): void { - if (isProfiling) { - recordReactMeasureCompleted('render'); + if (!isProfiling) { + return; } + recordReactMeasureCompleted('render'); if (supportsUserTimingV3) { markAndClear('--render-stop'); } } function markRenderScheduled(lane: Lane): void { - if (isProfiling) { - if (currentTimelineData) { - currentTimelineData.schedulingEvents.push({ - lanes: laneToLanesArray(lane), - timestamp: getRelativeTime(), - type: 'schedule-render', - warning: null, - }); - } + if (!isProfiling) { + return; + } + + if (currentTimelineData) { + currentTimelineData.schedulingEvents.push({ + lanes: laneToLanesArray(lane), + timestamp: getRelativeTime(), + type: 'schedule-render', + warning: null, + }); } if (supportsUserTimingV3) { @@ -782,25 +791,25 @@ export function createProfilingHooks({ } function markForceUpdateScheduled(fiber: Fiber, lane: Lane): void { - if (isProfiling || supportsUserTimingV3) { - const componentName = getDisplayNameForFiber(fiber) || 'Unknown'; + if (!isProfiling) { + return; + } - if (isProfiling) { - // TODO (timeline) Record and cache component stack - if (currentTimelineData) { - currentTimelineData.schedulingEvents.push({ - componentName, - lanes: laneToLanesArray(lane), - timestamp: getRelativeTime(), - type: 'schedule-force-update', - warning: null, - }); - } - } + const componentName = getDisplayNameForFiber(fiber) || 'Unknown'; - if (supportsUserTimingV3) { - markAndClear(`--schedule-forced-update-${lane}-${componentName}`); - } + // TODO (timeline) Record and cache component stack + if (currentTimelineData) { + currentTimelineData.schedulingEvents.push({ + componentName, + lanes: laneToLanesArray(lane), + timestamp: getRelativeTime(), + type: 'schedule-force-update', + warning: null, + }); + } + + if (supportsUserTimingV3) { + markAndClear(`--schedule-forced-update-${lane}-${componentName}`); } } @@ -815,30 +824,30 @@ export function createProfilingHooks({ } function markStateUpdateScheduled(fiber: Fiber, lane: Lane): void { - if (isProfiling || supportsUserTimingV3) { - const componentName = getDisplayNameForFiber(fiber) || 'Unknown'; + if (!isProfiling) { + return; + } - if (isProfiling) { - // TODO (timeline) Record and cache component stack - if (currentTimelineData) { - const event: ReactScheduleStateUpdateEvent = { - componentName, - // Store the parent fibers so we can post process - // them after we finish profiling - lanes: laneToLanesArray(lane), - timestamp: getRelativeTime(), - type: 'schedule-state-update', - warning: null, - }; - currentFiberStacks.set(event, getParentFibers(fiber)); - // $FlowFixMe[incompatible-use] found when upgrading Flow - currentTimelineData.schedulingEvents.push(event); - } - } + const componentName = getDisplayNameForFiber(fiber) || 'Unknown'; - if (supportsUserTimingV3) { - markAndClear(`--schedule-state-update-${lane}-${componentName}`); - } + // TODO (timeline) Record and cache component stack + if (currentTimelineData) { + const event: ReactScheduleStateUpdateEvent = { + componentName, + // Store the parent fibers so we can post process + // them after we finish profiling + lanes: laneToLanesArray(lane), + timestamp: getRelativeTime(), + type: 'schedule-state-update', + warning: null, + }; + currentFiberStacks.set(event, getParentFibers(fiber)); + // $FlowFixMe[incompatible-use] found when upgrading Flow + currentTimelineData.schedulingEvents.push(event); + } + + if (supportsUserTimingV3) { + markAndClear(`--schedule-state-update-${lane}-${componentName}`); } } diff --git a/packages/react-devtools-shared/src/backend/shared/DevToolsOwnerStack.js b/packages/react-devtools-shared/src/backend/shared/DevToolsOwnerStack.js index e03d948a45d3a..36102dcf963be 100644 --- a/packages/react-devtools-shared/src/backend/shared/DevToolsOwnerStack.js +++ b/packages/react-devtools-shared/src/backend/shared/DevToolsOwnerStack.js @@ -15,6 +15,7 @@ export function formatOwnerStack(error: Error): string { Error.prepareStackTrace = undefined; let stack = error.stack; Error.prepareStackTrace = prevPrepareStackTrace; + if (stack.startsWith('Error: react-stack-top-frame\n')) { // V8's default formatting prefixes with the error message which we // don't want/need. @@ -25,7 +26,10 @@ export function formatOwnerStack(error: Error): string { // Pop the JSX frame. stack = stack.slice(idx + 1); } - idx = stack.indexOf('react-stack-bottom-frame'); + idx = stack.indexOf('react_stack_bottom_frame'); + if (idx === -1) { + idx = stack.indexOf('react-stack-bottom-frame'); + } if (idx !== -1) { idx = stack.lastIndexOf('\n', idx); } diff --git a/packages/react-devtools-shared/src/backend/types.js b/packages/react-devtools-shared/src/backend/types.js index f9f11586a022d..9d3e5a0d04e25 100644 --- a/packages/react-devtools-shared/src/backend/types.js +++ b/packages/react-devtools-shared/src/backend/types.js @@ -32,8 +32,9 @@ import type { import type {InitBackend} from 'react-devtools-shared/src/backend'; import type {TimelineDataExport} from 'react-devtools-timeline/src/types'; import type {BackendBridge} from 'react-devtools-shared/src/bridge'; -import type {Source} from 'react-devtools-shared/src/shared/types'; +import type {ReactFunctionLocation, ReactStackTrace} from 'shared/ReactTypes'; import type Agent from './agent'; +import type {UnknownSuspendersReason} from '../constants'; type BundleType = | 0 // PROD @@ -232,10 +233,33 @@ export type PathMatch = { isFullMatch: boolean, }; +// Serialized version of ReactIOInfo +export type SerializedIOInfo = { + name: string, + description: string, + start: number, + end: number, + byteSize: null | number, + value: null | Promise, + env: null | string, + owner: null | SerializedElement, + stack: null | ReactStackTrace, +}; + +// Serialized version of ReactAsyncInfo +export type SerializedAsyncInfo = { + awaited: SerializedIOInfo, + env: null | string, + owner: null | SerializedElement, + stack: null | ReactStackTrace, +}; + export type SerializedElement = { displayName: string | null, id: number, key: number | string | null, + env: null | string, + stack: null | ReactStackTrace, type: ElementType, }; @@ -263,25 +287,36 @@ export type InspectedElement = { // Is this Suspense, and can its value be overridden now? canToggleSuspense: boolean, - - // Can view component source location. - canViewSource: boolean, + // If this Element is suspended. Currently only set on Suspense boundaries. + isSuspended: boolean | null, // Does the component have legacy context attached to it. hasLegacyContext: boolean, // Inspectable properties. - context: Object | null, - hooks: Object | null, - props: Object | null, - state: Object | null, + context: Object | null, // DehydratedData or {[string]: mixed} + hooks: Object | null, // DehydratedData or {[string]: mixed} + props: Object | null, // DehydratedData or {[string]: mixed} + state: Object | null, // DehydratedData or {[string]: mixed} key: number | string | null, errors: Array<[string, number]>, warnings: Array<[string, number]>, + // Things that suspended this Instances + suspendedBy: Object, // DehydratedData or Array + suspendedByRange: null | [number, number], + unknownSuspenders: UnknownSuspendersReason, + // List of owners owners: Array | null, - source: Source | null, + + // Environment name that this component executed in or null for the client + env: string | null, + + source: ReactFunctionLocation | null, + + // The location of the JSX creation. + stack: ReactStackTrace | null, type: ElementType, @@ -402,6 +437,10 @@ export type RendererInterface = { onErrorOrWarning?: OnErrorOrWarning, overrideError: (id: number, forceError: boolean) => void, overrideSuspense: (id: number, forceFallback: boolean) => void, + overrideSuspenseMilestone: ( + rootID: number, + suspendedSet: Array, + ) => void, overrideValueAtPath: ( type: Type, id: number, @@ -434,6 +473,7 @@ export type RendererInterface = { path: Array, count: number, ) => void, + supportsTogglingSuspense: boolean, updateComponentFilters: (componentFilters: Array) => void, getEnvironmentNames: () => Array, diff --git a/packages/react-devtools-shared/src/backend/utils/index.js b/packages/react-devtools-shared/src/backend/utils/index.js index 977683ef9a208..fcb8d448a0c1e 100644 --- a/packages/react-devtools-shared/src/backend/utils/index.js +++ b/packages/react-devtools-shared/src/backend/utils/index.js @@ -12,7 +12,6 @@ import {compareVersions} from 'compare-versions'; import {dehydrate} from 'react-devtools-shared/src/hydration'; import isArray from 'shared/isArray'; -import type {Source} from 'react-devtools-shared/src/shared/types'; import type {DehydratedData} from 'react-devtools-shared/src/frontend/types'; export {default as formatWithStyles} from './formatWithStyles'; @@ -256,95 +255,6 @@ export const isReactNativeEnvironment = (): boolean => { return window.document == null; }; -function extractLocation( - url: string, -): null | {sourceURL: string, line?: string, column?: string} { - if (url.indexOf(':') === -1) { - return null; - } - - // remove any parentheses from start and end - const withoutParentheses = url.replace(/^\(+/, '').replace(/\)+$/, ''); - const locationParts = /(at )?(.+?)(?::(\d+))?(?::(\d+))?$/.exec( - withoutParentheses, - ); - - if (locationParts == null) { - return null; - } - - const [, , sourceURL, line, column] = locationParts; - return {sourceURL, line, column}; -} - -const CHROME_STACK_REGEXP = /^\s*at .*(\S+:\d+|\(native\))/m; -function parseSourceFromChromeStack(stack: string): Source | null { - const frames = stack.split('\n'); - // eslint-disable-next-line no-for-of-loops/no-for-of-loops - for (const frame of frames) { - const sanitizedFrame = frame.trim(); - - const locationInParenthesesMatch = sanitizedFrame.match(/ (\(.+\)$)/); - const possibleLocation = locationInParenthesesMatch - ? locationInParenthesesMatch[1] - : sanitizedFrame; - - const location = extractLocation(possibleLocation); - // Continue the search until at least sourceURL is found - if (location == null) { - continue; - } - - const {sourceURL, line = '1', column = '1'} = location; - - return { - sourceURL, - line: parseInt(line, 10), - column: parseInt(column, 10), - }; - } - - return null; -} - -function parseSourceFromFirefoxStack(stack: string): Source | null { - const frames = stack.split('\n'); - // eslint-disable-next-line no-for-of-loops/no-for-of-loops - for (const frame of frames) { - const sanitizedFrame = frame.trim(); - const frameWithoutFunctionName = sanitizedFrame.replace( - /((.*".+"[^@]*)?[^@]*)(?:@)/, - '', - ); - - const location = extractLocation(frameWithoutFunctionName); - // Continue the search until at least sourceURL is found - if (location == null) { - continue; - } - - const {sourceURL, line = '1', column = '1'} = location; - - return { - sourceURL, - line: parseInt(line, 10), - column: parseInt(column, 10), - }; - } - - return null; -} - -export function parseSourceFromComponentStack( - componentStack: string, -): Source | null { - if (componentStack.match(CHROME_STACK_REGEXP)) { - return parseSourceFromChromeStack(componentStack); - } - - return parseSourceFromFirefoxStack(componentStack); -} - // 0.123456789 => 0.123 // Expects high-resolution timestamp in milliseconds, like from performance.now() // Mainly used for optimizing the size of serialized profiling payload diff --git a/packages/react-devtools-shared/src/backend/utils/parseStackTrace.js b/packages/react-devtools-shared/src/backend/utils/parseStackTrace.js new file mode 100644 index 0000000000000..300bfbebcb369 --- /dev/null +++ b/packages/react-devtools-shared/src/backend/utils/parseStackTrace.js @@ -0,0 +1,332 @@ +/** +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import type {ReactStackTrace, ReactFunctionLocation} from 'shared/ReactTypes'; + +function parseStackTraceFromChromeStack( + stack: string, + skipFrames: number, +): ReactStackTrace { + if (stack.startsWith('Error: react-stack-top-frame\n')) { + // V8's default formatting prefixes with the error message which we + // don't want/need. + stack = stack.slice(29); + } + let idx = stack.indexOf('react_stack_bottom_frame'); + if (idx === -1) { + idx = stack.indexOf('react-stack-bottom-frame'); + } + if (idx !== -1) { + idx = stack.lastIndexOf('\n', idx); + } + if (idx !== -1) { + // Cut off everything after the bottom frame since it'll be internals. + stack = stack.slice(0, idx); + } + const frames = stack.split('\n'); + const parsedFrames: ReactStackTrace = []; + // We skip top frames here since they may or may not be parseable but we + // want to skip the same number of frames regardless. I.e. we can't do it + // in the caller. + for (let i = skipFrames; i < frames.length; i++) { + const parsed = chromeFrameRegExp.exec(frames[i]); + if (!parsed) { + continue; + } + let name = parsed[1] || ''; + let isAsync = parsed[8] === 'async '; + if (name === '') { + name = ''; + } else if (name.startsWith('async ')) { + name = name.slice(5); + isAsync = true; + } + let filename = parsed[2] || parsed[5] || ''; + if (filename === '') { + filename = ''; + } + const line = +(parsed[3] || parsed[6] || 0); + const col = +(parsed[4] || parsed[7] || 0); + parsedFrames.push([name, filename, line, col, 0, 0, isAsync]); + } + return parsedFrames; +} + +const firefoxFrameRegExp = /^((?:.*".+")?[^@]*)@(.+):(\d+):(\d+)$/; +function parseStackTraceFromFirefoxStack( + stack: string, + skipFrames: number, +): ReactStackTrace { + let idx = stack.indexOf('react_stack_bottom_frame'); + if (idx === -1) { + idx = stack.indexOf('react-stack-bottom-frame'); + } + if (idx !== -1) { + idx = stack.lastIndexOf('\n', idx); + } + if (idx !== -1) { + // Cut off everything after the bottom frame since it'll be internals. + stack = stack.slice(0, idx); + } + const frames = stack.split('\n'); + const parsedFrames: ReactStackTrace = []; + // We skip top frames here since they may or may not be parseable but we + // want to skip the same number of frames regardless. I.e. we can't do it + // in the caller. + for (let i = skipFrames; i < frames.length; i++) { + const parsed = firefoxFrameRegExp.exec(frames[i]); + if (!parsed) { + continue; + } + const name = parsed[1] || ''; + const filename = parsed[2] || ''; + const line = +parsed[3]; + const col = +parsed[4]; + parsedFrames.push([name, filename, line, col, 0, 0, false]); + } + return parsedFrames; +} + +const CHROME_STACK_REGEXP = /^\s*at .*(\S+:\d+|\(native\))/m; +export function parseStackTraceFromString( + stack: string, + skipFrames: number, +): ReactStackTrace { + if (stack.match(CHROME_STACK_REGEXP)) { + return parseStackTraceFromChromeStack(stack, skipFrames); + } + return parseStackTraceFromFirefoxStack(stack, skipFrames); +} + +let framesToSkip: number = 0; +let collectedStackTrace: null | ReactStackTrace = null; + +const identifierRegExp = /^[a-zA-Z_$][0-9a-zA-Z_$]*$/; + +function getMethodCallName(callSite: CallSite): string { + const typeName = callSite.getTypeName(); + const methodName = callSite.getMethodName(); + const functionName = callSite.getFunctionName(); + let result = ''; + if (functionName) { + if ( + typeName && + identifierRegExp.test(functionName) && + functionName !== typeName + ) { + result += typeName + '.'; + } + result += functionName; + if ( + methodName && + functionName !== methodName && + !functionName.endsWith('.' + methodName) && + !functionName.endsWith(' ' + methodName) + ) { + result += ' [as ' + methodName + ']'; + } + } else { + if (typeName) { + result += typeName + '.'; + } + if (methodName) { + result += methodName; + } else { + result += ''; + } + } + return result; +} + +function collectStackTrace( + error: Error, + structuredStackTrace: CallSite[], +): string { + const result: ReactStackTrace = []; + // Collect structured stack traces from the callsites. + // We mirror how V8 serializes stack frames and how we later parse them. + for (let i = framesToSkip; i < structuredStackTrace.length; i++) { + const callSite = structuredStackTrace[i]; + let name = callSite.getFunctionName() || ''; + if ( + name.includes('react_stack_bottom_frame') || + name.includes('react-stack-bottom-frame') + ) { + // Skip everything after the bottom frame since it'll be internals. + break; + } else if (callSite.isNative()) { + // $FlowFixMe[prop-missing] + const isAsync = callSite.isAsync(); + result.push([name, '', 0, 0, 0, 0, isAsync]); + } else { + // We encode complex function calls as if they're part of the function + // name since we cannot simulate the complex ones and they look the same + // as function names in UIs on the client as well as stacks. + if (callSite.isConstructor()) { + name = 'new ' + name; + } else if (!callSite.isToplevel()) { + name = getMethodCallName(callSite); + } + if (name === '') { + name = ''; + } + let filename = callSite.getScriptNameOrSourceURL() || ''; + if (filename === '') { + filename = ''; + if (callSite.isEval()) { + const origin = callSite.getEvalOrigin(); + if (origin) { + filename = origin.toString() + ', '; + } + } + } + const line = callSite.getLineNumber() || 0; + const col = callSite.getColumnNumber() || 0; + const enclosingLine: number = + // $FlowFixMe[prop-missing] + typeof callSite.getEnclosingLineNumber === 'function' + ? (callSite: any).getEnclosingLineNumber() || 0 + : 0; + const enclosingCol: number = + // $FlowFixMe[prop-missing] + typeof callSite.getEnclosingColumnNumber === 'function' + ? (callSite: any).getEnclosingColumnNumber() || 0 + : 0; + // $FlowFixMe[prop-missing] + const isAsync = callSite.isAsync(); + result.push([ + name, + filename, + line, + col, + enclosingLine, + enclosingCol, + isAsync, + ]); + } + } + collectedStackTrace = result; + + // At the same time we generate a string stack trace just in case someone + // else reads it. Ideally, we'd call the previous prepareStackTrace to + // ensure it's in the expected format but it's common for that to be + // source mapped and since we do a lot of eager parsing of errors, it + // would be slow in those environments. We could maybe just rely on those + // environments having to disable source mapping globally to speed things up. + // For now, we just generate a default V8 formatted stack trace without + // source mapping as a fallback. + const name = error.name || 'Error'; + const message = error.message || ''; + let stack = name + ': ' + message; + for (let i = 0; i < structuredStackTrace.length; i++) { + stack += '\n at ' + structuredStackTrace[i].toString(); + } + return stack; +} + +// This matches either of these V8 formats. +// at name (filename:0:0) +// at filename:0:0 +// at async filename:0:0 +// at Array.map () +const chromeFrameRegExp = + /^ *at (?:(.+) \((?:(.+):(\d+):(\d+)|\)\)|(?:async )?(.+):(\d+):(\d+)|\)$/; + +const stackTraceCache: WeakMap = new WeakMap(); + +export function parseStackTrace( + error: Error, + skipFrames: number, +): ReactStackTrace { + // We can only get structured data out of error objects once. So we cache the information + // so we can get it again each time. It also helps performance when the same error is + // referenced more than once. + const existing = stackTraceCache.get(error); + if (existing !== undefined) { + return existing; + } + // We override Error.prepareStackTrace with our own version that collects + // the structured data. We need more information than the raw stack gives us + // and we need to ensure that we don't get the source mapped version. + collectedStackTrace = null; + framesToSkip = skipFrames; + const previousPrepare = Error.prepareStackTrace; + Error.prepareStackTrace = collectStackTrace; + let stack; + try { + stack = String(error.stack); + } finally { + Error.prepareStackTrace = previousPrepare; + } + + if (collectedStackTrace !== null) { + const result = collectedStackTrace; + collectedStackTrace = null; + stackTraceCache.set(error, result); + return result; + } + + // If the stack has already been read, or this is not actually a V8 compatible + // engine then we might not get a normalized stack and it might still have been + // source mapped. Regardless we try our best to parse it. + + const parsedFrames = parseStackTraceFromString(stack, skipFrames); + stackTraceCache.set(error, parsedFrames); + return parsedFrames; +} + +export function extractLocationFromOwnerStack( + error: Error, +): ReactFunctionLocation | null { + const stackTrace = parseStackTrace(error, 1); + const stack = error.stack; + if ( + !stack.includes('react_stack_bottom_frame') && + !stack.includes('react-stack-bottom-frame') + ) { + // This didn't have a bottom to it, we can't trust it. + return null; + } + // We start from the bottom since that will have the best location for the owner itself. + for (let i = stackTrace.length - 1; i >= 0; i--) { + const [functionName, fileName, line, col, encLine, encCol] = stackTrace[i]; + // Take the first match with a colon in the file name. + if (fileName.indexOf(':') !== -1) { + return [ + functionName, + fileName, + // Use enclosing line if available, since that points to the start of the function. + encLine || line, + encCol || col, + ]; + } + } + return null; +} + +export function extractLocationFromComponentStack( + stack: string, +): ReactFunctionLocation | null { + const stackTrace = parseStackTraceFromString(stack, 0); + for (let i = 0; i < stackTrace.length; i++) { + const [functionName, fileName, line, col, encLine, encCol] = stackTrace[i]; + // Take the first match with a colon in the file name. + if (fileName.indexOf(':') !== -1) { + return [ + functionName, + fileName, + // Use enclosing line if available. (Never the case here because we parse from string.) + encLine || line, + encCol || col, + ]; + } + } + return null; +} diff --git a/packages/react-devtools-shared/src/backend/views/utils.js b/packages/react-devtools-shared/src/backend/views/utils.js index 595b87c481874..a73c8094edb9f 100644 --- a/packages/react-devtools-shared/src/backend/views/utils.js +++ b/packages/react-devtools-shared/src/backend/views/utils.js @@ -40,6 +40,7 @@ export function getOwnerIframe(node: HTMLElement): HTMLElement | null { // offset added to compensate for its border. export function getBoundingClientRectWithBorderOffset(node: HTMLElement): Rect { const dimensions = getElementDimensions(node); + // $FlowFixMe[incompatible-variance] return mergeRectOffsets([ node.getBoundingClientRect(), { @@ -102,8 +103,10 @@ export function getNestedBoundingClientRect( } } + // $FlowFixMe[incompatible-variance] return mergeRectOffsets(rects); } else { + // $FlowFixMe[incompatible-variance] return node.getBoundingClientRect(); } } diff --git a/packages/react-devtools-shared/src/backendAPI.js b/packages/react-devtools-shared/src/backendAPI.js index b3668b9d98c99..f6c11fb10d66a 100644 --- a/packages/react-devtools-shared/src/backendAPI.js +++ b/packages/react-devtools-shared/src/backendAPI.js @@ -16,6 +16,7 @@ import ElementPollingCancellationError from 'react-devtools-shared/src/errors/El import type { InspectedElement as InspectedElementBackend, InspectedElementPayload, + SerializedAsyncInfo as SerializedAsyncInfoBackend, } from 'react-devtools-shared/src/backend/types'; import type { BackendEvents, @@ -24,6 +25,7 @@ import type { import type { DehydratedData, InspectedElement as InspectedElementFrontend, + SerializedAsyncInfo as SerializedAsyncInfoFrontend, } from 'react-devtools-shared/src/frontend/types'; import type {InspectedElementPath} from 'react-devtools-shared/src/frontend/types'; @@ -209,6 +211,34 @@ export function cloneInspectedElementWithPath( return clonedInspectedElement; } +function backendToFrontendSerializedAsyncInfo( + asyncInfo: SerializedAsyncInfoBackend, +): SerializedAsyncInfoFrontend { + const ioInfo = asyncInfo.awaited; + return { + awaited: { + name: ioInfo.name, + description: ioInfo.description, + start: ioInfo.start, + end: ioInfo.end, + byteSize: ioInfo.byteSize, + value: ioInfo.value, + env: ioInfo.env, + owner: + ioInfo.owner === null + ? null + : backendToFrontendSerializedElementMapper(ioInfo.owner), + stack: ioInfo.stack, + }, + env: asyncInfo.env, + owner: + asyncInfo.owner === null + ? null + : backendToFrontendSerializedElementMapper(asyncInfo.owner), + stack: asyncInfo.stack, + }; +} + export function convertInspectedElementBackendToFrontend( inspectedElementBackend: InspectedElementBackend, ): InspectedElementFrontend { @@ -222,12 +252,14 @@ export function convertInspectedElementBackendToFrontend( canToggleError, isErrored, canToggleSuspense, - canViewSource, + isSuspended, hasLegacyContext, id, type, owners, + env, source, + stack, context, hooks, plugins, @@ -239,9 +271,15 @@ export function convertInspectedElementBackendToFrontend( key, errors, warnings, + suspendedBy, + suspendedByRange, + unknownSuspenders, nativeTag, } = inspectedElementBackend; + const hydratedSuspendedBy: null | Array = + hydrateHelper(suspendedBy); + const inspectedElement: InspectedElementFrontend = { canEditFunctionProps, canEditFunctionPropsDeletePaths, @@ -252,7 +290,7 @@ export function convertInspectedElementBackendToFrontend( canToggleError, isErrored, canToggleSuspense, - canViewSource, + isSuspended, hasLegacyContext, id, key, @@ -260,20 +298,28 @@ export function convertInspectedElementBackendToFrontend( rendererPackageName, rendererVersion, rootType, - // Previous backend implementations (<= 5.0.1) have a different interface for Source, with fileName. - // This gates the source features for only compatible backends: >= 5.0.2 - source: source && source.sourceURL ? source : null, + // Previous backend implementations (<= 6.1.5) have a different interface for Source. + // This gates the source features for only compatible backends: >= 6.1.6 + source: Array.isArray(source) ? source : null, + stack: stack, type, owners: owners === null ? null : owners.map(backendToFrontendSerializedElementMapper), + env, context: hydrateHelper(context), hooks: hydrateHelper(hooks), props: hydrateHelper(props), state: hydrateHelper(state), errors, warnings, + suspendedBy: + hydratedSuspendedBy == null // backwards compat + ? [] + : hydratedSuspendedBy.map(backendToFrontendSerializedAsyncInfo), + suspendedByRange, + unknownSuspenders, nativeTag, }; diff --git a/packages/react-devtools-shared/src/bridge.js b/packages/react-devtools-shared/src/bridge.js index 3a12ae7415025..616f2d3d3ec23 100644 --- a/packages/react-devtools-shared/src/bridge.js +++ b/packages/react-devtools-shared/src/bridge.js @@ -27,7 +27,7 @@ export type BridgeProtocol = { // Version supported by the current frontend/backend. version: number, - // NPM version range that also supports this version. + // NPM version range of `react-devtools-inline` that also supports this version. // Note that 'maxNpmVersion' is only set when the version is bumped. minNpmVersion: string, maxNpmVersion: string | null, @@ -65,6 +65,12 @@ export const BRIDGE_PROTOCOL: Array = [ { version: 2, minNpmVersion: '4.22.0', + maxNpmVersion: '6.2.0', + }, + // Version 3 adds supports-toggling-suspense bit to add-root + { + version: 3, + minNpmVersion: '6.2.0', maxNpmVersion: null, }, ]; @@ -134,6 +140,12 @@ type OverrideSuspense = { forceFallback: boolean, }; +type OverrideSuspenseMilestone = { + rendererID: number, + rootID: number, + suspendedSet: Array, +}; + type CopyElementPathParams = { ...ElementAndRendererID, path: Array, @@ -178,6 +190,7 @@ export type BackendEvents = { backendInitialized: [], backendVersion: [string], bridgeProtocol: [BridgeProtocol], + enableSuspenseTab: [], extensionBackendInitialized: [], fastRefreshScheduled: [], getSavedPreferences: [], @@ -230,6 +243,7 @@ type FrontendEvents = { logElementToConsole: [ElementAndRendererID], overrideError: [OverrideError], overrideSuspense: [OverrideSuspense], + overrideSuspenseMilestone: [OverrideSuspenseMilestone], overrideValueAtPath: [OverrideValueAtPath], profilingData: [ProfilingDataBackend], reloadAndProfile: [ReloadAndProfilingParams], @@ -313,7 +327,7 @@ class Bridge< send>( event: EventName, - ...payload: $ElementType + ...payload: OutgoingEvents[EventName] ) { if (this._isShutdown) { console.warn( diff --git a/packages/react-devtools-shared/src/config/DevToolsFeatureFlags.core-fb.js b/packages/react-devtools-shared/src/config/DevToolsFeatureFlags.core-fb.js index 47e15953c9780..9cec3ce338c76 100644 --- a/packages/react-devtools-shared/src/config/DevToolsFeatureFlags.core-fb.js +++ b/packages/react-devtools-shared/src/config/DevToolsFeatureFlags.core-fb.js @@ -13,9 +13,9 @@ * It should always be imported from "react-devtools-feature-flags". ************************************************************************/ -export const enableLogger = true; -export const enableStyleXFeatures = true; -export const isInternalFacebookBuild = true; +export const enableLogger: boolean = true; +export const enableStyleXFeatures: boolean = true; +export const isInternalFacebookBuild: boolean = true; /************************************************************************ * Do not edit the code below. diff --git a/packages/react-devtools-shared/src/config/DevToolsFeatureFlags.core-oss.js b/packages/react-devtools-shared/src/config/DevToolsFeatureFlags.core-oss.js index e7ec62243adef..326b0fd16ccad 100644 --- a/packages/react-devtools-shared/src/config/DevToolsFeatureFlags.core-oss.js +++ b/packages/react-devtools-shared/src/config/DevToolsFeatureFlags.core-oss.js @@ -13,9 +13,9 @@ * It should always be imported from "react-devtools-feature-flags". ************************************************************************/ -export const enableLogger = false; -export const enableStyleXFeatures = false; -export const isInternalFacebookBuild = false; +export const enableLogger: boolean = false; +export const enableStyleXFeatures: boolean = false; +export const isInternalFacebookBuild: boolean = false; /************************************************************************ * Do not edit the code below. diff --git a/packages/react-devtools-shared/src/config/DevToolsFeatureFlags.default.js b/packages/react-devtools-shared/src/config/DevToolsFeatureFlags.default.js index bba2c8fcbfb05..e7355f8a3475b 100644 --- a/packages/react-devtools-shared/src/config/DevToolsFeatureFlags.default.js +++ b/packages/react-devtools-shared/src/config/DevToolsFeatureFlags.default.js @@ -13,6 +13,6 @@ * It should always be imported from "react-devtools-feature-flags". ************************************************************************/ -export const enableLogger = false; -export const enableStyleXFeatures = false; -export const isInternalFacebookBuild = false; +export const enableLogger: boolean = false; +export const enableStyleXFeatures: boolean = false; +export const isInternalFacebookBuild: boolean = false; diff --git a/packages/react-devtools-shared/src/config/DevToolsFeatureFlags.extension-fb.js b/packages/react-devtools-shared/src/config/DevToolsFeatureFlags.extension-fb.js index 55ea045715013..dc4f05d16fb80 100644 --- a/packages/react-devtools-shared/src/config/DevToolsFeatureFlags.extension-fb.js +++ b/packages/react-devtools-shared/src/config/DevToolsFeatureFlags.extension-fb.js @@ -13,9 +13,9 @@ * It should always be imported from "react-devtools-feature-flags". ************************************************************************/ -export const enableLogger = true; -export const enableStyleXFeatures = true; -export const isInternalFacebookBuild = true; +export const enableLogger: boolean = true; +export const enableStyleXFeatures: boolean = true; +export const isInternalFacebookBuild: boolean = true; /************************************************************************ * Do not edit the code below. diff --git a/packages/react-devtools-shared/src/config/DevToolsFeatureFlags.extension-oss.js b/packages/react-devtools-shared/src/config/DevToolsFeatureFlags.extension-oss.js index 75c8f149b3814..71df63eef0518 100644 --- a/packages/react-devtools-shared/src/config/DevToolsFeatureFlags.extension-oss.js +++ b/packages/react-devtools-shared/src/config/DevToolsFeatureFlags.extension-oss.js @@ -13,9 +13,9 @@ * It should always be imported from "react-devtools-feature-flags". ************************************************************************/ -export const enableLogger = false; -export const enableStyleXFeatures = false; -export const isInternalFacebookBuild = false; +export const enableLogger: boolean = false; +export const enableStyleXFeatures: boolean = false; +export const isInternalFacebookBuild: boolean = false; /************************************************************************ * Do not edit the code below. diff --git a/packages/react-devtools-shared/src/constants.js b/packages/react-devtools-shared/src/constants.js index b08738165906c..8071d3d4a2c6a 100644 --- a/packages/react-devtools-shared/src/constants.js +++ b/packages/react-devtools-shared/src/constants.js @@ -24,10 +24,21 @@ export const TREE_OPERATION_UPDATE_TREE_BASE_DURATION = 4; export const TREE_OPERATION_UPDATE_ERRORS_OR_WARNINGS = 5; export const TREE_OPERATION_REMOVE_ROOT = 6; export const TREE_OPERATION_SET_SUBTREE_MODE = 7; +export const SUSPENSE_TREE_OPERATION_ADD = 8; +export const SUSPENSE_TREE_OPERATION_REMOVE = 9; +export const SUSPENSE_TREE_OPERATION_REORDER_CHILDREN = 10; +export const SUSPENSE_TREE_OPERATION_RESIZE = 11; export const PROFILING_FLAG_BASIC_SUPPORT = 0b01; export const PROFILING_FLAG_TIMELINE_SUPPORT = 0b10; +export const UNKNOWN_SUSPENDERS_NONE: UnknownSuspendersReason = 0; // If we had at least one debugInfo, then that might have been the reason. +export const UNKNOWN_SUSPENDERS_REASON_PRODUCTION: UnknownSuspendersReason = 1; // We're running in prod. That might be why we had unknown suspenders. +export const UNKNOWN_SUSPENDERS_REASON_OLD_VERSION: UnknownSuspendersReason = 2; // We're running an old version of React that doesn't have full coverage. That might be the reason. +export const UNKNOWN_SUSPENDERS_REASON_THROWN_PROMISE: UnknownSuspendersReason = 3; // If we're in dev, didn't detect and debug info and still suspended (other than CSS/image) the only reason is thrown promise. + +export opaque type UnknownSuspendersReason = 0 | 1 | 2 | 3; + export const LOCAL_STORAGE_DEFAULT_TAB_KEY = 'React::DevTools::defaultTab'; export const LOCAL_STORAGE_COMPONENT_FILTER_PREFERENCES_KEY = 'React::DevTools::componentFilters'; @@ -37,6 +48,8 @@ export const LOCAL_STORAGE_OPEN_IN_EDITOR_URL = 'React::DevTools::openInEditorUrl'; export const LOCAL_STORAGE_OPEN_IN_EDITOR_URL_PRESET = 'React::DevTools::openInEditorUrlPreset'; +export const LOCAL_STORAGE_ALWAYS_OPEN_IN_EDITOR = + 'React::DevTools::alwaysOpenInEditor'; export const LOCAL_STORAGE_PARSE_HOOK_NAMES_KEY = 'React::DevTools::parseHookNames'; export const SESSION_STORAGE_RECORD_CHANGE_DESCRIPTIONS_KEY = diff --git a/packages/react-devtools-shared/src/devtools/ContextMenu/types.js b/packages/react-devtools-shared/src/devtools/ContextMenu/types.js index c2f296db10fe5..9fda9be199941 100644 --- a/packages/react-devtools-shared/src/devtools/ContextMenu/types.js +++ b/packages/react-devtools-shared/src/devtools/ContextMenu/types.js @@ -25,7 +25,7 @@ export type ContextMenuHandle = { hide(): void, }; -/*:: -export type ContextMenuComponent = component(ref: React$RefSetter); -*/ +export type ContextMenuComponent = component( + ref: React$RefSetter, +); export type ContextMenuRef = {current: ContextMenuHandle | null}; diff --git a/packages/react-devtools-shared/src/devtools/cache.js b/packages/react-devtools-shared/src/devtools/cache.js index eda30ed56ae36..5ed2133b06898 100644 --- a/packages/react-devtools-shared/src/devtools/cache.js +++ b/packages/react-devtools-shared/src/devtools/cache.js @@ -7,7 +7,12 @@ * @flow */ -import type {ReactContext, Thenable} from 'shared/ReactTypes'; +import type { + ReactContext, + Thenable, + FulfilledThenable, + RejectedThenable, +} from 'shared/ReactTypes'; import * as React from 'react'; import {createContext} from 'react'; @@ -26,27 +31,6 @@ import {createContext} from 'react'; export type {Thenable}; -interface Suspender { - then(resolve: () => mixed, reject: () => mixed): mixed; -} - -type PendingResult = { - status: 0, - value: Suspender, -}; - -type ResolvedResult = { - status: 1, - value: Value, -}; - -type RejectedResult = { - status: 2, - value: mixed, -}; - -type Result = PendingResult | ResolvedResult | RejectedResult; - export type Resource = { clear(): void, invalidate(Key): void, @@ -55,10 +39,6 @@ export type Resource = { write(Key, Value): void, }; -const Pending = 0; -const Resolved = 1; -const Rejected = 2; - let readContext; if (typeof React.use === 'function') { readContext = function (Context: ReactContext) { @@ -115,33 +95,25 @@ function accessResult( fetch: Input => Thenable, input: Input, key: Key, -): Result { +): Thenable { const entriesForResource = getEntriesForResource(resource); const entry = entriesForResource.get(key); if (entry === undefined) { const thenable = fetch(input); thenable.then( value => { - if (newResult.status === Pending) { - const resolvedResult: ResolvedResult = (newResult: any); - resolvedResult.status = Resolved; - resolvedResult.value = value; - } + const fulfilledThenable: FulfilledThenable = (thenable: any); + fulfilledThenable.status = 'fulfilled'; + fulfilledThenable.value = value; }, error => { - if (newResult.status === Pending) { - const rejectedResult: RejectedResult = (newResult: any); - rejectedResult.status = Rejected; - rejectedResult.value = error; - } + const rejectedThenable: RejectedThenable = (thenable: any); + rejectedThenable.status = 'rejected'; + rejectedThenable.reason = error; }, ); - const newResult: PendingResult = { - status: Pending, - value: thenable, - }; - entriesForResource.set(key, newResult); - return newResult; + entriesForResource.set(key, thenable); + return thenable; } else { return entry; } @@ -167,23 +139,22 @@ export function createResource( readContext(CacheContext); const key = hashInput(input); - const result: Result = accessResult(resource, fetch, input, key); + const result: Thenable = accessResult(resource, fetch, input, key); + if (typeof React.use === 'function') { + return React.use(result); + } + switch (result.status) { - case Pending: { - const suspender = result.value; - throw suspender; - } - case Resolved: { + case 'fulfilled': { const value = result.value; return value; } - case Rejected: { - const error = result.value; + case 'rejected': { + const error = result.reason; throw error; } default: - // Should be unreachable - return (undefined: any); + throw result; } }, @@ -198,12 +169,13 @@ export function createResource( write(key: Key, value: Value): void { const entriesForResource = getEntriesForResource(resource); - const resolvedResult: ResolvedResult = { - status: Resolved, + const fulfilledThenable: FulfilledThenable = (Promise.resolve( value, - }; + ): any); + fulfilledThenable.status = 'fulfilled'; + fulfilledThenable.value = value; - entriesForResource.set(key, resolvedResult); + entriesForResource.set(key, fulfilledThenable); }, }; diff --git a/packages/react-devtools-shared/src/devtools/constants.js b/packages/react-devtools-shared/src/devtools/constants.js index cfa3d5af1648b..ee13e5b1630a2 100644 --- a/packages/react-devtools-shared/src/devtools/constants.js +++ b/packages/react-devtools-shared/src/devtools/constants.js @@ -135,6 +135,9 @@ export const THEME_STYLES: {[style: Theme | DisplayDensity]: any, ...} = { '--color-timeline-text-color': '#000000', '--color-timeline-text-dim-color': '#ccc', '--color-timeline-react-work-border': '#eeeeee', + '--color-timebar-background': '#f6f6f6', + '--color-timespan-background': '#62bc6a', + '--color-timespan-background-errored': '#d57066', '--color-search-match': 'yellow', '--color-search-match-current': '#f7923b', '--color-selected-tree-highlight-active': 'rgba(0, 136, 250, 0.1)', @@ -283,6 +286,9 @@ export const THEME_STYLES: {[style: Theme | DisplayDensity]: any, ...} = { '--color-timeline-text-color': '#282c34', '--color-timeline-text-dim-color': '#555b66', '--color-timeline-react-work-border': '#3d424a', + '--color-timebar-background': '#1d2129', + '--color-timespan-background': '#62bc6a', + '--color-timespan-background-errored': '#d57066', '--color-search-match': 'yellow', '--color-search-match-current': '#f7923b', '--color-selected-tree-highlight-active': 'rgba(23, 143, 185, 0.15)', diff --git a/packages/react-devtools-shared/src/devtools/store.js b/packages/react-devtools-shared/src/devtools/store.js index 3895217053df1..02e60a080af3e 100644 --- a/packages/react-devtools-shared/src/devtools/store.js +++ b/packages/react-devtools-shared/src/devtools/store.js @@ -20,6 +20,10 @@ import { TREE_OPERATION_SET_SUBTREE_MODE, TREE_OPERATION_UPDATE_ERRORS_OR_WARNINGS, TREE_OPERATION_UPDATE_TREE_BASE_DURATION, + SUSPENSE_TREE_OPERATION_ADD, + SUSPENSE_TREE_OPERATION_REMOVE, + SUSPENSE_TREE_OPERATION_REORDER_CHILDREN, + SUSPENSE_TREE_OPERATION_RESIZE, } from '../constants'; import {ElementTypeRoot} from '../frontend/types'; import { @@ -44,6 +48,7 @@ import type { Element, ComponentFilter, ElementType, + SuspenseNode, } from 'react-devtools-shared/src/frontend/types'; import type { FrontendBridge, @@ -84,6 +89,7 @@ export type Capabilities = { supportsBasicProfiling: boolean, hasOwnerMetadata: boolean, supportsStrictMode: boolean, + supportsTogglingSuspense: boolean, supportsTimeline: boolean, }; @@ -95,15 +101,17 @@ export default class Store extends EventEmitter<{ backendVersion: [], collapseNodesByDefault: [], componentFilters: [], + enableSuspenseTab: [], error: [Error], hookSettings: [$ReadOnly], hostInstanceSelected: [Element['id']], settingsUpdated: [$ReadOnly], - mutated: [[Array, Map]], + mutated: [[Array, Map]], recordChangeDescriptions: [], roots: [], rootSupportsBasicProfiling: [], rootSupportsTimelineProfiling: [], + suspenseTreeMutated: [], supportsNativeStyleEditor: [], supportsReloadAndProfile: [], unsupportedBridgeProtocolDetected: [], @@ -126,8 +134,10 @@ export default class Store extends EventEmitter<{ _componentFilters: Array; // Map of ID to number of recorded error and warning message IDs. - _errorsAndWarnings: Map = - new Map(); + _errorsAndWarnings: Map< + Element['id'], + {errorCount: number, warningCount: number}, + > = new Map(); // At least one of the injected renderers contains (DEV only) owner metadata. _hasOwnerMetadata: boolean = false; @@ -135,7 +145,9 @@ export default class Store extends EventEmitter<{ // Map of ID to (mutable) Element. // Elements are mutated to avoid excessive cloning during tree updates. // The InspectedElement Suspense cache also relies on this mutability for its WeakMap usage. - _idToElement: Map = new Map(); + _idToElement: Map = new Map(); + + _idToSuspense: Map = new Map(); // Should the React Native style editor panel be shown? _isNativeStyleEditorSupported: boolean = false; @@ -148,7 +160,7 @@ export default class Store extends EventEmitter<{ // Map of element (id) to the set of elements (ids) it owns. // This map enables getOwnersListForElement() to avoid traversing the entire tree. - _ownersMap: Map> = new Map(); + _ownersMap: Map> = new Map(); _profilerStore: ProfilerStore; @@ -157,21 +169,24 @@ export default class Store extends EventEmitter<{ // Incremented each time the store is mutated. // This enables a passive effect to detect a mutation between render and commit phase. _revision: number = 0; + _revisionSuspense: number = 0; // This Array must be treated as immutable! // Passive effects will check it for changes between render and mount. - _roots: $ReadOnlyArray = []; + _roots: $ReadOnlyArray = []; - _rootIDToCapabilities: Map = new Map(); + _rootIDToCapabilities: Map = new Map(); // Renderer ID is needed to support inspection fiber props, state, and hooks. - _rootIDToRendererID: Map = new Map(); + _rootIDToRendererID: Map = new Map(); // These options may be initially set by a configuration option when constructing the Store. _supportsInspectMatchingDOMElement: boolean = false; _supportsClickToInspect: boolean = false; _supportsTimeline: boolean = false; _supportsTraceUpdates: boolean = false; + // Dynamically set if the renderer supports the Suspense tab. + _supportsSuspenseTab: boolean = false; _isReloadAndProfileFrontendSupported: boolean = false; _isReloadAndProfileBackendSupported: boolean = false; @@ -195,6 +210,10 @@ export default class Store extends EventEmitter<{ // Only used in browser extension for synchronization with built-in Elements panel. _lastSelectedHostInstanceElementId: Element['id'] | null = null; + // Maximum recorded node depth during the lifetime of this Store. + // Can only increase: not guaranteed to return maximal value for currently recorded elements. + _maximumRecordedDepth = 0; + constructor(bridge: FrontendBridge, config?: Config) { super(); @@ -271,6 +290,7 @@ export default class Store extends EventEmitter<{ bridge.addListener('hookSettings', this.onHookSettings); bridge.addListener('backendInitialized', this.onBackendInitialized); bridge.addListener('selectElement', this.onHostInstanceSelected); + bridge.addListener('enableSuspenseTab', this.onEnableSuspenseTab); } // This is only used in tests to avoid memory leaks. @@ -431,6 +451,9 @@ export default class Store extends EventEmitter<{ get revision(): number { return this._revision; } + get revisionSuspense(): number { + return this._revisionSuspense; + } get rootIDToRendererID(): Map { return this._rootIDToRendererID; @@ -469,6 +492,14 @@ export default class Store extends EventEmitter<{ ); } + supportsTogglingSuspense(rootID: Element['id']): boolean { + const capabilities = this._rootIDToCapabilities.get(rootID); + if (capabilities === undefined) { + throw new Error(`No capabilities registered for root ${rootID}`); + } + return capabilities.supportsTogglingSuspense; + } + // This build of DevTools supports the Timeline profiler. // This is a static flag, controlled by the Store config. get supportsTimeline(): boolean { @@ -587,6 +618,16 @@ export default class Store extends EventEmitter<{ return element; } + getSuspenseByID(id: SuspenseNode['id']): SuspenseNode | null { + const suspense = this._idToSuspense.get(id); + if (suspense === undefined) { + console.warn(`No suspense found with id "${id}"`); + return null; + } + + return suspense; + } + // Returns a tuple of [id, index] getElementsWithErrorsAndWarnings(): ErrorAndWarningTuples { if (!this._shouldShowWarningsAndErrors) { @@ -698,6 +739,50 @@ export default class Store extends EventEmitter<{ return index; } + isDescendantOf(parentId: number, descendantId: number): boolean { + if (descendantId === 0) { + return false; + } + + const descendant = this.getElementByID(descendantId); + if (descendant === null) { + return false; + } + + if (descendant.parentID === parentId) { + return true; + } + + const parent = this.getElementByID(parentId); + if (!parent || parent.depth >= descendant.depth) { + return false; + } + + return this.isDescendantOf(parentId, descendant.parentID); + } + + /** + * Returns index of the lowest descendant element, if available. + * May not be the deepest element, the lowest is used in a sense of bottom-most from UI Tree representation perspective. + */ + getIndexOfLowestDescendantElement(element: Element): number | null { + let current: null | Element = element; + while (current !== null) { + if (current.isCollapsed || current.children.length === 0) { + if (current === element) { + return null; + } + + return this.getIndexOfElementID(current.id); + } else { + const lastChildID = current.children[current.children.length - 1]; + current = this.getElementByID(lastChildID); + } + } + + return null; + } + getOwnersListForElement(ownerID: number): Array { const list: Array = []; const element = this._idToElement.get(ownerID); @@ -937,6 +1022,7 @@ export default class Store extends EventEmitter<{ let haveRootsChanged = false; let haveErrorsOrWarningsChanged = false; + let hasSuspenseTreeChanged = false; // The first two values are always rendererID and rootID const rendererID = operations[0]; @@ -1003,6 +1089,7 @@ export default class Store extends EventEmitter<{ let supportsStrictMode = false; let hasOwnerMetadata = false; + let supportsTogglingSuspense = false; // If we don't know the bridge protocol, guess that we're dealing with the latest. // If we do know it, we can take it into consideration when parsing operations. @@ -1015,6 +1102,9 @@ export default class Store extends EventEmitter<{ hasOwnerMetadata = operations[i] > 0; i++; + + supportsTogglingSuspense = operations[i] > 0; + i++; } this._roots = this._roots.concat(id); @@ -1023,6 +1113,7 @@ export default class Store extends EventEmitter<{ supportsBasicProfiling, hasOwnerMetadata, supportsStrictMode, + supportsTogglingSuspense, supportsTimeline, }); @@ -1040,6 +1131,7 @@ export default class Store extends EventEmitter<{ isCollapsed: false, // Never collapse roots; it would hide the entire tree. isStrictModeNonCompliant, key: null, + nameProp: null, ownerID: 0, parentID: 0, type, @@ -1063,6 +1155,10 @@ export default class Store extends EventEmitter<{ const key = stringTable[keyStringID]; i++; + const namePropStringID = operations[i]; + const nameProp = stringTable[namePropStringID]; + i++; + if (__DEBUG__) { debug( 'Add', @@ -1089,15 +1185,22 @@ export default class Store extends EventEmitter<{ compiledWithForget, } = parseElementDisplayNameFromBackend(displayName, type); + const elementDepth = parentElement.depth + 1; + this._maximumRecordedDepth = Math.max( + this._maximumRecordedDepth, + elementDepth, + ); + const element: Element = { children: [], - depth: parentElement.depth + 1, + depth: elementDepth, displayName: displayNameWithoutHOCs, hocDisplayNames, id, isCollapsed: this._collapseNodesByDefault, isStrictModeNonCompliant: parentElement.isStrictModeNonCompliant, key, + nameProp, ownerID, parentID, type, @@ -1117,6 +1220,14 @@ export default class Store extends EventEmitter<{ } set.add(id); } + + const suspense = this._idToSuspense.get(id); + if (suspense !== undefined) { + // We're reconnecting a node. + if (suspense.name === null) { + suspense.name = this._guessSuspenseName(element); + } + } } break; } @@ -1311,7 +1422,7 @@ export default class Store extends EventEmitter<{ // The profiler UI uses them lazily in order to generate the tree. i += 3; break; - case TREE_OPERATION_UPDATE_ERRORS_OR_WARNINGS: + case TREE_OPERATION_UPDATE_ERRORS_OR_WARNINGS: { const id = operations[i + 1]; const errorCount = operations[i + 2]; const warningCount = operations[i + 3]; @@ -1325,6 +1436,246 @@ export default class Store extends EventEmitter<{ } haveErrorsOrWarningsChanged = true; break; + } + case SUSPENSE_TREE_OPERATION_ADD: { + const id = operations[i + 1]; + const parentID = operations[i + 2]; + const nameStringID = operations[i + 3]; + const numRects = ((operations[i + 4]: any): number); + let name = stringTable[nameStringID]; + + if (this._idToSuspense.has(id)) { + this._throwAndEmitError( + Error( + `Cannot add suspense node "${id}" because a suspense node with that id is already in the Store.`, + ), + ); + } + + const element = this._idToElement.get(id); + if (element === undefined) { + // This element isn't connected yet. + } else { + if (name === null) { + // The boundary isn't explicitly named. + // Pick a sensible default. + name = this._guessSuspenseName(element); + } + } + + i += 5; + let rects: SuspenseNode['rects']; + if (numRects === -1) { + rects = null; + } else { + rects = []; + for (let rectIndex = 0; rectIndex < numRects; rectIndex++) { + const x = operations[i + 0]; + const y = operations[i + 1]; + const width = operations[i + 2]; + const height = operations[i + 3]; + rects.push({x, y, width, height}); + i += 4; + } + } + + if (__DEBUG__) { + debug('Suspense Add', `node ${id} as child of ${parentID}`); + } + + if (parentID !== 0) { + const parentSuspense = this._idToSuspense.get(parentID); + if (parentSuspense === undefined) { + this._throwAndEmitError( + Error( + `Cannot add suspense child "${id}" to parent suspense "${parentID}" because parent suspense node was not found in the Store.`, + ), + ); + + break; + } + + parentSuspense.children.push(id); + } + + if (name === null) { + name = 'Unknown'; + } + + this._idToSuspense.set(id, { + id, + parentID, + children: [], + name, + rects, + }); + + hasSuspenseTreeChanged = true; + break; + } + case SUSPENSE_TREE_OPERATION_REMOVE: { + const removeLength = operations[i + 1]; + i += 2; + + for (let removeIndex = 0; removeIndex < removeLength; removeIndex++) { + const id = operations[i]; + const suspense = this._idToSuspense.get(id); + + if (suspense === undefined) { + this._throwAndEmitError( + Error( + `Cannot remove suspense node "${id}" because no matching node was found in the Store.`, + ), + ); + + break; + } + + i += 1; + + const {children, parentID} = suspense; + if (children.length > 0) { + this._throwAndEmitError( + Error(`Suspense node "${id}" was removed before its children.`), + ); + } + + this._idToSuspense.delete(id); + + let parentSuspense: ?SuspenseNode = null; + if (parentID === 0) { + if (__DEBUG__) { + debug('Suspense remove', `node ${id} root`); + } + } else { + if (__DEBUG__) { + debug('Suspense Remove', `node ${id} from parent ${parentID}`); + } + + parentSuspense = this._idToSuspense.get(parentID); + if (parentSuspense === undefined) { + this._throwAndEmitError( + Error( + `Cannot remove suspense node "${id}" from parent "${parentID}" because no matching node was found in the Store.`, + ), + ); + + break; + } + + const index = parentSuspense.children.indexOf(id); + parentSuspense.children.splice(index, 1); + } + } + + hasSuspenseTreeChanged = true; + break; + } + case SUSPENSE_TREE_OPERATION_REORDER_CHILDREN: { + const id = operations[i + 1]; + const numChildren = operations[i + 2]; + i += 3; + + const suspense = this._idToSuspense.get(id); + if (suspense === undefined) { + this._throwAndEmitError( + Error( + `Cannot reorder children for suspense node "${id}" because no matching node was found in the Store.`, + ), + ); + + break; + } + + const children = suspense.children; + if (children.length !== numChildren) { + this._throwAndEmitError( + Error( + `Suspense children cannot be added or removed during a reorder operation.`, + ), + ); + } + + for (let j = 0; j < numChildren; j++) { + const childID = operations[i + j]; + children[j] = childID; + if (__DEV__) { + // This check is more expensive so it's gated by __DEV__. + const childSuspense = this._idToSuspense.get(childID); + if (childSuspense == null || childSuspense.parentID !== id) { + console.error( + `Suspense children cannot be added or removed during a reorder operation.`, + ); + } + } + } + i += numChildren; + + if (__DEBUG__) { + debug( + 'Re-order', + `Suspense node ${id} children ${children.join(',')}`, + ); + } + + hasSuspenseTreeChanged = true; + break; + } + case SUSPENSE_TREE_OPERATION_RESIZE: { + const id = ((operations[i + 1]: any): number); + const numRects = ((operations[i + 2]: any): number); + i += 3; + + const suspense = this._idToSuspense.get(id); + if (suspense === undefined) { + this._throwAndEmitError( + Error( + `Cannot set rects for suspense node "${id}" because no matching node was found in the Store.`, + ), + ); + + break; + } + + let nextRects: SuspenseNode['rects']; + if (numRects === -1) { + nextRects = null; + } else { + nextRects = []; + for (let rectIndex = 0; rectIndex < numRects; rectIndex++) { + const x = operations[i + 0]; + const y = operations[i + 1]; + const width = operations[i + 2]; + const height = operations[i + 3]; + + nextRects.push({x, y, width, height}); + + i += 4; + } + } + + suspense.rects = nextRects; + + if (__DEBUG__) { + debug( + 'Resize', + `Suspense node ${id} resize to ${ + nextRects === null + ? 'null' + : nextRects + .map( + rect => + `(${rect.x},${rect.y},${rect.width},${rect.height})`, + ) + .join(',') + }`, + ); + } + + hasSuspenseTreeChanged = true; + + break; + } default: this._throwAndEmitError( new UnsupportedBridgeOperationError( @@ -1335,6 +1686,9 @@ export default class Store extends EventEmitter<{ } this._revision++; + if (hasSuspenseTreeChanged) { + this._revisionSuspense++; + } // Any time the tree changes (e.g. elements added, removed, or reordered) cached indices may be invalid. this._cachedErrorAndWarningTuples = null; @@ -1393,6 +1747,10 @@ export default class Store extends EventEmitter<{ } } + if (hasSuspenseTreeChanged) { + this.emit('suspenseTreeMutated'); + } + if (__DEBUG__) { console.log(printStore(this, true)); console.groupEnd(); @@ -1536,6 +1894,14 @@ export default class Store extends EventEmitter<{ } }; + /** + * Maximum recorded node depth during the lifetime of this Store. + * Can only increase: not guaranteed to return maximal value for currently recorded elements. + */ + getMaximumRecordedDepth(): number { + return this._maximumRecordedDepth; + } + updateHookSettings: (settings: $ReadOnly) => void = settings => { this._hookSettings = settings; @@ -1562,6 +1928,15 @@ export default class Store extends EventEmitter<{ } } + get supportsSuspenseTab(): boolean { + return this._supportsSuspenseTab; + } + + onEnableSuspenseTab = (): void => { + this._supportsSuspenseTab = true; + this.emit('enableSuspenseTab'); + }; + // The Store should never throw an Error without also emitting an event. // Otherwise Store errors will be invisible to users, // but the downstream errors they cause will be reported as bugs. @@ -1574,4 +1949,15 @@ export default class Store extends EventEmitter<{ // and for unit testing the Store itself. throw error; } + + _guessSuspenseName(element: Element): string | null { + // TODO: Use key + const owner = this._idToElement.get(element.ownerID); + if (owner !== undefined) { + // TODO: This is clowny + return `${owner.displayName || 'Unknown'}>?`; + } + + return null; + } } diff --git a/packages/react-devtools-shared/src/devtools/utils.js b/packages/react-devtools-shared/src/devtools/utils.js index b6814f73f8d0f..0501e861bbd67 100644 --- a/packages/react-devtools-shared/src/devtools/utils.js +++ b/packages/react-devtools-shared/src/devtools/utils.js @@ -9,7 +9,11 @@ import JSON5 from 'json5'; -import type {Element} from 'react-devtools-shared/src/frontend/types'; +import type {ReactFunctionLocation} from 'shared/ReactTypes'; +import type { + Element, + SuspenseNode, +} from 'react-devtools-shared/src/frontend/types'; import type {StateContext} from './views/Components/TreeContext'; import type Store from './store'; @@ -27,6 +31,11 @@ export function printElement( key = ` key="${element.key}"`; } + let name = ''; + if (element.nameProp !== null) { + name = ` name="${element.nameProp}"`; + } + let hocDisplayNames = null; if (element.hocDisplayNames !== null) { hocDisplayNames = [...element.hocDisplayNames]; @@ -42,7 +51,45 @@ export function printElement( return `${' '.repeat(element.depth + 1)}${prefix} <${ element.displayName || 'null' - }${key}>${hocs}${suffix}`; + }${key}${name}>${hocs}${suffix}`; +} + +function printSuspense( + suspense: SuspenseNode, + includeWeight: boolean = false, +): string { + let name = ''; + if (suspense.name !== null) { + name = ` name="${suspense.name}"`; + } + + let printedRects = ''; + const rects = suspense.rects; + if (rects === null) { + printedRects = ' rects={null}'; + } else { + printedRects = ` rects={[${rects.map(rect => `{x:${rect.x},y:${rect.y},width:${rect.width},height:${rect.height}}`).join(', ')}]}`; + } + + return ``; +} + +function printSuspenseWithChildren( + store: Store, + suspense: SuspenseNode, + depth: number, +): Array { + const lines = [' '.repeat(depth) + printSuspense(suspense)]; + for (let i = 0; i < suspense.children.length; i++) { + const childID = suspense.children[i]; + const child = store.getSuspenseByID(childID); + if (child === null) { + throw new Error(`Could not find Suspense node with ID "${childID}".`); + } + lines.push(...printSuspenseWithChildren(store, child, depth + 1)); + } + + return lines; } export function printOwnersList( @@ -58,6 +105,7 @@ export function printStore( store: Store, includeWeight: boolean = false, state: StateContext | null = null, + includeSuspense: boolean = true, ): string { const snapshotLines = []; @@ -128,6 +176,26 @@ export function printStore( } rootWeight += weight; + + if (includeSuspense) { + const shell = store.getSuspenseByID(rootID); + // Roots from legacy renderers don't have a separate Suspense tree + if (shell !== null) { + if (shell.children.length > 0) { + snapshotLines.push('[shell]'); + for (let i = 0; i < shell.children.length; i++) { + const childID = shell.children[i]; + const child = store.getSuspenseByID(childID); + if (child === null) { + throw new Error( + `Could not find Suspense node with ID "${childID}".`, + ); + } + snapshotLines.push(...printSuspenseWithChildren(store, child, 1)); + } + } + } + } }); // Make sure the pretty-printed test align with the Store's reported number of total rows. @@ -188,16 +256,13 @@ export function smartStringify(value: any): string { return JSON.stringify(value); } -// [url, row, column] -export type Stack = [string, number, number]; - const STACK_DELIMETER = /\n\s+at /; const STACK_SOURCE_LOCATION = /([^\s]+) \((.+):(.+):(.+)\)/; -export function stackToComponentSources( +export function stackToComponentLocations( stack: string, -): Array<[string, ?Stack]> { - const out: Array<[string, ?Stack]> = []; +): Array<[string, ?ReactFunctionLocation]> { + const out: Array<[string, ?ReactFunctionLocation]> = []; stack .split(STACK_DELIMETER) .slice(1) @@ -205,7 +270,10 @@ export function stackToComponentSources( const match = STACK_SOURCE_LOCATION.exec(entry); if (match) { const [, component, url, row, column] = match; - out.push([component, [url, parseInt(row, 10), parseInt(column, 10)]]); + out.push([ + component, + [component, url, parseInt(row, 10), parseInt(column, 10)], + ]); } else { out.push([entry, null]); } diff --git a/packages/react-devtools-shared/src/devtools/views/ButtonIcon.js b/packages/react-devtools-shared/src/devtools/views/ButtonIcon.js index a91c7e04b5545..454497e6b02da 100644 --- a/packages/react-devtools-shared/src/devtools/views/ButtonIcon.js +++ b/packages/react-devtools-shared/src/devtools/views/ButtonIcon.js @@ -34,6 +34,12 @@ export type IconType = | 'save' | 'search' | 'settings' + | 'panel-left-close' + | 'panel-left-open' + | 'panel-right-close' + | 'panel-right-open' + | 'panel-bottom-open' + | 'panel-bottom-close' | 'error' | 'suspend' | 'undo' @@ -46,8 +52,10 @@ type Props = { type: IconType, }; +const materialIconsViewBox = '0 -960 960 960'; export default function ButtonIcon({className = '', type}: Props): React.Node { let pathData = null; + let viewBox = '0 0 24 24'; switch (type) { case 'add': pathData = PATH_ADD; @@ -121,6 +129,30 @@ export default function ButtonIcon({className = '', type}: Props): React.Node { case 'error': pathData = PATH_ERROR; break; + case 'panel-left-close': + pathData = PATH_MATERIAL_PANEL_LEFT_CLOSE; + viewBox = materialIconsViewBox; + break; + case 'panel-left-open': + pathData = PATH_MATERIAL_PANEL_LEFT_OPEN; + viewBox = materialIconsViewBox; + break; + case 'panel-right-close': + pathData = PATH_MATERIAL_PANEL_RIGHT_CLOSE; + viewBox = materialIconsViewBox; + break; + case 'panel-right-open': + pathData = PATH_MATERIAL_PANEL_RIGHT_OPEN; + viewBox = materialIconsViewBox; + break; + case 'panel-bottom-open': + pathData = PATH_MATERIAL_PANEL_BOTTOM_OPEN; + viewBox = materialIconsViewBox; + break; + case 'panel-bottom-close': + pathData = PATH_MATERIAL_PANEL_BOTTOM_CLOSE; + viewBox = materialIconsViewBox; + break; case 'suspend': pathData = PATH_SUSPEND; break; @@ -147,7 +179,7 @@ export default function ButtonIcon({className = '', type}: Props): React.Node { className={`${styles.ButtonIcon} ${className}`} width="24" height="24" - viewBox="0 0 24 24"> + viewBox={viewBox}> {typeof pathData === 'string' ? ( @@ -276,3 +308,33 @@ const PATH_VIEW_SOURCE = ` const PATH_EDITOR = ` M7 5h10v2h2V3c0-1.1-.9-1.99-2-1.99L7 1c-1.1 0-2 .9-2 2v4h2V5zm8.41 11.59L20 12l-4.59-4.59L14 8.83 17.17 12 14 15.17l1.41 1.42zM10 15.17L6.83 12 10 8.83 8.59 7.41 4 12l4.59 4.59L10 15.17zM17 19H7v-2H5v4c0 1.1.9 2 2 2h10c1.1 0 2-.9 2-2v-4h-2v2z `; + +// Source: Material Design Icons left_panel_close +const PATH_MATERIAL_PANEL_LEFT_CLOSE = ` + M648-324v-312L480-480l168 156ZM211-144q-27.64 0-47.32-19.68T144-211v-538q0-27.64 19.68-47.32T211-816h538q27.64 0 47.32 19.68T816-749v538q0 27.64-19.68 47.32T749-144H211Zm125-72v-528H216v528h120Zm72 0h336v-528H408v528Zm-72 0H216h120Z +`; + +// Source: Material Design Icons left_panel_open +const PATH_MATERIAL_PANEL_LEFT_OPEN = ` + M504-595v230q0 12.25 10.5 16.62Q525-344 534-352l110-102q11-11.18 11-26.09T644-506L534-608q-8.82-8-19.41-3.5T504-595ZM211-144q-27.64 0-47.32-19.68T144-211v-538q0-27.64 19.68-47.32T211-816h538q27.64 0 47.32 19.68T816-749v538q0 27.64-19.68 47.32T749-144H211Zm125-72v-528H216v528h120Zm72 0h336v-528H408v528Zm-72 0H216h120Z +`; + +// Source: Material Design Icons right_panel_close +const PATH_MATERIAL_PANEL_RIGHT_CLOSE = ` + M312-365q0 12.25 10.5 16.62Q333-344 342-352l110-102q11-11.18 11-26.09T452-506L342-608q-8.82-8-19.41-3.5T312-595v230ZM211-144q-27.64 0-47.32-19.68T144-211v-538q0-27.64 19.68-47.32T211-816h538q27.64 0 47.32 19.68T816-749v538q0 27.64-19.68 47.32T749-144H211Zm413-72h120v-528H624v528Zm-72 0v-528H216v528h336Zm72 0h120-120Z +`; + +// Source: Material Design Icons right_panel_open +const PATH_MATERIAL_PANEL_RIGHT_OPEN = ` + M456-365v-230q0-12.25-10.5-16.63Q435-616 426-608L316-506q-11 11.18-11 26.09T316-454l110 102q8.82 8 19.41 3.5T456-365ZM211-144q-27.64 0-47.32-19.68T144-211v-538q0-27.64 19.68-47.32T211-816h538q27.64 0 47.32 19.68T816-749v538q0 27.64-19.68 47.32T749-144H211Zm413-72h120v-528H624v528Zm-72 0v-528H216v528h336Zm72 0h120-120Z +`; + +// Source: Material Design Icons bottom_panel_open +const PATH_MATERIAL_PANEL_BOTTOM_OPEN = ` + M365-504h230q12.25 0 16.63-10.5Q616-525 608-534L506-644q-11.18-11-26.09-11T454-644L352-534q-8 8.82-3.5 19.41T365-504ZM211-144q-27.64 0-47.32-19.68T144-211v-538q0-27.64 19.68-47.32T211-816h538q27.64 0 47.32 19.68T816-749v538q0 27.64-19.68 47.32T749-144H211Zm5-192v120h528v-120H216Zm0-72h528v-336H216v336Zm0 72v120-120Z +`; + +// Source: Material Design Icons bottom_panel_close +const PATH_MATERIAL_PANEL_BOTTOM_CLOSE = ` + m506-508 102-110q8-8.82 3.5-19.41T595-648H365q-12.25 0-16.62 10.5Q344-627 352-618l102 110q11.18 11 26.09 11T506-508Zm243-308q27.64 0 47.32 19.68T816-749v538q0 27.64-19.68 47.32T749-144H211q-27.64 0-47.32-19.68T144-211v-538q0-27.64 19.68-47.32T211-816h538ZM216-336v120h528v-120H216Zm528-72v-336H216v336h528Zm-528 72v120-120Z +`; diff --git a/packages/react-devtools-shared/src/devtools/views/ButtonLabel.css b/packages/react-devtools-shared/src/devtools/views/ButtonLabel.css new file mode 100644 index 0000000000000..5a0656794568a --- /dev/null +++ b/packages/react-devtools-shared/src/devtools/views/ButtonLabel.css @@ -0,0 +1,7 @@ +.ButtonLabel { + padding-left: 1.5rem; + margin-left: -1rem; + user-select: none; + flex: 1 0 auto; + text-align: center; +} diff --git a/packages/react-devtools-shared/src/devtools/views/ButtonLabel.js b/packages/react-devtools-shared/src/devtools/views/ButtonLabel.js new file mode 100644 index 0000000000000..3c8cf42f27702 --- /dev/null +++ b/packages/react-devtools-shared/src/devtools/views/ButtonLabel.js @@ -0,0 +1,20 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import * as React from 'react'; + +import styles from './ButtonLabel.css'; + +type Props = { + children: React$Node, +}; + +export default function ButtonLabel({children}: Props): React.Node { + return {children}; +} diff --git a/packages/react-devtools-shared/src/devtools/views/Components/Components.css b/packages/react-devtools-shared/src/devtools/views/Components/Components.css index 5624f23489346..8df59f72f1654 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/Components.css +++ b/packages/react-devtools-shared/src/devtools/views/Components/Components.css @@ -16,7 +16,6 @@ .TreeWrapper { flex: 0 0 var(--horizontal-resize-percentage); - overflow: auto; } .InspectedElementWrapper { @@ -32,13 +31,20 @@ .ResizeBar { position: absolute; - left: -2px; + /* + * moving the bar out of its bounding box might cause its hitbox to overlap + * with another scrollbar creating disorienting UX where you both resize and scroll + * at the same time. + * If you adjust this value, double check that starting resize right on this edge + * doesn't also cause scroll + */ + left: 1px; width: 5px; height: 100%; cursor: ew-resize; } -@media screen and (max-width: 600px) { +@container devtools (width < 600px) { .Components { flex-direction: column; } @@ -52,7 +58,7 @@ } .ResizeBar { - top: -2px; + top: 1px; left: 0; width: 100%; height: 5px; diff --git a/packages/react-devtools-shared/src/devtools/views/Components/Components.js b/packages/react-devtools-shared/src/devtools/views/Components/Components.js index b7ce607685051..1f0927de98a5b 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/Components.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/Components.js @@ -24,12 +24,12 @@ import SettingsModal from 'react-devtools-shared/src/devtools/views/Settings/Set import {NativeStyleContextController} from './NativeStyleEditor/context'; import styles from './Components.css'; +import typeof {SyntheticPointerEvent} from 'react-dom-bindings/src/events/SyntheticEvent'; type Orientation = 'horizontal' | 'vertical'; type ResizeActionType = | 'ACTION_SET_DID_MOUNT' - | 'ACTION_SET_IS_RESIZING' | 'ACTION_SET_HORIZONTAL_PERCENTAGE' | 'ACTION_SET_VERTICAL_PERCENTAGE'; @@ -40,7 +40,6 @@ type ResizeAction = { type ResizeState = { horizontalPercentage: number, - isResizing: boolean, verticalPercentage: number, }; @@ -81,82 +80,81 @@ function Components(_: {}) { return () => clearTimeout(timeoutID); }, [horizontalPercentage, verticalPercentage]); - const {isResizing} = state; + const onResizeStart = (event: SyntheticPointerEvent) => { + const element = event.currentTarget; + element.setPointerCapture(event.pointerId); + }; + + const onResizeEnd = (event: SyntheticPointerEvent) => { + const element = event.currentTarget; + element.releasePointerCapture(event.pointerId); + }; + + const onResize = (event: SyntheticPointerEvent) => { + const element = event.currentTarget; + const isResizing = element.hasPointerCapture(event.pointerId); + if (!isResizing) { + return; + } - const onResizeStart = () => - dispatch({type: 'ACTION_SET_IS_RESIZING', payload: true}); + const resizeElement = resizeElementRef.current; + const wrapperElement = wrapperElementRef.current; - let onResize; - let onResizeEnd; - if (isResizing) { - onResizeEnd = () => - dispatch({type: 'ACTION_SET_IS_RESIZING', payload: false}); + if (wrapperElement === null || resizeElement === null) { + return; + } - // $FlowFixMe[missing-local-annot] - onResize = event => { - const resizeElement = resizeElementRef.current; - const wrapperElement = wrapperElementRef.current; + event.preventDefault(); - if (!isResizing || wrapperElement === null || resizeElement === null) { - return; - } + const orientation = getOrientation(wrapperElement); - event.preventDefault(); + const {height, width, left, top} = wrapperElement.getBoundingClientRect(); - const orientation = getOrientation(wrapperElement); + const currentMousePosition = + orientation === 'horizontal' ? event.clientX - left : event.clientY - top; - const {height, width, left, top} = wrapperElement.getBoundingClientRect(); + const boundaryMin = MINIMUM_SIZE; + const boundaryMax = + orientation === 'horizontal' + ? width - MINIMUM_SIZE + : height - MINIMUM_SIZE; - const currentMousePosition = - orientation === 'horizontal' - ? event.clientX - left - : event.clientY - top; + const isMousePositionInBounds = + currentMousePosition > boundaryMin && currentMousePosition < boundaryMax; - const boundaryMin = MINIMUM_SIZE; - const boundaryMax = + if (isMousePositionInBounds) { + const resizedElementDimension = + orientation === 'horizontal' ? width : height; + const actionType = orientation === 'horizontal' - ? width - MINIMUM_SIZE - : height - MINIMUM_SIZE; - - const isMousePositionInBounds = - currentMousePosition > boundaryMin && - currentMousePosition < boundaryMax; - - if (isMousePositionInBounds) { - const resizedElementDimension = - orientation === 'horizontal' ? width : height; - const actionType = - orientation === 'horizontal' - ? 'ACTION_SET_HORIZONTAL_PERCENTAGE' - : 'ACTION_SET_VERTICAL_PERCENTAGE'; - const percentage = - (currentMousePosition / resizedElementDimension) * 100; - - setResizeCSSVariable(resizeElement, orientation, percentage); - - dispatch({ - type: actionType, - payload: currentMousePosition / resizedElementDimension, - }); - } - }; - } + ? 'ACTION_SET_HORIZONTAL_PERCENTAGE' + : 'ACTION_SET_VERTICAL_PERCENTAGE'; + const percentage = (currentMousePosition / resizedElementDimension) * 100; + + setResizeCSSVariable(resizeElement, orientation, percentage); + + dispatch({ + type: actionType, + payload: currentMousePosition / resizedElementDimension, + }); + } + }; return ( -
    +
    -
    +
    @@ -176,7 +174,7 @@ function Components(_: {}) { const LOCAL_STORAGE_KEY = 'React::DevTools::createResizeReducer'; const VERTICAL_MODE_MAX_WIDTH = 600; -const MINIMUM_SIZE = 50; +const MINIMUM_SIZE = 100; function initResizeState(): ResizeState { let horizontalPercentage = 0.65; @@ -193,18 +191,12 @@ function initResizeState(): ResizeState { return { horizontalPercentage, - isResizing: false, verticalPercentage, }; } function resizeReducer(state: ResizeState, action: ResizeAction): ResizeState { switch (action.type) { - case 'ACTION_SET_IS_RESIZING': - return { - ...state, - isResizing: action.payload, - }; case 'ACTION_SET_HORIZONTAL_PERCENTAGE': return { ...state, @@ -243,4 +235,4 @@ function setResizeCSSVariable( } } -export default (portaledContent(Components): React$ComponentType<{}>); +export default (portaledContent(Components): component()); diff --git a/packages/react-devtools-shared/src/devtools/views/Components/Element.css b/packages/react-devtools-shared/src/devtools/views/Components/Element.css index b11e321e2e6d5..c25eddbdbb42c 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/Element.css +++ b/packages/react-devtools-shared/src/devtools/views/Components/Element.css @@ -1,7 +1,9 @@ .Element, +.HoveredElement, .InactiveSelectedElement, -.SelectedElement, -.HoveredElement { +.HighlightedElement, +.InactiveHighlightedElement, +.SelectedElement { color: var(--color-component-name); } .HoveredElement { @@ -10,8 +12,15 @@ .InactiveSelectedElement { background-color: var(--color-background-inactive); } +.HighlightedElement { + background-color: var(--color-selected-tree-highlight-active); +} +.InactiveHighlightedElement { + background-color: var(--color-selected-tree-highlight-inactive); +} .Wrapper { + position: relative; padding: 0 0.25rem; white-space: pre; height: var(--line-height-data); diff --git a/packages/react-devtools-shared/src/devtools/views/Components/Element.js b/packages/react-devtools-shared/src/devtools/views/Components/Element.js index 71e0ebfbe9cbe..00fae0951fb1e 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/Element.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/Element.js @@ -45,10 +45,6 @@ export default function Element({data, index, style}: Props): React.Node { const [isHovered, setIsHovered] = useState(false); - const {isNavigatingWithKeyboard, onElementMouseEnter, treeFocused} = data; - const id = element === null ? null : element.id; - const isSelected = inspectedElementID === id; - const errorsAndWarningsSubscription = useMemo( () => ({ getCurrentValue: () => @@ -68,6 +64,15 @@ export default function Element({data, index, style}: Props): React.Node { }>(errorsAndWarningsSubscription); const changeOwnerAction = useChangeOwnerAction(); + + // Handle elements that are removed from the tree while an async render is in progress. + if (element == null) { + console.warn(` Could not find element at index ${index}`); + + // This return needs to happen after hooks, since hooks can't be conditional. + return null; + } + const handleDoubleClick = () => { if (id !== null) { changeOwnerAction(id); @@ -75,8 +80,8 @@ export default function Element({data, index, style}: Props): React.Node { }; // $FlowFixMe[missing-local-annot] - const handleClick = ({metaKey}) => { - if (id !== null) { + const handleClick = ({metaKey, button}) => { + if (id !== null && button === 0) { logEvent({ event_name: 'select-element', metadata: {source: 'click-element'}, @@ -107,22 +112,29 @@ export default function Element({data, index, style}: Props): React.Node { event.preventDefault(); }; - // Handle elements that are removed from the tree while an async render is in progress. - if (element == null) { - console.warn(` Could not find element at index ${index}`); - - // This return needs to happen after hooks, since hooks can't be conditional. - return null; - } - const { + id, depth, displayName, hocDisplayNames, isStrictModeNonCompliant, key, + nameProp, compiledWithForget, } = element; + const { + isNavigatingWithKeyboard, + onElementMouseEnter, + treeFocused, + calculateElementOffset, + } = data; + + const isSelected = inspectedElementID === id; + const isDescendantOfSelected = + inspectedElementID !== null && + !isSelected && + store.isDescendantOf(inspectedElementID, id); + const elementOffset = calculateElementOffset(depth); // Only show strict mode non-compliance badges for top level elements. // Showing an inline badge for every element in the tree would be noisy. @@ -135,6 +147,10 @@ export default function Element({data, index, style}: Props): React.Node { : styles.InactiveSelectedElement; } else if (isHovered && !isNavigatingWithKeyboard) { className = styles.HoveredElement; + } else if (isDescendantOfSelected) { + className = treeFocused + ? styles.HighlightedElement + : styles.InactiveHighlightedElement; } return ( @@ -144,17 +160,13 @@ export default function Element({data, index, style}: Props): React.Node { onMouseLeave={handleMouseLeave} onMouseDown={handleClick} onDoubleClick={handleDoubleClick} - style={style} - data-testname="ComponentTreeListItem" - data-depth={depth}> + style={{ + ...style, + paddingLeft: elementOffset, + }} + data-testname="ComponentTreeListItem"> {/* This wrapper is used by Tree for measurement purposes. */} -
    +
    {ownerID === null && ( )} @@ -168,7 +180,24 @@ export default function Element({data, index, style}: Props): React.Node { className={styles.KeyValue} title={key} onDoubleClick={handleKeyDoubleClick}> -
    {key}
    +
    +                
    +              
    + + " + + )} + + {nameProp && ( + +  name=" + +
    +                
    +              
    "
    diff --git a/packages/react-devtools-shared/src/devtools/views/Components/ElementBadges.js b/packages/react-devtools-shared/src/devtools/views/Components/ElementBadges.js index a829ad0153aa7..5a3355c60c491 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/ElementBadges.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/ElementBadges.js @@ -16,18 +16,21 @@ import styles from './ElementBadges.css'; type Props = { hocDisplayNames: Array | null, + environmentName: string | null, compiledWithForget: boolean, className?: string, }; export default function ElementBadges({ compiledWithForget, + environmentName, hocDisplayNames, className = '', }: Props): React.Node { if ( !compiledWithForget && - (hocDisplayNames == null || hocDisplayNames.length === 0) + (hocDisplayNames == null || hocDisplayNames.length === 0) && + environmentName == null ) { return null; } @@ -36,6 +39,8 @@ export default function ElementBadges({
    {compiledWithForget && } + {environmentName != null ? {environmentName} : null} + {hocDisplayNames != null && hocDisplayNames.length > 0 && ( {hocDisplayNames[0]} )} diff --git a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElement.js b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElement.js index de9f3490190d9..200f78586c51a 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElement.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElement.js @@ -7,6 +7,8 @@ * @flow */ +import type {SourceMappedLocation} from 'react-devtools-shared/src/symbolicateSource'; + import * as React from 'react'; import {useCallback, useContext, useSyncExternalStore} from 'react'; import {TreeStateContext} from './TreeContext'; @@ -18,22 +20,22 @@ import Toggle from '../Toggle'; import {ElementTypeSuspense} from 'react-devtools-shared/src/frontend/types'; import InspectedElementView from './InspectedElementView'; import {InspectedElementContext} from './InspectedElementContext'; -import {getOpenInEditorURL} from '../../../utils'; -import {LOCAL_STORAGE_OPEN_IN_EDITOR_URL} from '../../../constants'; +import {getAlwaysOpenInEditor} from '../../../utils'; +import {LOCAL_STORAGE_ALWAYS_OPEN_IN_EDITOR} from '../../../constants'; import FetchFileWithCachingContext from './FetchFileWithCachingContext'; import {symbolicateSourceWithCache} from 'react-devtools-shared/src/symbolicateSource'; import OpenInEditorButton from './OpenInEditorButton'; import InspectedElementViewSourceButton from './InspectedElementViewSourceButton'; -import Skeleton from './Skeleton'; +import useEditorURL from '../useEditorURL'; import styles from './InspectedElement.css'; -import type {Source} from 'react-devtools-shared/src/shared/types'; - export type Props = {}; // TODO Make edits and deletes also use transition API! +const noSourcePromise = Promise.resolve(null); + export default function InspectedElementWrapper(_: Props): React.Node { const {inspectedElementID} = useContext(TreeStateContext); const bridge = useContext(BridgeContext); @@ -50,22 +52,29 @@ export default function InspectedElementWrapper(_: Props): React.Node { const fetchFileWithCaching = useContext(FetchFileWithCachingContext); - const symbolicatedSourcePromise: null | Promise = + const source = + inspectedElement == null + ? null + : inspectedElement.source != null + ? inspectedElement.source + : inspectedElement.stack != null && inspectedElement.stack.length > 0 + ? inspectedElement.stack[0] + : null; + + const symbolicatedSourcePromise: Promise = React.useMemo(() => { - if (inspectedElement == null) return null; - if (fetchFileWithCaching == null) return Promise.resolve(null); + if (fetchFileWithCaching == null) return noSourcePromise; - const {source} = inspectedElement; - if (source == null) return Promise.resolve(null); + if (source == null) return noSourcePromise; - const {sourceURL, line, column} = source; + const [, sourceURL, line, column] = source; return symbolicateSourceWithCache( fetchFileWithCaching, sourceURL, line, column, ); - }, [inspectedElement]); + }, [source]); const element = inspectedElementID !== null @@ -106,7 +115,7 @@ export default function InspectedElementWrapper(_: Props): React.Node { element !== null && element.type === ElementTypeSuspense && inspectedElement != null && - inspectedElement.state != null; + inspectedElement.isSuspended; const canToggleError = !hideToggleErrorAction && @@ -118,18 +127,21 @@ export default function InspectedElementWrapper(_: Props): React.Node { inspectedElement != null && inspectedElement.canToggleSuspense; - const editorURL = useSyncExternalStore( - function subscribe(callback) { - window.addEventListener(LOCAL_STORAGE_OPEN_IN_EDITOR_URL, callback); + const alwaysOpenInEditor = useSyncExternalStore( + useCallback(function subscribe(callback) { + window.addEventListener(LOCAL_STORAGE_ALWAYS_OPEN_IN_EDITOR, callback); return function unsubscribe() { - window.removeEventListener(LOCAL_STORAGE_OPEN_IN_EDITOR_URL, callback); + window.removeEventListener( + LOCAL_STORAGE_ALWAYS_OPEN_IN_EDITOR, + callback, + ); }; - }, - function getState() { - return getOpenInEditorURL(); - }, + }, []), + getAlwaysOpenInEditor, ); + const editorURL = useEditorURL(); + const toggleErrored = useCallback(() => { if (inspectedElement == null) { return; @@ -192,7 +204,9 @@ export default function InspectedElementWrapper(_: Props): React.Node { } return ( -
    +
    {strictModeBadge} @@ -217,17 +231,15 @@ export default function InspectedElementWrapper(_: Props): React.Node {
    - {!!editorURL && - inspectedElement != null && - inspectedElement.source != null && + {!alwaysOpenInEditor && + !!editorURL && + source != null && symbolicatedSourcePromise != null && ( - }> - - + )} {canToggleError && ( @@ -271,8 +283,7 @@ export default function InspectedElementWrapper(_: Props): React.Node { {!hideViewSourceAction && ( )} @@ -282,11 +293,8 @@ export default function InspectedElementWrapper(_: Props): React.Node {
    Loading...
    )} - {inspectedElement !== null && symbolicatedSourcePromise != null && ( + {inspectedElement !== null && ( {name} - {!!hookName && ({hookName})} + { + // $FlowFixMe[constant-condition] + !!hookName && ({hookName}) + } ) : ( name @@ -386,6 +389,6 @@ function HookView({ } } -export default (React.memo( - InspectedElementHooksTree, -): React.ComponentType); +export default (React.memo(InspectedElementHooksTree): component( + ...props: HookViewProps +)); diff --git a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementSharedStyles.css b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementSharedStyles.css index e9916d467cfa8..978077d2d9a24 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementSharedStyles.css +++ b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementSharedStyles.css @@ -51,3 +51,91 @@ .EditableValue { min-width: 1rem; } + +.InfoRow { + border-top: 1px solid var(--color-border); + padding: 0.5rem 1rem; +} + +.InfoRow:last-child { + margin-bottom: -0.25rem; +} + +.CollapsableRow { + border-top: 1px solid var(--color-border); +} + +.CollapsableRow:last-child { + margin-bottom: -0.25rem; +} + +.CollapsableHeader { + width: 100%; + padding: 0.25rem; + display: flex; +} + +.CollapsableHeaderIcon { + flex: 0 0 1rem; + margin-left: -0.25rem; + width: 1rem; + height: 1rem; + padding: 0; + color: var(--color-expand-collapse-toggle); +} + +.CollapsableHeaderTitle, .CollapsableHeaderDescription, .CollapsableHeaderSeparator, .CollapsableHeaderFiller { + font-family: var(--font-family-monospace); + font-size: var(--font-size-monospace-normal); + text-align: left; + white-space: nowrap; +} +.CollapsableHeaderTitle { + flex: 0 1 auto; + overflow: hidden; + text-overflow: ellipsis; +} + +.CollapsableHeaderSeparator { + flex: 0 0 auto; + white-space: pre; +} + +.CollapsableHeaderFiller { + flex: 1 0 0; +} + +.CollapsableContent { + margin-top: -0.25rem; +} + +.PreviewContainer { + padding: 0.25rem; +} + +.TimeBarContainer { + position: relative; + flex: 0 0 20%; + height: 0.25rem; + border-radius: 0.125rem; + background-color: var(--color-timebar-background); +} + +.TimeBarSpan, .TimeBarSpanErrored { + position: absolute; + border-radius: 0.125rem; + background-color: var(--color-timespan-background); + width: 100%; + height: 100%; +} + +.TimeBarSpanErrored { + background-color: var(--color-timespan-background-errored); +} + +.SmallHeader { + font-family: var(--font-family-monospace); + font-size: var(--font-size-monospace-normal); + padding-left: 1.25rem; + margin-top: 0.25rem; +} diff --git a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementSourcePanel.css b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementSourcePanel.css index 444e070c37e77..3c96c2bf2cf69 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementSourcePanel.css +++ b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementSourcePanel.css @@ -18,3 +18,18 @@ max-width: 100%; margin-left: 1rem; } + +.Link { + color: var(--color-link); + white-space: pre; + overflow: hidden; + text-overflow: ellipsis; + flex: 1; + cursor: pointer; + border-radius: 0.125rem; + padding: 0px 2px; +} + +.Link:hover { + background-color: var(--color-background-hover); +} diff --git a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementSourcePanel.js b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementSourcePanel.js index 0f7203b12a750..9faf6c1d7abcc 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementSourcePanel.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementSourcePanel.js @@ -9,19 +9,23 @@ import * as React from 'react'; import {copy} from 'clipboard-js'; -import {toNormalUrl} from 'jsc-safe-url'; import Button from '../Button'; import ButtonIcon from '../ButtonIcon'; import Skeleton from './Skeleton'; import {withPermissionsCheck} from 'react-devtools-shared/src/frontend/utils/withPermissionsCheck'; -import type {Source as InspectedElementSource} from 'react-devtools-shared/src/shared/types'; +import useOpenResource from '../useOpenResource'; + +import type {SourceMappedLocation} from 'react-devtools-shared/src/symbolicateSource'; +import type {ReactFunctionLocation} from 'shared/ReactTypes'; import styles from './InspectedElementSourcePanel.css'; +import formatLocationForDisplay from './formatLocationForDisplay'; + type Props = { - source: InspectedElementSource, - symbolicatedSourcePromise: Promise, + source: ReactFunctionLocation, + symbolicatedSourcePromise: Promise, }; function InspectedElementSourcePanel({ @@ -33,7 +37,12 @@ function InspectedElementSourcePanel({
    source
    - }> + + + + }> copy(`${sourceURL}:${line}:${column}`), @@ -72,7 +81,7 @@ function CopySourceButton({source, symbolicatedSourcePromise}: Props) { ); } - const {sourceURL, line, column} = symbolicatedSource; + const [, sourceURL, line, column] = symbolicatedSource.location; const handleCopy = withPermissionsCheck( {permissions: ['clipboardWrite']}, () => copy(`${sourceURL}:${line}:${column}`), @@ -87,57 +96,27 @@ function CopySourceButton({source, symbolicatedSourcePromise}: Props) { function FormattedSourceString({source, symbolicatedSourcePromise}: Props) { const symbolicatedSource = React.use(symbolicatedSourcePromise); - if (symbolicatedSource == null) { - const {sourceURL, line} = source; - return ( -
    - {formatSourceForDisplay(sourceURL, line)} -
    - ); - } + const [linkIsEnabled, viewSource] = useOpenResource( + source, + symbolicatedSource == null ? null : symbolicatedSource.location, + ); - const {sourceURL, line} = symbolicatedSource; + const [, sourceURL, line, column] = + symbolicatedSource == null ? source : symbolicatedSource.location; return (
    - {formatSourceForDisplay(sourceURL, line)} + + {formatLocationForDisplay(sourceURL, line, column)} +
    ); } -// This function is based on describeComponentFrame() in packages/shared/ReactComponentStackFrame -function formatSourceForDisplay(sourceURL: string, line: number) { - // Metro can return JSC-safe URLs, which have `//&` as a delimiter - // https://www.npmjs.com/package/jsc-safe-url - const sanitizedSourceURL = sourceURL.includes('//&') - ? toNormalUrl(sourceURL) - : sourceURL; - - // Note: this RegExp doesn't work well with URLs from Metro, - // which provides bundle URL with query parameters prefixed with /& - const BEFORE_SLASH_RE = /^(.*)[\\\/]/; - - let nameOnly = sanitizedSourceURL.replace(BEFORE_SLASH_RE, ''); - - // In DEV, include code for a common special case: - // prefer "folder/index.js" instead of just "index.js". - if (/^index\./.test(nameOnly)) { - const match = sanitizedSourceURL.match(BEFORE_SLASH_RE); - if (match) { - const pathBeforeSlash = match[1]; - if (pathBeforeSlash) { - const folderName = pathBeforeSlash.replace(BEFORE_SLASH_RE, ''); - nameOnly = folderName + '/' + nameOnly; - } - } - } - - return `${nameOnly}:${line}`; -} - export default InspectedElementSourcePanel; diff --git a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementSuspendedBy.js b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementSuspendedBy.js new file mode 100644 index 0000000000000..a9d22b5a881be --- /dev/null +++ b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementSuspendedBy.js @@ -0,0 +1,405 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import {copy} from 'clipboard-js'; +import * as React from 'react'; +import {useState} from 'react'; +import Button from '../Button'; +import ButtonIcon from '../ButtonIcon'; +import KeyValue from './KeyValue'; +import {serializeDataForCopy} from '../utils'; +import Store from '../../store'; +import styles from './InspectedElementSharedStyles.css'; +import {withPermissionsCheck} from 'react-devtools-shared/src/frontend/utils/withPermissionsCheck'; +import StackTraceView from './StackTraceView'; +import OwnerView from './OwnerView'; +import {meta} from '../../../hydration'; +import useInferredName from '../useInferredName'; + +import type { + InspectedElement, + SerializedAsyncInfo, +} from 'react-devtools-shared/src/frontend/types'; +import type {FrontendBridge} from 'react-devtools-shared/src/bridge'; + +import { + UNKNOWN_SUSPENDERS_NONE, + UNKNOWN_SUSPENDERS_REASON_PRODUCTION, + UNKNOWN_SUSPENDERS_REASON_OLD_VERSION, + UNKNOWN_SUSPENDERS_REASON_THROWN_PROMISE, +} from '../../../constants'; + +type RowProps = { + bridge: FrontendBridge, + element: Element, + inspectedElement: InspectedElement, + store: Store, + asyncInfo: SerializedAsyncInfo, + index: number, + minTime: number, + maxTime: number, +}; + +function getShortDescription(name: string, description: string): string { + const descMaxLength = 30 - name.length; + if (descMaxLength > 1) { + const l = description.length; + if (l > 0 && l <= descMaxLength) { + // We can fit the full description + return description; + } else if ( + description.startsWith('http://') || + description.startsWith('https://') || + description.startsWith('/') + ) { + // Looks like a URL. Let's see if we can extract something shorter. + // We don't have to do a full parse so let's try something cheaper. + let queryIdx = description.indexOf('?'); + if (queryIdx === -1) { + queryIdx = description.length; + } + if (description.charCodeAt(queryIdx - 1) === 47 /* "/" */) { + // Ends with slash. Look before that. + queryIdx--; + } + const slashIdx = description.lastIndexOf('/', queryIdx - 1); + // This may now be either the file name or the host. + // Include the slash to make it more obvious what we trimmed. + return '…' + description.slice(slashIdx, queryIdx); + } + } + return ''; +} + +function formatBytes(bytes: number) { + if (bytes < 1_000) { + return bytes + ' bytes'; + } + if (bytes < 1_000_000) { + return (bytes / 1_000).toFixed(1) + ' kB'; + } + if (bytes < 1_000_000_000) { + return (bytes / 1_000_000).toFixed(1) + ' mB'; + } + return (bytes / 1_000_000_000).toFixed(1) + ' gB'; +} + +function SuspendedByRow({ + bridge, + element, + inspectedElement, + store, + asyncInfo, + index, + minTime, + maxTime, +}: RowProps) { + const [isOpen, setIsOpen] = useState(false); + const ioInfo = asyncInfo.awaited; + const name = useInferredName(asyncInfo); + const description = ioInfo.description; + const longName = description === '' ? name : name + ' (' + description + ')'; + const shortDescription = getShortDescription(name, description); + const start = ioInfo.start; + const end = ioInfo.end; + const timeScale = 100 / (maxTime - minTime); + let left = (start - minTime) * timeScale; + let width = (end - start) * timeScale; + if (width < 5) { + // Use at least a 5% width to avoid showing too small indicators. + width = 5; + if (left > 95) { + left = 95; + } + } + + const ioOwner = ioInfo.owner; + const asyncOwner = asyncInfo.owner; + const showIOStack = ioInfo.stack !== null && ioInfo.stack.length !== 0; + // Only show the awaited stack if the I/O started in a different owner + // than where it was awaited. If it's started by the same component it's + // probably easy enough to infer and less noise in the common case. + const canShowAwaitStack = + (asyncInfo.stack !== null && asyncInfo.stack.length > 0) || + (asyncOwner !== null && asyncOwner.id !== inspectedElement.id); + const showAwaitStack = + canShowAwaitStack && + (!showIOStack || + (ioOwner === null + ? asyncOwner !== null + : asyncOwner === null || ioOwner.id !== asyncOwner.id)); + + const value: any = ioInfo.value; + const metaName = + value !== null && typeof value === 'object' ? value[meta.name] : null; + const isFulfilled = metaName === 'fulfilled Thenable'; + const isRejected = metaName === 'rejected Thenable'; + return ( +
    + + {isOpen && ( +
    + {showIOStack && ( + + )} + {ioOwner !== null && + ioOwner.id !== inspectedElement.id && + (showIOStack || + !showAwaitStack || + asyncOwner === null || + ioOwner.id !== asyncOwner.id) ? ( + + ) : null} + {showAwaitStack ? ( + <> +
    awaited at:
    + {asyncInfo.stack !== null && asyncInfo.stack.length > 0 && ( + + )} + {asyncOwner !== null && asyncOwner.id !== inspectedElement.id ? ( + + ) : null} + + ) : null} +
    +
    +
    + )} +
    + ); +} + +type Props = { + bridge: FrontendBridge, + element: Element, + inspectedElement: InspectedElement, + store: Store, +}; + +function compareTime(a: SerializedAsyncInfo, b: SerializedAsyncInfo): number { + const ioA = a.awaited; + const ioB = b.awaited; + if (ioA.start === ioB.start) { + return ioA.end - ioB.end; + } + return ioA.start - ioB.start; +} + +export default function InspectedElementSuspendedBy({ + bridge, + element, + inspectedElement, + store, +}: Props): React.Node { + const {suspendedBy, suspendedByRange} = inspectedElement; + + // Skip the section if nothing suspended this component. + if ( + (suspendedBy == null || suspendedBy.length === 0) && + inspectedElement.unknownSuspenders === UNKNOWN_SUSPENDERS_NONE + ) { + return null; + } + + const handleCopy = withPermissionsCheck( + {permissions: ['clipboardWrite']}, + () => copy(serializeDataForCopy(suspendedBy)), + ); + + let minTime = Infinity; + let maxTime = -Infinity; + if (suspendedByRange !== null) { + // The range of the whole suspense boundary. + minTime = suspendedByRange[0]; + maxTime = suspendedByRange[1]; + } + for (let i = 0; i < suspendedBy.length; i++) { + const asyncInfo: SerializedAsyncInfo = suspendedBy[i]; + if (asyncInfo.awaited.start < minTime) { + minTime = asyncInfo.awaited.start; + } + if (asyncInfo.awaited.end > maxTime) { + maxTime = asyncInfo.awaited.end; + } + } + + if (maxTime - minTime < 25) { + // Stretch the time span a bit to ensure that we don't show + // large bars that represent very small timespans. + minTime = maxTime - 25; + } + + const sortedSuspendedBy = suspendedBy === null ? [] : suspendedBy.slice(0); + sortedSuspendedBy.sort(compareTime); + + let unknownSuspenders = null; + switch (inspectedElement.unknownSuspenders) { + case UNKNOWN_SUSPENDERS_REASON_PRODUCTION: + unknownSuspenders = ( +
    + Something suspended but we don't know the exact reason in production + builds of React. Test this in development mode to see exactly what + might suspend. +
    + ); + break; + case UNKNOWN_SUSPENDERS_REASON_OLD_VERSION: + unknownSuspenders = ( +
    + Something suspended but we don't track all the necessary information + in older versions of React. Upgrade to the latest version of React to + see exactly what might suspend. +
    + ); + break; + case UNKNOWN_SUSPENDERS_REASON_THROWN_PROMISE: + unknownSuspenders = ( +
    + Something threw a Promise to suspend this boundary. It's likely an + outdated version of a library that doesn't yet fully take advantage of + use(). Upgrade your data fetching library to see exactly what might + suspend. +
    + ); + break; + } + + return ( +
    +
    +
    suspended by
    + +
    + {sortedSuspendedBy.map((asyncInfo, index) => ( + + ))} + {unknownSuspenders} +
    + ); +} diff --git a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementSuspenseToggle.js b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementSuspenseToggle.js index 3b445505cbcc9..ebb4b5bcd5439 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementSuspenseToggle.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementSuspenseToggle.js @@ -30,15 +30,13 @@ export default function InspectedElementSuspenseToggle({ }: Props): React.Node { const {readOnly} = React.useContext(OptionsContext); - const {id, state, type} = inspectedElement; + const {id, isSuspended, type} = inspectedElement; const canToggleSuspense = !readOnly && inspectedElement.canToggleSuspense; if (type !== ElementTypeSuspense) { return null; } - const isSuspended = state !== null; - const toggleSuspense = (path: any, value: boolean) => { const rendererID = store.getRendererIDForElement(id); if (rendererID !== null) { diff --git a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementView.css b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementView.css index 57dc15d8b3d9f..12c20cc954833 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementView.css +++ b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementView.css @@ -2,16 +2,6 @@ font-family: var(--font-family-sans); } -.Owner { - color: var(--color-component-name); - font-family: var(--font-family-monospace); - font-size: var(--font-size-monospace-normal); - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - max-width: 100%; -} - .InspectedElement { overflow-x: hidden; overflow-y: auto; @@ -28,44 +18,13 @@ } } -.Owner { - border-radius: 0.25rem; - padding: 0.125rem 0.25rem; - background: none; - border: none; - display: block; -} -.Owner:focus { - outline: none; - background-color: var(--color-button-background-focus); -} - -.NotInStore { - color: var(--color-dim); - cursor: default; -} - -.OwnerButton { - cursor: pointer; - width: 100%; - padding: 0; -} - -.OwnerContent { - display: flex; - align-items: center; - padding-left: 1rem; - width: 100%; - border-radius: 0.25rem; -} - -.OwnerContent:hover { - background-color: var(--color-background-hover); -} - .OwnersMetaField { padding-left: 1.25rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } + +.RenderedBySkeleton { + padding-left: 1.25rem; +} \ No newline at end of file diff --git a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementView.js b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementView.js index 7e18ad5cd715c..7e2175db741f2 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementView.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementView.js @@ -8,10 +8,8 @@ */ import * as React from 'react'; -import {Fragment, useCallback, useContext} from 'react'; -import {TreeDispatcherContext} from './TreeContext'; +import {Fragment, useContext} from 'react'; import {BridgeContext, StoreContext} from '../context'; -import Button from '../Button'; import InspectedElementBadges from './InspectedElementBadges'; import InspectedElementContextTree from './InspectedElementContextTree'; import InspectedElementErrorsAndWarningsTree from './InspectedElementErrorsAndWarningsTree'; @@ -20,12 +18,13 @@ import InspectedElementPropsTree from './InspectedElementPropsTree'; import InspectedElementStateTree from './InspectedElementStateTree'; import InspectedElementStyleXPlugin from './InspectedElementStyleXPlugin'; import InspectedElementSuspenseToggle from './InspectedElementSuspenseToggle'; +import InspectedElementSuspendedBy from './InspectedElementSuspendedBy'; import NativeStyleEditor from './NativeStyleEditor'; -import ElementBadges from './ElementBadges'; -import {useHighlightHostInstance} from '../hooks'; import {enableStyleXFeatures} from 'react-devtools-feature-flags'; -import {logEvent} from 'react-devtools-shared/src/Logger'; import InspectedElementSourcePanel from './InspectedElementSourcePanel'; +import StackTraceView from './StackTraceView'; +import OwnerView from './OwnerView'; +import Skeleton from './Skeleton'; import styles from './InspectedElementView.css'; @@ -35,7 +34,7 @@ import type { } from 'react-devtools-shared/src/frontend/types'; import type {HookNames} from 'react-devtools-shared/src/frontend/types'; import type {ToggleParseHookNames} from './InspectedElementContext'; -import type {Source} from 'react-devtools-shared/src/shared/types'; +import type {SourceMappedLocation} from 'react-devtools-shared/src/symbolicateSource'; type Props = { element: Element, @@ -43,7 +42,7 @@ type Props = { inspectedElement: InspectedElement, parseHookNames: boolean, toggleParseHookNames: ToggleParseHookNames, - symbolicatedSourcePromise: Promise, + symbolicatedSourcePromise: Promise, }; export default function InspectedElementView({ @@ -55,6 +54,7 @@ export default function InspectedElementView({ symbolicatedSourcePromise, }: Props): React.Node { const { + stack, owners, rendererPackageName, rendererVersion, @@ -71,8 +71,9 @@ export default function InspectedElementView({ ? `${rendererPackageName}@${rendererVersion}` : null; const showOwnersList = owners !== null && owners.length > 0; + const showStack = stack != null && stack.length > 0; const showRenderedBy = - showOwnersList || rendererLabel !== null || rootType !== null; + showStack || showOwnersList || rendererLabel !== null || rootType !== null; return ( @@ -156,31 +157,54 @@ export default function InspectedElementView({
    +
    + +
    + {showRenderedBy && (
    rendered by
    - - {showOwnersList && - owners?.map(owner => ( - - ))} - - {rootType !== null && ( -
    {rootType}
    - )} - {rendererLabel !== null && ( -
    {rendererLabel}
    - )} + + +
    + }> + {showStack ? : null} + {showOwnersList && + owners?.map(owner => ( + + + {owner.stack != null && owner.stack.length > 0 ? ( + + ) : null} + + ))} + + {rootType !== null && ( +
    {rootType}
    + )} + {rendererLabel !== null && ( +
    {rendererLabel}
    + )} +
    )} @@ -196,57 +220,3 @@ export default function InspectedElementView({ ); } - -type OwnerViewProps = { - displayName: string, - hocDisplayNames: Array | null, - compiledWithForget: boolean, - id: number, - isInStore: boolean, -}; - -function OwnerView({ - displayName, - hocDisplayNames, - compiledWithForget, - id, - isInStore, -}: OwnerViewProps) { - const dispatch = useContext(TreeDispatcherContext); - const {highlightHostInstance, clearHighlightHostInstance} = - useHighlightHostInstance(); - - const handleClick = useCallback(() => { - logEvent({ - event_name: 'select-element', - metadata: {source: 'owner-view'}, - }); - dispatch({ - type: 'SELECT_ELEMENT_BY_ID', - payload: id, - }); - }, [dispatch, id]); - - return ( - - ); -} diff --git a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementViewSourceButton.js b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementViewSourceButton.js index 31c6d1e557771..7170c2494f9c2 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementViewSourceButton.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementViewSourceButton.js @@ -11,79 +11,53 @@ import * as React from 'react'; import ButtonIcon from '../ButtonIcon'; import Button from '../Button'; -import ViewElementSourceContext from './ViewElementSourceContext'; -import Skeleton from './Skeleton'; -import type {Source as InspectedElementSource} from 'react-devtools-shared/src/shared/types'; -import type { - CanViewElementSource, - ViewElementSource, -} from 'react-devtools-shared/src/devtools/views/DevTools'; +import type {ReactFunctionLocation} from 'shared/ReactTypes'; +import type {SourceMappedLocation} from 'react-devtools-shared/src/symbolicateSource'; -const {useCallback, useContext} = React; +import useOpenResource from '../useOpenResource'; type Props = { - canViewSource: ?boolean, - source: ?InspectedElementSource, - symbolicatedSourcePromise: Promise | null, + source: null | ReactFunctionLocation, + symbolicatedSourcePromise: Promise | null, }; function InspectedElementViewSourceButton({ - canViewSource, source, symbolicatedSourcePromise, }: Props): React.Node { - const {canViewElementSourceFunction, viewElementSourceFunction} = useContext( - ViewElementSourceContext, - ); - return ( - }> + + + + }> ); } type ActualSourceButtonProps = { - canViewSource: ?boolean, - source: ?InspectedElementSource, - symbolicatedSourcePromise: Promise | null, - canViewElementSourceFunction: CanViewElementSource | null, - viewElementSourceFunction: ViewElementSource | null, + source: null | ReactFunctionLocation, + symbolicatedSourcePromise: Promise | null, }; function ActualSourceButton({ - canViewSource, source, symbolicatedSourcePromise, - canViewElementSourceFunction, - viewElementSourceFunction, }: ActualSourceButtonProps): React.Node { const symbolicatedSource = symbolicatedSourcePromise == null ? null : React.use(symbolicatedSourcePromise); - // In some cases (e.g. FB internal usage) the standalone shell might not be able to view the source. - // To detect this case, we defer to an injected helper function (if present). - const buttonIsEnabled = - !!canViewSource && - viewElementSourceFunction != null && - source != null && - (canViewElementSourceFunction == null || - canViewElementSourceFunction(source, symbolicatedSource)); - - const viewSource = useCallback(() => { - if (viewElementSourceFunction != null && source != null) { - viewElementSourceFunction(source, symbolicatedSource); - } - }, [source, symbolicatedSource]); - + const [buttonIsEnabled, viewSource] = useOpenResource( + source, + symbolicatedSource == null ? null : symbolicatedSource.location, + ); return ( - {name} + + {name} + : + ); } diff --git a/packages/react-devtools-shared/src/devtools/views/Components/OpenInEditorButton.js b/packages/react-devtools-shared/src/devtools/views/Components/OpenInEditorButton.js index c63e8319b83f8..b739a0602819d 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/OpenInEditorButton.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/OpenInEditorButton.js @@ -4,6 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * + * @flow */ import * as React from 'react'; @@ -11,61 +12,18 @@ import * as React from 'react'; import Button from 'react-devtools-shared/src/devtools/views/Button'; import ButtonIcon from 'react-devtools-shared/src/devtools/views/ButtonIcon'; -import type {Source} from 'react-devtools-shared/src/shared/types'; +import type {ReactFunctionLocation} from 'shared/ReactTypes'; +import type {SourceMappedLocation} from 'react-devtools-shared/src/symbolicateSource'; + +import {checkConditions} from '../Editor/utils'; type Props = { editorURL: string, - source: Source, - symbolicatedSourcePromise: Promise, + source: ReactFunctionLocation, + symbolicatedSourcePromise: Promise, }; -function checkConditions( - editorURL: string, - source: Source, -): {url: URL | null, shouldDisableButton: boolean} { - try { - const url = new URL(editorURL); - - let sourceURL = source.sourceURL; - - // Check if sourceURL is a correct URL, which has a protocol specified - if (sourceURL.includes('://')) { - if (!__IS_INTERNAL_VERSION__) { - // In this case, we can't really determine the path to a file, disable a button - return {url: null, shouldDisableButton: true}; - } else { - const endOfSourceMapURLPattern = '.js/'; - const endOfSourceMapURLIndex = sourceURL.lastIndexOf( - endOfSourceMapURLPattern, - ); - - if (endOfSourceMapURLIndex === -1) { - return {url: null, shouldDisableButton: true}; - } else { - sourceURL = sourceURL.slice( - endOfSourceMapURLIndex + endOfSourceMapURLPattern.length, - sourceURL.length, - ); - } - } - } - - const lineNumberAsString = String(source.line); - - url.href = url.href - .replace('{path}', sourceURL) - .replace('{line}', lineNumberAsString) - .replace('%7Bpath%7D', sourceURL) - .replace('%7Bline%7D', lineNumberAsString); - - return {url, shouldDisableButton: false}; - } catch (e) { - // User has provided incorrect editor url - return {url: null, shouldDisableButton: true}; - } -} - -function OpenInEditorButton({ +function OpenSymbolicatedSourceInEditorButton({ editorURL, source, symbolicatedSourcePromise, @@ -74,7 +32,7 @@ function OpenInEditorButton({ const {url, shouldDisableButton} = checkConditions( editorURL, - symbolicatedSource ? symbolicatedSource : source, + symbolicatedSource ? symbolicatedSource.location : source, ); return ( @@ -87,4 +45,17 @@ function OpenInEditorButton({ ); } +function OpenInEditorButton(props: Props): React.Node { + return ( + + + + }> + + + ); +} + export default OpenInEditorButton; diff --git a/packages/react-devtools-shared/src/devtools/views/Components/OwnerView.css b/packages/react-devtools-shared/src/devtools/views/Components/OwnerView.css new file mode 100644 index 0000000000000..985fe4b9fb5d5 --- /dev/null +++ b/packages/react-devtools-shared/src/devtools/views/Components/OwnerView.css @@ -0,0 +1,42 @@ +.Owner { + color: var(--color-component-name); + font-family: var(--font-family-monospace); + font-size: var(--font-size-monospace-normal); + font-weight: bold; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: 100%; + border-radius: 0.25rem; + padding: 0.125rem 0.25rem; + background: none; + border: none; + display: block; +} +.Owner:focus { + outline: none; + background-color: var(--color-button-background-focus); +} + +.OwnerButton { + cursor: pointer; + width: 100%; + padding: 0; +} + +.OwnerContent { + display: flex; + align-items: center; + padding-left: 1rem; + width: 100%; + border-radius: 0.25rem; +} + +.OwnerContent:hover { + background-color: var(--color-background-hover); +} + +.NotInStore { + color: var(--color-dim); + cursor: default; +} diff --git a/packages/react-devtools-shared/src/devtools/views/Components/OwnerView.js b/packages/react-devtools-shared/src/devtools/views/Components/OwnerView.js new file mode 100644 index 0000000000000..2b0f4b035a261 --- /dev/null +++ b/packages/react-devtools-shared/src/devtools/views/Components/OwnerView.js @@ -0,0 +1,76 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import * as React from 'react'; +import {useCallback, useContext} from 'react'; +import {TreeDispatcherContext} from './TreeContext'; +import Button from '../Button'; +import ElementBadges from './ElementBadges'; +import {useHighlightHostInstance} from '../hooks'; +import {logEvent} from 'react-devtools-shared/src/Logger'; + +import styles from './OwnerView.css'; + +type OwnerViewProps = { + displayName: string, + hocDisplayNames: Array | null, + environmentName: string | null, + compiledWithForget: boolean, + id: number, + isInStore: boolean, +}; + +export default function OwnerView({ + displayName, + environmentName, + hocDisplayNames, + compiledWithForget, + id, + isInStore, +}: OwnerViewProps): React.Node { + const dispatch = useContext(TreeDispatcherContext); + const {highlightHostInstance, clearHighlightHostInstance} = + useHighlightHostInstance(); + + const handleClick = useCallback(() => { + logEvent({ + event_name: 'select-element', + metadata: {source: 'owner-view'}, + }); + dispatch({ + type: 'SELECT_ELEMENT_BY_ID', + payload: id, + }); + }, [dispatch, id]); + + return ( + + ); +} diff --git a/packages/react-devtools-shared/src/devtools/views/Components/OwnersStack.js b/packages/react-devtools-shared/src/devtools/views/Components/OwnersStack.js index 0fa5c0910bb6e..09bdb96af01cd 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/OwnersStack.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/OwnersStack.js @@ -220,6 +220,7 @@ function ElementsDropdown({owners, selectOwner}: ElementsDropdownProps) { @@ -268,6 +269,7 @@ function ElementView({isSelected, owner, selectOwner}: ElementViewProps) { diff --git a/packages/react-devtools-shared/src/devtools/views/Components/SelectedTreeHighlight.css b/packages/react-devtools-shared/src/devtools/views/Components/SelectedTreeHighlight.css deleted file mode 100644 index 19b64a8ef4702..0000000000000 --- a/packages/react-devtools-shared/src/devtools/views/Components/SelectedTreeHighlight.css +++ /dev/null @@ -1,16 +0,0 @@ -.Active, -.Inactive { - position: absolute; - left: 0; - width: 100%; - z-index: 0; - pointer-events: none; -} - -.Active { - background-color: var(--color-selected-tree-highlight-active); -} - -.Inactive { - background-color: var(--color-selected-tree-highlight-inactive); -} diff --git a/packages/react-devtools-shared/src/devtools/views/Components/SelectedTreeHighlight.js b/packages/react-devtools-shared/src/devtools/views/Components/SelectedTreeHighlight.js deleted file mode 100644 index 16035a13d65f9..0000000000000 --- a/packages/react-devtools-shared/src/devtools/views/Components/SelectedTreeHighlight.js +++ /dev/null @@ -1,110 +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. - * - * @flow - */ - -import type {Element} from 'react-devtools-shared/src/frontend/types'; - -import * as React from 'react'; -import {useContext, useMemo} from 'react'; -import {TreeStateContext} from './TreeContext'; -import {SettingsContext} from '../Settings/SettingsContext'; -import TreeFocusedContext from './TreeFocusedContext'; -import {StoreContext} from '../context'; -import {useSubscription} from '../hooks'; - -import styles from './SelectedTreeHighlight.css'; - -type Data = { - startIndex: number, - stopIndex: number, -}; - -export default function SelectedTreeHighlight(_: {}): React.Node { - const {lineHeight} = useContext(SettingsContext); - const store = useContext(StoreContext); - const treeFocused = useContext(TreeFocusedContext); - const {ownerID, inspectedElementID} = useContext(TreeStateContext); - - const subscription = useMemo( - () => ({ - getCurrentValue: () => { - if ( - inspectedElementID === null || - store.isInsideCollapsedSubTree(inspectedElementID) - ) { - return null; - } - - const element = store.getElementByID(inspectedElementID); - if ( - element === null || - element.isCollapsed || - element.children.length === 0 - ) { - return null; - } - - const startIndex = store.getIndexOfElementID(element.children[0]); - if (startIndex === null) { - return null; - } - - let stopIndex = null; - let current: null | Element = element; - while (current !== null) { - if (current.isCollapsed || current.children.length === 0) { - // We've found the last/deepest descendant. - stopIndex = store.getIndexOfElementID(current.id); - current = null; - } else { - const lastChildID = current.children[current.children.length - 1]; - current = store.getElementByID(lastChildID); - } - } - - if (stopIndex === null) { - return null; - } - - return { - startIndex, - stopIndex, - }; - }, - subscribe: (callback: Function) => { - store.addListener('mutated', callback); - return () => { - store.removeListener('mutated', callback); - }; - }, - }), - [inspectedElementID, store], - ); - const data = useSubscription(subscription); - - if (ownerID !== null) { - return null; - } - - if (data === null) { - return null; - } - - const {startIndex, stopIndex} = data; - - return ( -
    - ); -} diff --git a/packages/react-devtools-shared/src/devtools/views/Components/Skeleton.css b/packages/react-devtools-shared/src/devtools/views/Components/Skeleton.css index 70dcbe0c19ae3..e2bf670729551 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/Skeleton.css +++ b/packages/react-devtools-shared/src/devtools/views/Components/Skeleton.css @@ -5,9 +5,9 @@ @keyframes pulse { 0%, 100% { - background-color: var(--color-dim); + background-color: none; } 50% { - background-color: var(--color-dimmest) + background-color: var(--color-dimmest); } } diff --git a/packages/react-devtools-shared/src/devtools/views/Components/StackTraceView.css b/packages/react-devtools-shared/src/devtools/views/Components/StackTraceView.css new file mode 100644 index 0000000000000..20603ae7ccd08 --- /dev/null +++ b/packages/react-devtools-shared/src/devtools/views/Components/StackTraceView.css @@ -0,0 +1,28 @@ +.StackTraceView { + padding: 0.25rem; +} + +.CallSite, .IgnoredCallSite { + display: block; + padding-left: 1rem; +} + +.IgnoredCallSite { + opacity: 0.5; +} + +.Link { + color: var(--color-link); + white-space: pre; + overflow: hidden; + text-overflow: ellipsis; + flex: 1; + cursor: pointer; + border-radius: 0.125rem; + padding: 0px 2px; +} + +.Link:hover { + background-color: var(--color-background-hover); +} + diff --git a/packages/react-devtools-shared/src/devtools/views/Components/StackTraceView.js b/packages/react-devtools-shared/src/devtools/views/Components/StackTraceView.js new file mode 100644 index 0000000000000..57b5c4eef044a --- /dev/null +++ b/packages/react-devtools-shared/src/devtools/views/Components/StackTraceView.js @@ -0,0 +1,114 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import * as React from 'react'; +import {use, useContext} from 'react'; + +import useOpenResource from '../useOpenResource'; + +import ElementBadges from './ElementBadges'; + +import styles from './StackTraceView.css'; + +import type {ReactStackTrace, ReactCallSite} from 'shared/ReactTypes'; + +import type {SourceMappedLocation} from 'react-devtools-shared/src/symbolicateSource'; + +import FetchFileWithCachingContext from './FetchFileWithCachingContext'; + +import {symbolicateSourceWithCache} from 'react-devtools-shared/src/symbolicateSource'; + +import formatLocationForDisplay from './formatLocationForDisplay'; + +type CallSiteViewProps = { + callSite: ReactCallSite, + environmentName: null | string, +}; + +export function CallSiteView({ + callSite, + environmentName, +}: CallSiteViewProps): React.Node { + const fetchFileWithCaching = useContext(FetchFileWithCachingContext); + + const [virtualFunctionName, virtualURL, virtualLine, virtualColumn] = + callSite; + + const symbolicatedCallSite: null | SourceMappedLocation = + fetchFileWithCaching !== null + ? use( + symbolicateSourceWithCache( + fetchFileWithCaching, + virtualURL, + virtualLine, + virtualColumn, + ), + ) + : null; + + const [linkIsEnabled, viewSource] = useOpenResource( + callSite, + symbolicatedCallSite == null ? null : symbolicatedCallSite.location, + ); + const [functionName, url, line, column] = + symbolicatedCallSite !== null ? symbolicatedCallSite.location : callSite; + const ignored = + symbolicatedCallSite !== null ? symbolicatedCallSite.ignored : false; + if (ignored) { + // TODO: Make an option to be able to toggle the display of ignore listed rows. + // Ideally this UI should be higher than a single Stack Trace so that there's not + // multiple buttons in a single inspection taking up space. + return null; + } + return ( +
    + {functionName || virtualFunctionName} + {url !== '' && ( + <> + {' @ '} + + {formatLocationForDisplay(url, line, column)} + + + )} + + +
    + ); +} + +type Props = { + stack: ReactStackTrace, + environmentName: null | string, +}; + +export default function StackTraceView({ + stack, + environmentName, +}: Props): React.Node { + return ( +
    + {stack.map((callSite, index) => ( + + ))} +
    + ); +} diff --git a/packages/react-devtools-shared/src/devtools/views/Components/Tree.css b/packages/react-devtools-shared/src/devtools/views/Components/Tree.css index bf18f1d2e6019..cb2799d4a9c3f 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/Tree.css +++ b/packages/react-devtools-shared/src/devtools/views/Components/Tree.css @@ -5,17 +5,16 @@ display: flex; flex-direction: column; border-top: 1px solid var(--color-border); - - /* Default size will be adjusted by Tree after scrolling */ - --indentation-size: 12px; } -.List { - overflow-x: hidden !important; +.InnerElementType { + position: relative; } -.InnerElementType { - overflow-x: hidden; +.VerticalDelimiter { + position: absolute; + width: 0.025rem; + background: #b0b0b0; } .SearchInput { @@ -39,6 +38,7 @@ font-family: var(--font-family-monospace); font-size: var(--font-size-monospace-normal); line-height: var(--line-height-data); + user-select: none; } .VRule { @@ -97,4 +97,4 @@ .Link { color: var(--color-button-active); -} \ No newline at end of file +} diff --git a/packages/react-devtools-shared/src/devtools/views/Components/Tree.js b/packages/react-devtools-shared/src/devtools/views/Components/Tree.js index 1ba61c52dd1a4..8d763536e3020 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/Tree.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/Tree.js @@ -24,12 +24,11 @@ import {TreeDispatcherContext, TreeStateContext} from './TreeContext'; import Icon from '../Icon'; import {SettingsContext} from '../Settings/SettingsContext'; import {BridgeContext, StoreContext, OptionsContext} from '../context'; -import Element from './Element'; +import ComponentsTreeElement from './Element'; import InspectHostNodesToggle from './InspectHostNodesToggle'; import OwnersStack from './OwnersStack'; import ComponentSearchInput from './ComponentSearchInput'; import SettingsModalContextToggle from 'react-devtools-shared/src/devtools/views/Settings/SettingsModalContextToggle'; -import SelectedTreeHighlight from './SelectedTreeHighlight'; import TreeFocusedContext from './TreeFocusedContext'; import {useHighlightHostInstance, useSubscription} from '../hooks'; import {clearErrorsAndWarnings as clearErrorsAndWarningsAPI} from 'react-devtools-shared/src/backendAPI'; @@ -40,13 +39,18 @@ import {logEvent} from 'react-devtools-shared/src/Logger'; import {useExtensionComponentsPanelVisibility} from 'react-devtools-shared/src/frontend/hooks/useExtensionComponentsPanelVisibility'; import {useChangeOwnerAction} from './OwnersListContext'; -// Never indent more than this number of pixels (even if we have the room). -const DEFAULT_INDENTATION_SIZE = 12; +// Indent for each node at level N, compared to node at level N - 1. +const INDENTATION_SIZE = 10; + +function calculateElementOffset(elementDepth: number): number { + return elementDepth * INDENTATION_SIZE; +} export type ItemData = { isNavigatingWithKeyboard: boolean, onElementMouseEnter: (id: number) => void, treeFocused: boolean, + calculateElementOffset: (depth: number) => number, }; function calculateInitialScrollOffset( @@ -89,17 +93,97 @@ export default function Tree(): React.Node { const treeRef = useRef(null); const focusTargetRef = useRef(null); - const listRef = useRef(null); + const listDOMElementRef = useRef(null); + const setListDOMElementRef = useCallback((listDOMElement: Element) => { + listDOMElementRef.current = listDOMElement; + + // Controls the initial horizontal offset of the Tree if the element was pre-selected. For example, via Elements panel in browser DevTools. + // Initial vertical offset is controlled via initialScrollOffset prop of the FixedSizeList component. + if ( + !componentsPanelVisible || + inspectedElementIndex == null || + listDOMElement == null + ) { + return; + } + + const element = store.getElementAtIndex(inspectedElementIndex); + if (element == null) { + return; + } + + const viewportLeft = listDOMElement.scrollLeft; + const viewportRight = viewportLeft + listDOMElement.clientWidth; + const elementLeft = calculateElementOffset(element.depth); + // Because of virtualization, this element might not be rendered yet; we can't look up its width. + // Assuming that it may take up to the half of the viewport. + const elementRight = elementLeft + listDOMElement.clientWidth / 2; + + const isElementFullyVisible = + elementLeft >= viewportLeft && elementRight <= viewportRight; + + if (!isElementFullyVisible) { + const horizontalDelta = + Math.min(0, elementLeft - viewportLeft) + + Math.max(0, elementRight - viewportRight); + + // $FlowExpectedError[incompatible-call] Flow doesn't support instant as an option for behavior. + listDOMElement.scrollBy({ + left: horizontalDelta, + behavior: 'instant', + }); + } + }, []); useEffect(() => { - if (!componentsPanelVisible) { + if (!componentsPanelVisible || inspectedElementIndex == null) { return; } - if (listRef.current != null && inspectedElementIndex !== null) { - listRef.current.scrollToItem(inspectedElementIndex, 'smart'); + const listDOMElement = listDOMElementRef.current; + if (listDOMElement == null) { + return; } - }, [inspectedElementIndex, componentsPanelVisible]); + + const viewportHeight = listDOMElement.clientHeight; + const viewportLeft = listDOMElement.scrollLeft; + const viewportRight = viewportLeft + listDOMElement.clientWidth; + const viewportTop = listDOMElement.scrollTop; + const viewportBottom = viewportTop + viewportHeight; + + const element = store.getElementAtIndex(inspectedElementIndex); + if (element == null) { + return; + } + const elementLeft = calculateElementOffset(element.depth); + // Because of virtualization, this element might not be rendered yet; we can't look up its width. + // Assuming that it may take up to the half of the viewport. + const elementRight = elementLeft + listDOMElement.clientWidth / 2; + const elementTop = inspectedElementIndex * lineHeight; + const elementBottom = elementTop + lineHeight; + + const isElementFullyVisible = + elementTop >= viewportTop && + elementBottom <= viewportBottom && + elementLeft >= viewportLeft && + elementRight <= viewportRight; + + if (!isElementFullyVisible) { + const verticalDelta = + Math.min(0, elementTop - viewportTop) + + Math.max(0, elementBottom - viewportBottom); + const horizontalDelta = + Math.min(0, elementLeft - viewportLeft) + + Math.max(0, elementRight - viewportRight); + + // $FlowExpectedError[incompatible-call] Flow doesn't support instant as an option for behavior. + listDOMElement.scrollBy({ + top: verticalDelta, + left: horizontalDelta, + behavior: treeFocused && ownerID == null ? 'smooth' : 'instant', + }); + } + }, [inspectedElementIndex, componentsPanelVisible, lineHeight]); // Picking an element in the inspector should put focus into the tree. // If possible, navigation works right after picking a node. @@ -291,8 +375,14 @@ export default function Tree(): React.Node { isNavigatingWithKeyboard, onElementMouseEnter: handleElementMouseEnter, treeFocused, + calculateElementOffset, }), - [isNavigatingWithKeyboard, handleElementMouseEnter, treeFocused], + [ + isNavigatingWithKeyboard, + handleElementMouseEnter, + treeFocused, + calculateElementOffset, + ], ); const itemKey = useCallback( @@ -421,9 +511,10 @@ export default function Tree(): React.Node { itemData={itemData} itemKey={itemKey} itemSize={lineHeight} - ref={listRef} + outerRef={setListDOMElementRef} + overscanCount={10} width={width}> - {Element} + {ComponentsTreeElement} )} @@ -434,153 +525,57 @@ export default function Tree(): React.Node { ); } -// Indentation size can be adjusted but child width is fixed. -// We need to adjust indentations so the widest child can fit without overflowing. -// Sometimes the widest child is also the deepest in the tree: -// ┏----------------------┓ -// ┆ ┆ -// ┆ •••• ┆ -// ┆ •••••••• ┆ -// ┗----------------------┛ -// -// But this is not always the case. -// Even with the above example, a change in indentation may change the overall widest child: -// ┏----------------------┓ -// ┆ ┆ -// ┆ •• ┆ -// ┆ •••• ┆ -// ┗----------------------┛ -// -// In extreme cases this difference can be important: -// ┏----------------------┓ -// ┆ ┆ -// ┆ •• ┆ -// ┆ •••• ┆ -// ┆ •••••• ┆ -// ┆ •••••••• ┆ -// ┗----------------------┛ -// -// In the above example, the current indentation is fine, -// but if we naively assumed that the widest element is also the deepest element, -// we would end up compressing the indentation unnecessarily: -// ┏----------------------┓ -// ┆ ┆ -// ┆ • ┆ -// ┆ •• ┆ -// ┆ ••• ┆ -// ┆ •••• ┆ -// ┗----------------------┛ -// -// The way we deal with this is to compute the max indentation size that can fit each child, -// given the child's fixed width and depth within the tree. -// Then we take the smallest of these indentation sizes... -function updateIndentationSizeVar( - innerDiv: HTMLDivElement, - cachedChildWidths: WeakMap, - indentationSizeRef: {current: number}, - prevListWidthRef: {current: number}, -): void { - const list = ((innerDiv.parentElement: any): HTMLDivElement); - const listWidth = list.clientWidth; - - // Skip measurements when the Components panel is hidden. - if (listWidth === 0) { - return; - } - - // Reset the max indentation size if the width of the tree has increased. - if (listWidth > prevListWidthRef.current) { - indentationSizeRef.current = DEFAULT_INDENTATION_SIZE; - } - prevListWidthRef.current = listWidth; - - let maxIndentationSize: number = indentationSizeRef.current; - - // eslint-disable-next-line no-for-of-loops/no-for-of-loops - for (const child of innerDiv.children) { - const depth = parseInt(child.getAttribute('data-depth'), 10) || 0; - - let childWidth: number = 0; - - const cachedChildWidth = cachedChildWidths.get(child); - if (cachedChildWidth != null) { - childWidth = cachedChildWidth; - } else { - const {firstElementChild} = child; - - // Skip over e.g. the guideline element - if (firstElementChild != null) { - childWidth = firstElementChild.clientWidth; - cachedChildWidths.set(child, childWidth); - } - } - - const remainingWidth = Math.max(0, listWidth - childWidth); +// $FlowFixMe[missing-local-annot] +function InnerElementType({children, style}) { + const store = useContext(StoreContext); - maxIndentationSize = Math.min(maxIndentationSize, remainingWidth / depth); - } + const {height} = style; + const maxDepth = store.getMaximumRecordedDepth(); + // Maximum possible indentation plus some arbitrary offset for the node content. + const width = calculateElementOffset(maxDepth) + 500; - indentationSizeRef.current = maxIndentationSize; + return ( +
    + {children} - list.style.setProperty('--indentation-size', `${maxIndentationSize}px`); + +
    + ); } -// $FlowFixMe[missing-local-annot] -function InnerElementType({children, style}) { - const {ownerID} = useContext(TreeStateContext); +function VerticalDelimiter() { + const store = useContext(StoreContext); + const {ownerID, inspectedElementIndex} = useContext(TreeStateContext); + const {lineHeight} = useContext(SettingsContext); - const cachedChildWidths = useMemo>( - () => new WeakMap(), - [], - ); + if (ownerID != null || inspectedElementIndex == null) { + return null; + } - // This ref tracks the current indentation size. - // We decrease indentation to fit wider/deeper trees. - // We intentionally do not increase it again afterward, to avoid the perception of content "jumping" - // e.g. clicking to toggle/collapse a row might otherwise jump horizontally beneath your cursor, - // e.g. scrolling a wide row off screen could cause narrower rows to jump to the right some. - // - // There are two exceptions for this: - // 1. The first is when the width of the tree increases. - // The user may have resized the window specifically to make more room for DevTools. - // In either case, this should reset our max indentation size logic. - // 2. The second is when the user enters or exits an owner tree. - const indentationSizeRef = useRef(DEFAULT_INDENTATION_SIZE); - const prevListWidthRef = useRef(0); - const prevOwnerIDRef = useRef(ownerID); - const divRef = useRef(null); - - // We shouldn't retain this width across different conceptual trees though, - // so when the user opens the "owners tree" view, we should discard the previous width. - if (ownerID !== prevOwnerIDRef.current) { - prevOwnerIDRef.current = ownerID; - indentationSizeRef.current = DEFAULT_INDENTATION_SIZE; + const element = store.getElementAtIndex(inspectedElementIndex); + if (element == null) { + return null; + } + const indexOfLowestDescendant = + store.getIndexOfLowestDescendantElement(element); + if (indexOfLowestDescendant == null) { + return null; } - // When we render new content, measure to see if we need to shrink indentation to fit it. - useEffect(() => { - if (divRef.current !== null) { - updateIndentationSizeVar( - divRef.current, - cachedChildWidths, - indentationSizeRef, - prevListWidthRef, - ); - } - }); + const delimiterLeft = calculateElementOffset(element.depth) + 12; + const delimiterTop = (inspectedElementIndex + 1) * lineHeight; + const delimiterHeight = + (indexOfLowestDescendant + 1) * lineHeight - delimiterTop; - // This style override enables the background color to fill the full visible width, - // when combined with the CSS tweaks in Element. - // A lot of options were considered; this seemed the one that requires the least code. - // See https://github.com/bvaughn/react-devtools-experimental/issues/9 return (
    - - {children} -
    + className={styles.VerticalDelimiter} + style={{ + left: delimiterLeft, + top: delimiterTop, + height: delimiterHeight, + }} + /> ); } diff --git a/packages/react-devtools-shared/src/devtools/views/Components/TreeContext.js b/packages/react-devtools-shared/src/devtools/views/Components/TreeContext.js index 46c76462d09d4..72556543f4b33 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/TreeContext.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/TreeContext.js @@ -803,6 +803,45 @@ type Props = { defaultInspectedElementIndex?: ?number, }; +function getInitialState({ + defaultOwnerID, + defaultInspectedElementID, + defaultInspectedElementIndex, + store, +}: { + defaultOwnerID?: ?number, + defaultInspectedElementID?: ?number, + defaultInspectedElementIndex?: ?number, + store: Store, +}): State { + return { + // Tree + numElements: store.numElements, + ownerSubtreeLeafElementID: null, + + // Search + searchIndex: null, + searchResults: [], + searchText: '', + + // Owners + ownerID: defaultOwnerID == null ? null : defaultOwnerID, + ownerFlatTree: null, + + // Inspection element panel + inspectedElementID: + defaultInspectedElementID != null + ? defaultInspectedElementID + : store.lastSelectedHostInstanceElementId, + inspectedElementIndex: + defaultInspectedElementIndex != null + ? defaultInspectedElementIndex + : store.lastSelectedHostInstanceElementId + ? store.getIndexOfElementID(store.lastSelectedHostInstanceElementId) + : null, + }; +} + // TODO Remove TreeContextController wrapper element once global Context.write API exists. function TreeContextController({ children, @@ -866,32 +905,16 @@ function TreeContextController({ [store], ); - const [state, dispatch] = useReducer(reducer, { - // Tree - numElements: store.numElements, - ownerSubtreeLeafElementID: null, - - // Search - searchIndex: null, - searchResults: [], - searchText: '', - - // Owners - ownerID: defaultOwnerID == null ? null : defaultOwnerID, - ownerFlatTree: null, - - // Inspection element panel - inspectedElementID: - defaultInspectedElementID != null - ? defaultInspectedElementID - : store.lastSelectedHostInstanceElementId, - inspectedElementIndex: - defaultInspectedElementIndex != null - ? defaultInspectedElementIndex - : store.lastSelectedHostInstanceElementId - ? store.getIndexOfElementID(store.lastSelectedHostInstanceElementId) - : null, - }); + const [state, dispatch] = useReducer( + reducer, + { + defaultOwnerID, + defaultInspectedElementID, + defaultInspectedElementIndex, + store, + }, + getInitialState, + ); const transitionDispatch = useMemo( () => (action: Action) => startTransition(() => { @@ -931,7 +954,7 @@ function TreeContextController({ Array, Map, ]) => { - transitionDispatch({ + dispatch({ type: 'HANDLE_STORE_MUTATION', payload: [addedElementIDs, removedElementIDs], }); @@ -942,7 +965,7 @@ function TreeContextController({ // At the moment, we can treat this as a mutation. // We don't know which Elements were newly added/removed, but that should be okay in this case. // It would only impact the search state, which is unlikely to exist yet at this point. - transitionDispatch({ + dispatch({ type: 'HANDLE_STORE_MUTATION', payload: [[], new Map()], }); @@ -972,7 +995,14 @@ function recursivelySearchTree( return; } - const {children, displayName, hocDisplayNames, compiledWithForget} = element; + const { + children, + displayName, + hocDisplayNames, + compiledWithForget, + key, + nameProp, + } = element; if (displayName != null && regExp.test(displayName) === true) { searchResults.push(elementID); } else if ( @@ -983,6 +1013,10 @@ function recursivelySearchTree( searchResults.push(elementID); } else if (compiledWithForget && regExp.test('Forget')) { searchResults.push(elementID); + } else if (typeof key === 'string' && regExp.test(key)) { + searchResults.push(elementID); + } else if (typeof nameProp === 'string' && regExp.test(nameProp)) { + searchResults.push(elementID); } children.forEach(childID => diff --git a/packages/react-devtools-shared/src/devtools/views/Components/formatLocationForDisplay.js b/packages/react-devtools-shared/src/devtools/views/Components/formatLocationForDisplay.js new file mode 100644 index 0000000000000..9bf693fe33d29 --- /dev/null +++ b/packages/react-devtools-shared/src/devtools/views/Components/formatLocationForDisplay.js @@ -0,0 +1,48 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import {toNormalUrl} from 'jsc-safe-url'; + +// This function is based on describeComponentFrame() in packages/shared/ReactComponentStackFrame +export default function formatLocationForDisplay( + sourceURL: string, + line: number, + column: number, +): string { + // Metro can return JSC-safe URLs, which have `//&` as a delimiter + // https://www.npmjs.com/package/jsc-safe-url + const sanitizedSourceURL = sourceURL.includes('//&') + ? toNormalUrl(sourceURL) + : sourceURL; + + // Note: this RegExp doesn't work well with URLs from Metro, + // which provides bundle URL with query parameters prefixed with /& + const BEFORE_SLASH_RE = /^(.*)[\\\/]/; + + let nameOnly = sanitizedSourceURL.replace(BEFORE_SLASH_RE, ''); + + // In DEV, include code for a common special case: + // prefer "folder/index.js" instead of just "index.js". + if (/^index\./.test(nameOnly)) { + const match = sanitizedSourceURL.match(BEFORE_SLASH_RE); + if (match) { + const pathBeforeSlash = match[1]; + if (pathBeforeSlash) { + const folderName = pathBeforeSlash.replace(BEFORE_SLASH_RE, ''); + nameOnly = folderName + '/' + nameOnly; + } + } + } + + if (line === 0) { + return nameOnly; + } + + return `${nameOnly}:${line}`; +} diff --git a/packages/react-devtools-shared/src/devtools/views/DevTools.css b/packages/react-devtools-shared/src/devtools/views/DevTools.css index 8091c15b8484f..0230860b9454d 100644 --- a/packages/react-devtools-shared/src/devtools/views/DevTools.css +++ b/packages/react-devtools-shared/src/devtools/views/DevTools.css @@ -5,6 +5,8 @@ flex-direction: column; background-color: var(--color-background); color: var(--color-text); + container-name: devtools; + container-type: inline-size; } .TabBar { diff --git a/packages/react-devtools-shared/src/devtools/views/DevTools.js b/packages/react-devtools-shared/src/devtools/views/DevTools.js index bd14bdda0f5ba..91a17dcad2b76 100644 --- a/packages/react-devtools-shared/src/devtools/views/DevTools.js +++ b/packages/react-devtools-shared/src/devtools/views/DevTools.js @@ -23,7 +23,9 @@ import { } from './context'; import Components from './Components/Components'; import Profiler from './Profiler/Profiler'; +import SuspenseTab from './SuspenseTab/SuspenseTab'; import TabBar from './TabBar'; +import EditorPane from './Editor/EditorPane'; import {SettingsContextController} from './Settings/SettingsContext'; import {TreeContextController} from './Components/TreeContext'; import ViewElementSourceContext from './Components/ViewElementSourceContext'; @@ -31,6 +33,7 @@ import FetchFileWithCachingContext from './Components/FetchFileWithCachingContex import {InspectedElementContextController} from './Components/InspectedElementContext'; import HookNamesModuleLoaderContext from 'react-devtools-shared/src/devtools/views/Components/HookNamesModuleLoaderContext'; import {ProfilerContextController} from './Profiler/ProfilerContext'; +import {SuspenseTreeContextController} from './SuspenseTab/SuspenseTreeContext'; import {TimelineContextController} from 'react-devtools-timeline/src/TimelineContext'; import {ModalDialogContextController} from './ModalDialog'; import ReactLogo from './ReactLogo'; @@ -50,21 +53,22 @@ import type {FetchFileWithCaching} from './Components/FetchFileWithCachingContex import type {HookNamesModuleLoaderFunction} from 'react-devtools-shared/src/devtools/views/Components/HookNamesModuleLoaderContext'; import type {FrontendBridge} from 'react-devtools-shared/src/bridge'; import type {BrowserTheme} from 'react-devtools-shared/src/frontend/types'; -import type {Source} from 'react-devtools-shared/src/shared/types'; +import type {ReactFunctionLocation, ReactCallSite} from 'shared/ReactTypes'; +import type {SourceSelection} from './Editor/EditorPane'; -export type TabID = 'components' | 'profiler'; +export type TabID = 'components' | 'profiler' | 'suspense'; export type ViewElementSource = ( - source: Source, - symbolicatedSource: Source | null, + source: ReactFunctionLocation | ReactCallSite, + symbolicatedSource: ReactFunctionLocation | ReactCallSite | null, ) => void; export type ViewAttributeSource = ( id: number, path: Array, ) => void; export type CanViewElementSource = ( - source: Source, - symbolicatedSource: Source | null, + source: ReactFunctionLocation | ReactCallSite, + symbolicatedSource: ReactFunctionLocation | ReactCallSite | null, ) => boolean; export type Props = { @@ -97,6 +101,10 @@ export type Props = { // but individual tabs (e.g. Components, Profiling) can be rendered into portals within their browser panels. componentsPortalContainer?: Element, profilerPortalContainer?: Element, + suspensePortalContainer?: Element, + editorPortalContainer?: Element, + + currentSelectedSource?: null | SourceSelection, // Loads and parses source maps for function components // and extracts hook "names" based on the variables the hook return values get assigned to. @@ -118,20 +126,43 @@ const profilerTab = { label: 'Profiler', title: 'React Profiler', }; +const suspenseTab = { + id: ('suspense': TabID), + icon: 'suspense', + label: 'Suspense', + title: 'React Suspense', +}; -const tabs = [componentsTab, profilerTab]; +const defaultTabs = [componentsTab, profilerTab]; +const tabsWithSuspense = [componentsTab, profilerTab, suspenseTab]; + +function useIsSuspenseTabEnabled(store: Store): boolean { + const subscribe = useCallback( + (onStoreChange: () => void) => { + store.addListener('enableSuspenseTab', onStoreChange); + return () => { + store.removeListener('enableSuspenseTab', onStoreChange); + }; + }, + [store], + ); + return React.useSyncExternalStore(subscribe, () => store.supportsSuspenseTab); +} export default function DevTools({ bridge, browserTheme = 'light', canViewElementSourceFunction, componentsPortalContainer, + editorPortalContainer, + profilerPortalContainer, + suspensePortalContainer, + currentSelectedSource, defaultTab = 'components', enabledInspectedElementContextMenu = false, fetchFileWithCaching, hookNamesModuleLoaderFunction, overrideTab, - profilerPortalContainer, showTabBar = false, store, warnIfLegacyBackendDetected = false, @@ -149,6 +180,8 @@ export default function DevTools({ LOCAL_STORAGE_DEFAULT_TAB_KEY, defaultTab, ); + const enableSuspenseTab = useIsSuspenseTabEnabled(store); + const tabs = enableSuspenseTab ? tabsWithSuspense : defaultTabs; let tab = currentTab; @@ -165,6 +198,8 @@ export default function DevTools({ if (showTabBar === true) { if (tabId === 'components') { logEvent({event_name: 'selected-components-tab'}); + } else if (tabId === 'suspense') { + logEvent({event_name: 'selected-suspense-tab'}); } else { logEvent({event_name: 'selected-profiler-tab'}); } @@ -235,6 +270,13 @@ export default function DevTools({ event.preventDefault(); event.stopPropagation(); break; + case '3': + if (tabs.length > 2) { + selectTab(tabs[2].id); + event.preventDefault(); + event.stopPropagation(); + } + break; } } }; @@ -278,45 +320,65 @@ export default function DevTools({ - -
    - {showTabBar && ( -
    - - - {process.env.DEVTOOLS_VERSION} - -
    - + +
    + {showTabBar && ( +
    + + + {process.env.DEVTOOLS_VERSION} + +
    + +
    + )} + + + - )} - - -
    - + ) : null} + + diff --git a/packages/react-devtools-shared/src/devtools/views/Editor/EditorPane.css b/packages/react-devtools-shared/src/devtools/views/Editor/EditorPane.css new file mode 100644 index 0000000000000..a4a4adbfa9f60 --- /dev/null +++ b/packages/react-devtools-shared/src/devtools/views/Editor/EditorPane.css @@ -0,0 +1,38 @@ +.EditorPane { + position: relative; + display: block; + background-color: var(--color-background); + color: var(--color-text); + font-family: var(--font-family-sans); +} + +.EditorPane, .EditorPane * { + box-sizing: border-box; + -webkit-font-smoothing: var(--font-smoothing); +} + +.EditorToolbar { + display: flex; + flex-direction: row; + align-items: center; + padding: 0.5rem; + border-bottom: 1px solid var(--color-border); +} + +.EditorInfo { + padding: 0.5rem; + text-align: center; +} + +.VRule { + height: 20px; + width: 1px; + flex: 0 0 1px; + margin: 0 0.5rem; + background-color: var(--color-border); +} + +.WideButton { + flex: 1 0 auto; + display: flex; +} diff --git a/packages/react-devtools-shared/src/devtools/views/Editor/EditorPane.js b/packages/react-devtools-shared/src/devtools/views/Editor/EditorPane.js new file mode 100644 index 0000000000000..8237a59956d15 --- /dev/null +++ b/packages/react-devtools-shared/src/devtools/views/Editor/EditorPane.js @@ -0,0 +1,118 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import * as React from 'react'; +import {useState, startTransition} from 'react'; + +import portaledContent from '../portaledContent'; + +import styles from './EditorPane.css'; + +import Button from 'react-devtools-shared/src/devtools/views/Button'; +import ButtonIcon from 'react-devtools-shared/src/devtools/views/ButtonIcon'; + +import OpenInEditorButton from './OpenInEditorButton'; +import useEditorURL from '../useEditorURL'; + +import EditorSettings from './EditorSettings'; +import CodeEditorByDefault from '../Settings/CodeEditorByDefault'; + +export type SourceSelection = { + url: string, + // The selection is a ref so that we don't have to rerender every keystroke. + selectionRef: { + line: number, + column: number, + }, +}; + +export type Props = {selectedSource: ?SourceSelection}; + +function EditorPane({selectedSource}: Props) { + const [showSettings, setShowSettings] = useState(false); + const [showLinkInfo, setShowLinkInfo] = useState(false); + + const editorURL = useEditorURL(); + + if (showLinkInfo) { + return ( +
    +
    +
    + To enable link handling in your browser's DevTools settings, look + for the option Extension -> Link Handling. Select "React Developer + Tools". +
    +
    + +
    +
    + ); + } + + let editorToolbar; + if (showSettings) { + editorToolbar = ( +
    + +
    + +
    + ); + } else { + editorToolbar = ( +
    + +
    + +
    + ); + } + + return ( +
    + {editorToolbar} +
    + {editorURL ? ( + { + if (alwaysOpenInEditor) { + startTransition(() => setShowLinkInfo(true)); + } + }} + /> + ) : ( + 'Configure an external editor to open local files.' + )} +
    +
    + ); +} +export default (portaledContent(EditorPane): component()); diff --git a/packages/react-devtools-shared/src/devtools/views/Editor/EditorSettings.css b/packages/react-devtools-shared/src/devtools/views/Editor/EditorSettings.css new file mode 100644 index 0000000000000..f674441499be7 --- /dev/null +++ b/packages/react-devtools-shared/src/devtools/views/Editor/EditorSettings.css @@ -0,0 +1,9 @@ +.EditorSettings { + display: flex; + flex: 1 0 auto; +} + +.EditorLabel { + display: inline; + margin-right: 0.5rem; +} diff --git a/packages/react-devtools-shared/src/devtools/views/Editor/EditorSettings.js b/packages/react-devtools-shared/src/devtools/views/Editor/EditorSettings.js new file mode 100644 index 0000000000000..40466cc778c4a --- /dev/null +++ b/packages/react-devtools-shared/src/devtools/views/Editor/EditorSettings.js @@ -0,0 +1,29 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import * as React from 'react'; + +import styles from './EditorSettings.css'; + +import CodeEditorOptions from '../Settings/CodeEditorOptions'; + +type Props = {}; + +function EditorSettings(_: Props): React.Node { + return ( +
    + +
    + ); +} + +export default EditorSettings; diff --git a/packages/react-devtools-shared/src/devtools/views/Editor/OpenInEditorButton.js b/packages/react-devtools-shared/src/devtools/views/Editor/OpenInEditorButton.js new file mode 100644 index 0000000000000..68c6981655fa7 --- /dev/null +++ b/packages/react-devtools-shared/src/devtools/views/Editor/OpenInEditorButton.js @@ -0,0 +1,93 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import * as React from 'react'; + +import Button from 'react-devtools-shared/src/devtools/views/Button'; +import ButtonIcon from 'react-devtools-shared/src/devtools/views/ButtonIcon'; +import ButtonLabel from 'react-devtools-shared/src/devtools/views/ButtonLabel'; + +import type {SourceSelection} from './EditorPane'; +import type {ReactFunctionLocation} from 'shared/ReactTypes'; + +import {checkConditions} from './utils'; + +type Props = { + editorURL: string, + source: ?SourceSelection, + className?: string, +}; + +function ActualOpenInEditorButton({ + editorURL, + source, + className, +}: Props): React.Node { + let disable; + if (source == null) { + disable = true; + } else { + const staleLocation: ReactFunctionLocation = [ + '', + source.url, + // This is not live but we just use any line/column to validate whether this can be opened. + // We'll call checkConditions again when we click it to get the latest line number. + source.selectionRef.line, + source.selectionRef.column, + ]; + disable = checkConditions(editorURL, staleLocation).shouldDisableButton; + } + return ( + + ); +} + +function OpenInEditorButton({editorURL, source, className}: Props): React.Node { + return ( + + + Loading source maps... + + }> + + + ); +} + +export default OpenInEditorButton; diff --git a/packages/react-devtools-shared/src/devtools/views/Editor/utils.js b/packages/react-devtools-shared/src/devtools/views/Editor/utils.js new file mode 100644 index 0000000000000..a107c517cb8cb --- /dev/null +++ b/packages/react-devtools-shared/src/devtools/views/Editor/utils.js @@ -0,0 +1,65 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import type {ReactFunctionLocation, ReactCallSite} from 'shared/ReactTypes'; + +export function checkConditions( + editorURL: string, + source: ReactFunctionLocation | ReactCallSite, +): {url: URL | null, shouldDisableButton: boolean} { + try { + const url = new URL(editorURL); + + const [, sourceURL, line, column] = source; + let filePath; + + // Check if sourceURL is a correct URL, which has a protocol specified + if (sourceURL.startsWith('file:///')) { + filePath = new URL(sourceURL).pathname; + } else if (sourceURL.includes('://')) { + // $FlowFixMe[cannot-resolve-name] + if (!__IS_INTERNAL_VERSION__) { + // In this case, we can't really determine the path to a file, disable a button + return {url: null, shouldDisableButton: true}; + } else { + const endOfSourceMapURLPattern = '.js/'; + const endOfSourceMapURLIndex = sourceURL.lastIndexOf( + endOfSourceMapURLPattern, + ); + + if (endOfSourceMapURLIndex === -1) { + return {url: null, shouldDisableButton: true}; + } else { + filePath = sourceURL.slice( + endOfSourceMapURLIndex + endOfSourceMapURLPattern.length, + sourceURL.length, + ); + } + } + } else { + filePath = sourceURL; + } + + const lineNumberAsString = String(line); + const columnNumberAsString = String(column); + + url.href = url.href + .replace('{path}', filePath) + .replace('{line}', lineNumberAsString) + .replace('{column}', columnNumberAsString) + .replace('%7Bpath%7D', filePath) + .replace('%7Bline%7D', lineNumberAsString) + .replace('%7Bcolumn%7D', columnNumberAsString); + + return {url, shouldDisableButton: false}; + } catch (e) { + // User has provided incorrect editor url + return {url: null, shouldDisableButton: true}; + } +} diff --git a/packages/react-devtools-shared/src/devtools/views/ErrorBoundary/cache.js b/packages/react-devtools-shared/src/devtools/views/ErrorBoundary/cache.js index eca76be605bed..6da066a110338 100644 --- a/packages/react-devtools-shared/src/devtools/views/ErrorBoundary/cache.js +++ b/packages/react-devtools-shared/src/devtools/views/ErrorBoundary/cache.js @@ -7,54 +7,46 @@ * @flow */ -import type {Wakeable} from 'shared/ReactTypes'; +import type { + Thenable, + FulfilledThenable, + RejectedThenable, +} from 'shared/ReactTypes'; import type {GitHubIssue} from './githubAPI'; +import * as React from 'react'; + import {unstable_getCacheForType as getCacheForType} from 'react'; import {searchGitHubIssues} from './githubAPI'; const API_TIMEOUT = 3000; - -const Pending = 0; -const Resolved = 1; -const Rejected = 2; - -type PendingRecord = { - status: 0, - value: Wakeable, -}; - -type ResolvedRecord = { - status: 1, - value: T, -}; - -type RejectedRecord = { - status: 2, - value: null, -}; - -type Record = PendingRecord | ResolvedRecord | RejectedRecord; - -function readRecord(record: Record): ResolvedRecord | RejectedRecord { - if (record.status === Resolved) { - // This is just a type refinement. - return record; - } else if (record.status === Rejected) { - // This is just a type refinement. - return record; +function readRecord(record: Thenable): T | null { + if (typeof React.use === 'function') { + try { + return React.use(record); + } catch (x) { + if (x === null) { + return null; + } + throw x; + } + } + if (record.status === 'fulfilled') { + return record.value; + } else if (record.status === 'rejected') { + return null; } else { - throw record.value; + throw record; } } -type GitHubIssueMap = Map>; +type GitHubIssueMap = Map>; function createMap(): GitHubIssueMap { return new Map(); } -function getRecordMap(): Map> { +function getRecordMap(): Map> { return getCacheForType(createMap); } @@ -65,10 +57,15 @@ export function findGitHubIssue(errorMessage: string): GitHubIssue | null { let record = map.get(errorMessage); if (!record) { - const callbacks = new Set<() => mixed>(); - const wakeable: Wakeable = { - then(callback: () => mixed) { + const callbacks = new Set<(value: any) => mixed>(); + const rejectCallbacks = new Set<(reason: mixed) => mixed>(); + const thenable: Thenable = { + status: 'pending', + value: null, + reason: null, + then(callback: (value: any) => mixed, reject: (error: mixed) => mixed) { callbacks.add(callback); + rejectCallbacks.add(reject); }, // Optional property used by Timeline: @@ -76,13 +73,17 @@ export function findGitHubIssue(errorMessage: string): GitHubIssue | null { }; const wake = () => { // This assumes they won't throw. - callbacks.forEach(callback => callback()); + callbacks.forEach(callback => callback((thenable: any).value)); callbacks.clear(); + rejectCallbacks.clear(); }; - const newRecord: Record = (record = { - status: Pending, - value: wakeable, - }); + const wakeRejections = () => { + // This assumes they won't throw. + rejectCallbacks.forEach(callback => callback((thenable: any).reason)); + rejectCallbacks.clear(); + callbacks.clear(); + }; + record = thenable; let didTimeout = false; @@ -93,41 +94,40 @@ export function findGitHubIssue(errorMessage: string): GitHubIssue | null { } if (maybeItem) { - const resolvedRecord = - ((newRecord: any): ResolvedRecord); - resolvedRecord.status = Resolved; - resolvedRecord.value = maybeItem; + const fulfilledThenable: FulfilledThenable = + (thenable: any); + fulfilledThenable.status = 'fulfilled'; + fulfilledThenable.value = maybeItem; + wake(); } else { - const notFoundRecord = ((newRecord: any): RejectedRecord); - notFoundRecord.status = Rejected; - notFoundRecord.value = null; + const notFoundThenable: RejectedThenable = + (thenable: any); + notFoundThenable.status = 'rejected'; + notFoundThenable.reason = null; + wakeRejections(); } - - wake(); }) .catch(error => { - const thrownRecord = ((newRecord: any): RejectedRecord); - thrownRecord.status = Rejected; - thrownRecord.value = null; - - wake(); + const rejectedThenable: RejectedThenable = (thenable: any); + rejectedThenable.status = 'rejected'; + rejectedThenable.reason = null; + wakeRejections(); }); // Only wait a little while for GitHub results before showing a fallback. setTimeout(() => { didTimeout = true; - const timedoutRecord = ((newRecord: any): RejectedRecord); - timedoutRecord.status = Rejected; - timedoutRecord.value = null; - - wake(); + const timedoutThenable: RejectedThenable = (thenable: any); + timedoutThenable.status = 'rejected'; + timedoutThenable.reason = null; + wakeRejections(); }, API_TIMEOUT); map.set(errorMessage, record); } - const response = readRecord(record).value; + const response = readRecord(record); return response; } diff --git a/packages/react-devtools-shared/src/devtools/views/Icon.js b/packages/react-devtools-shared/src/devtools/views/Icon.js index cd47eb257fffb..f65f331a12d44 100644 --- a/packages/react-devtools-shared/src/devtools/views/Icon.js +++ b/packages/react-devtools-shared/src/devtools/views/Icon.js @@ -26,6 +26,7 @@ export type IconType = | 'settings' | 'store-as-global-variable' | 'strict-mode-non-compliant' + | 'suspense' | 'warning'; type Props = { @@ -40,6 +41,7 @@ export default function Icon({ type, }: Props): React.Node { let pathData = null; + let viewBox = '0 0 24 24'; switch (type) { case 'arrow': pathData = PATH_ARROW; @@ -86,6 +88,10 @@ export default function Icon({ case 'strict-mode-non-compliant': pathData = PATH_STRICT_MODE_NON_COMPLIANT; break; + case 'suspense': + pathData = PATH_SUSPEND; + viewBox = '-2 -2 28 28'; + break; case 'warning': pathData = PATH_WARNING; break; @@ -100,7 +106,7 @@ export default function Icon({ className={`${styles.Icon} ${className}`} width="24" height="24" - viewBox="0 0 24 24"> + viewBox={viewBox}> {title && {title}} @@ -185,4 +191,9 @@ const PATH_STRICT_MODE_NON_COMPLIANT = ` 14c-.55 0-1-.45-1-1v-2c0-.55.45-1 1-1s1 .45 1 1v2c0 .55-.45 1-1 1zm1 4h-2v-2h2v2z `; +const PATH_SUSPEND = ` + M15 1H9v2h6V1zm-4 13h2V8h-2v6zm8.03-6.61l1.42-1.42c-.43-.51-.9-.99-1.41-1.41l-1.42 1.42C16.07 4.74 14.12 4 12 4c-4.97 + 0-9 4.03-9 9s4.02 9 9 9 9-4.03 9-9c0-2.12-.74-4.07-1.97-5.61zM12 20c-3.87 0-7-3.13-7-7s3.13-7 7-7 7 3.13 7 7-3.13 7-7 7z +`; + const PATH_WARNING = `M12 1l-12 22h24l-12-22zm-1 8h2v7h-2v-7zm1 11.25c-.69 0-1.25-.56-1.25-1.25s.56-1.25 1.25-1.25 1.25.56 1.25 1.25-.56 1.25-1.25 1.25z`; diff --git a/packages/react-devtools-shared/src/devtools/views/ModalDialog.js b/packages/react-devtools-shared/src/devtools/views/ModalDialog.js index 542961b4c932b..a584d9a9e3d14 100644 --- a/packages/react-devtools-shared/src/devtools/views/ModalDialog.js +++ b/packages/react-devtools-shared/src/devtools/views/ModalDialog.js @@ -75,7 +75,7 @@ function dialogReducer(state: State, action: Action) { content: action.content, id: action.id, title: action.title || null, - }, + } as Dialog, ], }; default: diff --git a/packages/react-devtools-shared/src/devtools/views/Profiler/ChartNode.js b/packages/react-devtools-shared/src/devtools/views/Profiler/ChartNode.js index ef3d47f4011ca..a6acf53fcd0de 100644 --- a/packages/react-devtools-shared/src/devtools/views/Profiler/ChartNode.js +++ b/packages/react-devtools-shared/src/devtools/views/Profiler/ChartNode.js @@ -10,16 +10,17 @@ import * as React from 'react'; import styles from './ChartNode.css'; +import typeof {SyntheticMouseEvent} from 'react-dom-bindings/src/events/SyntheticEvent'; type Props = { color: string, height: number, isDimmed?: boolean, label: string, - onClick: (event: SyntheticMouseEvent) => mixed, - onDoubleClick?: (event: SyntheticMouseEvent) => mixed, - onMouseEnter: (event: SyntheticMouseEvent) => mixed, - onMouseLeave: (event: SyntheticMouseEvent) => mixed, + onClick: (event: SyntheticMouseEvent) => mixed, + onDoubleClick?: (event: SyntheticMouseEvent) => mixed, + onMouseEnter: (event: SyntheticMouseEvent) => mixed, + onMouseLeave: (event: SyntheticMouseEvent) => mixed, placeLabelAboveNode?: boolean, textStyle?: Object, width: number, diff --git a/packages/react-devtools-shared/src/devtools/views/Profiler/CommitFlamegraphListItem.js b/packages/react-devtools-shared/src/devtools/views/Profiler/CommitFlamegraphListItem.js index 1fc586ed42b6a..5bfe0a299f68a 100644 --- a/packages/react-devtools-shared/src/devtools/views/Profiler/CommitFlamegraphListItem.js +++ b/packages/react-devtools-shared/src/devtools/views/Profiler/CommitFlamegraphListItem.js @@ -17,6 +17,7 @@ import {SettingsContext} from '../Settings/SettingsContext'; import type {ChartNode as ChartNodeType} from './FlamegraphChartBuilder'; import type {ItemData} from './CommitFlamegraph'; +import typeof {SyntheticMouseEvent} from 'react-dom-bindings/src/events/SyntheticEvent'; type Props = { data: ItemData, @@ -41,7 +42,7 @@ function CommitFlamegraphListItem({data, index, style}: Props): React.Node { const {lineHeight} = useContext(SettingsContext); const handleClick = useCallback( - (event: SyntheticMouseEvent, id: number, name: string) => { + (event: SyntheticMouseEvent, id: number, name: string) => { event.stopPropagation(); selectFiber(id, name); }, @@ -131,7 +132,6 @@ function CommitFlamegraphListItem({data, index, style}: Props): React.Node { ); } -export default (memo( - CommitFlamegraphListItem, - areEqual, -): React.ComponentType); +export default (memo(CommitFlamegraphListItem, areEqual): component( + ...props: Props +)); diff --git a/packages/react-devtools-shared/src/devtools/views/Profiler/CommitRankedListItem.js b/packages/react-devtools-shared/src/devtools/views/Profiler/CommitRankedListItem.js index 4a8ab9b7e6be2..707fddf6268cb 100644 --- a/packages/react-devtools-shared/src/devtools/views/Profiler/CommitRankedListItem.js +++ b/packages/react-devtools-shared/src/devtools/views/Profiler/CommitRankedListItem.js @@ -79,7 +79,6 @@ function CommitRankedListItem({data, index, style}: Props) { ); } -export default (memo( - CommitRankedListItem, - areEqual, -): React.ComponentType); +export default (memo(CommitRankedListItem, areEqual): component( + ...props: Props +)); diff --git a/packages/react-devtools-shared/src/devtools/views/Profiler/CommitTreeBuilder.js b/packages/react-devtools-shared/src/devtools/views/Profiler/CommitTreeBuilder.js index 75c9b8a6d9cc6..4b4e721ced61c 100644 --- a/packages/react-devtools-shared/src/devtools/views/Profiler/CommitTreeBuilder.js +++ b/packages/react-devtools-shared/src/devtools/views/Profiler/CommitTreeBuilder.js @@ -16,6 +16,10 @@ import { TREE_OPERATION_SET_SUBTREE_MODE, TREE_OPERATION_UPDATE_TREE_BASE_DURATION, TREE_OPERATION_UPDATE_ERRORS_OR_WARNINGS, + SUSPENSE_TREE_OPERATION_ADD, + SUSPENSE_TREE_OPERATION_REMOVE, + SUSPENSE_TREE_OPERATION_REORDER_CHILDREN, + SUSPENSE_TREE_OPERATION_RESIZE, } from 'react-devtools-shared/src/constants'; import { parseElementDisplayNameFromBackend, @@ -204,6 +208,7 @@ function updateTree( i++; // Profiling flag i++; // supportsStrictMode flag i++; // hasOwnerMetadata flag + i++; // supportsTogglingSuspense flag if (__DEBUG__) { debug('Add', `new root fiber ${id}`); @@ -236,6 +241,9 @@ function updateTree( const key = stringTable[keyStringID]; i++; + // skip name prop + i++; + if (__DEBUG__) { debug( 'Add', @@ -366,6 +374,84 @@ function updateTree( break; } + case SUSPENSE_TREE_OPERATION_ADD: { + const fiberID = operations[i + 1]; + const parentID = operations[i + 2]; + const nameStringID = operations[i + 3]; + const numRects = operations[i + 4]; + const name = stringTable[nameStringID]; + + if (__DEBUG__) { + let rects: string; + if (numRects === -1) { + rects = 'null'; + } else { + rects = + '[' + + operations.slice(i + 5, i + 5 + numRects * 4).join(',') + + ']'; + } + debug( + 'Add suspense', + `node ${fiberID} (name=${JSON.stringify(name)}, rects={${rects}}) under ${parentID}`, + ); + } + + i += 5 + (numRects === -1 ? 0 : numRects * 4); + break; + } + + case SUSPENSE_TREE_OPERATION_REMOVE: { + const removeLength = ((operations[i + 1]: any): number); + i += 2 + removeLength; + + break; + } + + case SUSPENSE_TREE_OPERATION_REORDER_CHILDREN: { + const suspenseID = ((operations[i + 1]: any): number); + const numChildren = ((operations[i + 2]: any): number); + const children = ((operations.slice( + i + 3, + i + 3 + numChildren, + ): any): Array); + + i = i + 3 + numChildren; + + if (__DEBUG__) { + debug( + 'Suspense re-order', + `suspense ${suspenseID} children ${children.join(',')}`, + ); + } + + break; + } + + case SUSPENSE_TREE_OPERATION_RESIZE: { + const suspenseID = ((operations[i + 1]: any): number); + const numRects = ((operations[i + 2]: any): number); + + if (__DEBUG__) { + if (numRects === -1) { + debug('Suspense resize', `suspense ${suspenseID} rects null`); + } else { + const rects = ((operations.slice( + i + 3, + i + 3 + numRects * 4, + ): any): Array); + debug( + 'Suspense resize', + `suspense ${suspenseID} rects [${rects.join(',')}]`, + ); + } + } + + i += 3 + (numRects === -1 ? 0 : numRects * 4); + + break; + } + default: throw Error(`Unsupported Bridge operation "${operation}"`); } diff --git a/packages/react-devtools-shared/src/devtools/views/Profiler/HookChangeSummary.js b/packages/react-devtools-shared/src/devtools/views/Profiler/HookChangeSummary.js index ed852c85c778a..5ce3eec42bc00 100644 --- a/packages/react-devtools-shared/src/devtools/views/Profiler/HookChangeSummary.js +++ b/packages/react-devtools-shared/src/devtools/views/Profiler/HookChangeSummary.js @@ -21,10 +21,8 @@ import ButtonIcon from '../ButtonIcon'; import {InspectedElementContext} from '../Components/InspectedElementContext'; import {StoreContext} from '../context'; -import { - getAlreadyLoadedHookNames, - getHookSourceLocationKey, -} from 'react-devtools-shared/src/hookNamesCache'; +import {getAlreadyLoadedHookNames} from 'react-devtools-shared/src/hookNamesCache'; +import {getHookSourceLocationKey} from 'react-devtools-shared/src/hookSourceLocation'; import Toggle from '../Toggle'; import type {HooksNode} from 'react-debug-tools/src/ReactDebugHooks'; import type {ChangeDescription} from './types'; @@ -40,34 +38,38 @@ type HookProps = { hookNames: Map | null, }; -const Hook: React.AbstractComponent = memo(({hook, hookNames}) => { - const hookSource = hook.hookSource; - const hookName = useMemo(() => { - if (!hookSource || !hookNames) return null; - const key = getHookSourceLocationKey(hookSource); - return hookNames.get(key) || null; - }, [hookSource, hookNames]); - - return ( -
      -
    • - {hook.id !== null && ( - - {String(hook.id + 1)} +const Hook: component(...props: HookProps) = memo( + ({hook, hookNames}: HookProps) => { + const hookSource = hook.hookSource; + const hookName = useMemo(() => { + if (!hookSource || !hookNames) return null; + const key = getHookSourceLocationKey(hookSource); + return hookNames.get(key) || null; + }, [hookSource, hookNames]); + + return ( +
        +
      • + {hook.id !== null && ( + + {String(hook.id + 1)} + + )} + + {hook.name} + {hookName && ({hookName})} - )} - - {hook.name} - {hookName && ({hookName})} - - {hook.subHooks?.map((subHook, index) => ( - - ))} -
      • -
      - ); -}); + {hook.subHooks?.map((subHook, index) => ( + + ))} +
    • +
    + ); + }, +); const shouldKeepHook = ( hook: HooksNode, @@ -107,12 +109,12 @@ const filterHooks = ( type Props = {| fiberID: number, - hooks: $PropertyType, - state: $PropertyType, + hooks: ChangeDescription['hooks'], + state: ChangeDescription['state'], displayMode?: 'detailed' | 'compact', |}; -const HookChangeSummary: React.AbstractComponent = memo( +const HookChangeSummary: component(...props: Props) = memo( ({hooks, fiberID, state, displayMode = 'detailed'}: Props) => { const {parseHookNames, toggleParseHookNames, inspectedElement} = useContext( InspectedElementContext, diff --git a/packages/react-devtools-shared/src/devtools/views/Profiler/Profiler.js b/packages/react-devtools-shared/src/devtools/views/Profiler/Profiler.js index cb06c98f933a2..4ac55b46f0e8c 100644 --- a/packages/react-devtools-shared/src/devtools/views/Profiler/Profiler.js +++ b/packages/react-devtools-shared/src/devtools/views/Profiler/Profiler.js @@ -190,4 +190,4 @@ const tabsWithTimeline = [ }, ]; -export default (portaledContent(Profiler): React.ComponentType<{}>); +export default (portaledContent(Profiler): component()); diff --git a/packages/react-devtools-shared/src/devtools/views/Profiler/SidebarEventInfo.js b/packages/react-devtools-shared/src/devtools/views/Profiler/SidebarEventInfo.js index 97977380efdb3..77f0d13feb72f 100644 --- a/packages/react-devtools-shared/src/devtools/views/Profiler/SidebarEventInfo.js +++ b/packages/react-devtools-shared/src/devtools/views/Profiler/SidebarEventInfo.js @@ -12,16 +12,16 @@ import type {SchedulingEvent} from 'react-devtools-timeline/src/types'; import * as React from 'react'; import Button from '../Button'; import ButtonIcon from '../ButtonIcon'; -import ViewElementSourceContext from '../Components/ViewElementSourceContext'; import {useContext} from 'react'; import {TimelineContext} from 'react-devtools-timeline/src/TimelineContext'; import { formatTimestamp, getSchedulingEventLabel, } from 'react-devtools-timeline/src/utils/formatting'; -import {stackToComponentSources} from 'react-devtools-shared/src/devtools/utils'; +import {stackToComponentLocations} from 'react-devtools-shared/src/devtools/utils'; import {copy} from 'clipboard-js'; import {withPermissionsCheck} from 'react-devtools-shared/src/frontend/utils/withPermissionsCheck'; +import useOpenResource from '../useOpenResource'; import styles from './SidebarEventInfo.css'; @@ -32,9 +32,6 @@ type SchedulingEventProps = { }; function SchedulingEventInfo({eventInfo}: SchedulingEventProps) { - const {canViewElementSourceFunction, viewElementSourceFunction} = useContext( - ViewElementSourceContext, - ); const {componentName, timestamp} = eventInfo; const componentStack = eventInfo.componentStack || null; @@ -63,9 +60,9 @@ function SchedulingEventInfo({eventInfo}: SchedulingEventProps) {