Compare commits

..

1 Commits

Author SHA1 Message Date
Lauren Tan
1d9c3927ea [compiler] new tests for props derived
Adds some new test cases for ValidateNoDerivedComputationsInEffects.
2025-09-09 14:09:57 -07:00
1277 changed files with 18358 additions and 44612 deletions

View File

@@ -331,7 +331,6 @@ module.exports = {
'packages/react-server-dom-turbopack/**/*.js',
'packages/react-server-dom-parcel/**/*.js',
'packages/react-server-dom-fb/**/*.js',
'packages/react-server-dom-unbundled/**/*.js',
'packages/react-test-renderer/**/*.js',
'packages/react-debug-tools/**/*.js',
'packages/react-devtools-extensions/**/*.js',
@@ -518,14 +517,6 @@ module.exports = {
__IS_INTERNAL_VERSION__: 'readonly',
},
},
{
files: ['packages/react-devtools-*/**/*.js'],
excludedFiles: '**/__tests__/**/*.js',
plugins: ['eslint-plugin-react-hooks-published'],
rules: {
'react-hooks-published/rules-of-hooks': ERROR,
},
},
{
files: ['packages/eslint-plugin-react-hooks/src/**/*'],
extends: ['plugin:@typescript-eslint/recommended'],
@@ -635,7 +626,6 @@ module.exports = {
FocusOptions: 'readonly',
OptionalEffectTiming: 'readonly',
__REACT_ROOT_PATH_TEST__: 'readonly',
spyOnDev: 'readonly',
spyOnDevAndProd: 'readonly',
spyOnProd: 'readonly',

View File

@@ -11,7 +11,7 @@ body:
options:
- label: React Compiler core (the JS output is incorrect, or your app works incorrectly after optimization)
- label: babel-plugin-react-compiler (build issue installing or using the Babel plugin)
- label: eslint-plugin-react-hooks (build issue installing or using the eslint plugin)
- label: eslint-plugin-react-compiler (build issue installing or using the eslint plugin)
- label: react-compiler-healthcheck (build issue installing or using the healthcheck script)
- type: input
attributes:

View File

@@ -19,9 +19,6 @@ on:
tag_version:
required: false
type: string
dry_run:
required: false
type: boolean
secrets:
NPM_TOKEN:
required: true
@@ -58,13 +55,7 @@ jobs:
key: compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('compiler/yarn.lock') }}
- run: yarn install --frozen-lockfile
if: steps.node_modules.outputs.cache-hit != 'true'
- if: inputs.dry_run == true
name: Publish packages to npm (dry run)
run: |
cp ./scripts/release/ci-npmrc ~/.npmrc
scripts/release/publish.js --frfr --debug --ci --versionName=${{ inputs.version_name }} --tag=${{ inputs.dist_tag }} ${{ inputs.tag_version && format('--tagVersion={0}', inputs.tag_version) || '' }}
- if: inputs.dry_run != true
name: Publish packages to npm
- name: Publish packages to npm
run: |
cp ./scripts/release/ci-npmrc ~/.npmrc
scripts/release/publish.js --frfr --ci --versionName=${{ inputs.version_name }} --tag=${{ inputs.dist_tag }} ${{ inputs.tag_version && format('--tagVersion={0}', inputs.tag_version) || '' }}

View File

@@ -17,9 +17,6 @@ on:
tag_version:
required: false
type: string
dry_run:
required: false
type: boolean
permissions: {}
@@ -36,6 +33,5 @@ jobs:
dist_tag: ${{ inputs.dist_tag }}
version_name: ${{ inputs.version_name }}
tag_version: ${{ inputs.tag_version }}
dry_run: ${{ inputs.dry_run }}
secrets:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}

View File

@@ -19,6 +19,5 @@ jobs:
release_channel: experimental
dist_tag: experimental
version_name: '0.0.0'
dry_run: false
secrets:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}

View File

@@ -1,49 +0,0 @@
name: (DevTools) Discord Notify
on:
pull_request_target:
types: [opened, ready_for_review]
paths:
- packages/react-devtools**
- .github/workflows/devtools_**.yml
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' }}
run: echo "is_member_or_collaborator=true" >> "$GITHUB_OUTPUT"
check_maintainer:
if: ${{ needs.check_access.outputs.is_member_or_collaborator == 'true' || needs.check_access.outputs.is_member_or_collaborator == true }}
needs: [check_access]
uses: facebook/react/.github/workflows/shared_check_maintainer.yml@main
permissions:
# Used by check_maintainer
contents: read
with:
actor: ${{ github.event.pull_request.user.login }}
notify:
if: ${{ needs.check_maintainer.outputs.is_core_team == 'true' }}
needs: check_maintainer
runs-on: ubuntu-latest
steps:
- name: Discord Webhook Action
uses: tsickert/discord-webhook@86dc739f3f165f16dadc5666051c367efa1692f4
with:
webhook-url: ${{ secrets.DEVTOOLS_DISCORD_WEBHOOK_URL }}
embed-author-name: ${{ github.event.pull_request.user.login }}
embed-author-url: ${{ github.event.pull_request.user.html_url }}
embed-author-icon-url: ${{ github.event.pull_request.user.avatar_url }}
embed-title: '#${{ github.event.number }} (+${{github.event.pull_request.additions}} -${{github.event.pull_request.deletions}}): ${{ github.event.pull_request.title }}'
embed-description: ${{ github.event.pull_request.body }}
embed-url: ${{ github.event.pull_request.html_url }}

View File

@@ -92,7 +92,7 @@ jobs:
uses: actions/upload-artifact@v4
with:
name: react-devtools
path: build/devtools
path: build/devtools.tgz
if-no-files-found: error
# Simplifies getting the extension for local testing
- name: Archive chrome extension
@@ -201,5 +201,5 @@ jobs:
- uses: actions/upload-artifact@v4
with:
name: screenshots
path: ./tmp/playwright-artifacts
path: ./tmp/screenshots
if-no-files-found: warn

View File

@@ -3,10 +3,6 @@ name: (Runtime) Build and Test
on:
push:
branches: [main]
tags:
# To get CI for backport releases.
# This will duplicate CI for releases from main which is acceptable
- "v*"
pull_request:
paths-ignore:
- compiler/**
@@ -45,7 +41,7 @@ jobs:
with:
path: |
**/node_modules
key: runtime-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
key: runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
lookup-only: true
- uses: actions/setup-node@v4
if: steps.node_modules.outputs.cache-hit != 'true'
@@ -59,8 +55,10 @@ jobs:
with:
path: |
**/node_modules
key: runtime-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
# Don't use restore-keys here. Otherwise the cache grows indefinitely.
key: runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
restore-keys: |
runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-
runtime-node_modules-v6-
- run: yarn install --frozen-lockfile
if: steps.node_modules.outputs.cache-hit != 'true'
- name: Save cache
@@ -69,7 +67,7 @@ jobs:
with:
path: |
**/node_modules
key: runtime-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
key: runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
runtime_compiler_node_modules_cache:
name: Cache Runtime, Compiler node_modules
@@ -84,7 +82,7 @@ jobs:
with:
path: |
**/node_modules
key: runtime-and-compiler-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }}
key: runtime-and-compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }}
lookup-only: true
- uses: actions/setup-node@v4
if: steps.node_modules.outputs.cache-hit != 'true'
@@ -100,8 +98,10 @@ jobs:
with:
path: |
**/node_modules
key: runtime-and-compiler-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }}
# Don't use restore-keys here. Otherwise the cache grows indefinitely.
key: runtime-and-compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }}
restore-keys: |
runtime-and-compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-
runtime-and-compiler-node_modules-v6-
- run: yarn install --frozen-lockfile
if: steps.node_modules.outputs.cache-hit != 'true'
- run: yarn --cwd compiler install --frozen-lockfile
@@ -112,7 +112,7 @@ jobs:
with:
path: |
**/node_modules
key: runtime-and-compiler-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }}
key: runtime-and-compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }}
# ----- FLOW -----
discover_flow_inline_configs:
@@ -154,8 +154,10 @@ jobs:
with:
path: |
**/node_modules
key: runtime-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
# Don't use restore-keys here. Otherwise the cache grows indefinitely.
key: runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
restore-keys: |
runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-
runtime-node_modules-v6-
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
@@ -182,15 +184,17 @@ jobs:
with:
path: |
**/node_modules
key: runtime-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
# Don't use restore-keys here. Otherwise the cache grows indefinitely.
key: runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
restore-keys: |
runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-
runtime-node_modules-v6-
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
if: steps.node_modules.outputs.cache-hit != 'true'
- run: |
yarn generate-inline-fizz-runtime
git diff --exit-code || (echo "There was a change to the Fizz runtime. Run \`yarn generate-inline-fizz-runtime\` and check in the result." && false)
git diff --quiet || (echo "There was a change to the Fizz runtime. Run `yarn generate-inline-fizz-runtime` and check in the result." && false)
# ----- FEATURE FLAGS -----
flags:
@@ -212,7 +216,7 @@ jobs:
with:
path: |
**/node_modules
key: runtime-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
key: runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
@@ -270,15 +274,16 @@ jobs:
with:
path: |
**/node_modules
key: runtime-and-compiler-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }}
# Don't use restore-keys here. Otherwise the cache grows indefinitely.
key: runtime-and-compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }}
restore-keys: |
runtime-and-compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-
runtime-and-compiler-node_modules-v6-
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
if: steps.node_modules.outputs.cache-hit != 'true'
- run: yarn --cwd compiler install --frozen-lockfile
if: steps.node_modules.outputs.cache-hit != 'true'
- run: node --version
- run: yarn test ${{ matrix.params }} --ci --shard=${{ matrix.shard }}
# Hardcoded to improve parallelism
@@ -301,7 +306,7 @@ jobs:
with:
path: |
**/node_modules
key: runtime-and-compiler-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }}
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'
@@ -344,8 +349,10 @@ jobs:
with:
path: |
**/node_modules
key: runtime-and-compiler-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }}
# Don't use restore-keys here. Otherwise the cache grows indefinitely.
key: runtime-and-compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }}
restore-keys: |
runtime-and-compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-
runtime-and-compiler-node_modules-v6-
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
@@ -383,6 +390,9 @@ jobs:
-r=experimental --env=development,
-r=experimental --env=production,
# Dev Tools
--project=devtools -r=experimental,
# TODO: Update test config to support www build tests
# - "-r=www-classic --env=development --variant=false"
# - "-r=www-classic --env=production --variant=false"
@@ -430,8 +440,10 @@ jobs:
with:
path: |
**/node_modules
key: runtime-and-compiler-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }}
# Don't use restore-keys here. Otherwise the cache grows indefinitely.
key: runtime-and-compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }}
restore-keys: |
runtime-and-compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-
runtime-and-compiler-node_modules-v6-
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
@@ -446,54 +458,8 @@ jobs:
merge-multiple: true
- name: Display structure of build
run: ls -R build
- run: node --version
- run: yarn test --build ${{ matrix.test_params }} --shard=${{ matrix.shard }} --ci
test_build_devtools:
name: yarn test-build (devtools)
needs: [build_and_lint, runtime_node_modules_cache]
strategy:
fail-fast: false
matrix:
shard:
- 1/5
- 2/5
- 3/5
- 4/5
- 5/5
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
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'
cache: yarn
cache-dependency-path: yarn.lock
- name: Restore cached node_modules
uses: actions/cache/restore@v4
id: node_modules
with:
path: |
**/node_modules
key: runtime-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
# Don't use restore-keys here. Otherwise the cache grows indefinitely.
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
if: steps.node_modules.outputs.cache-hit != 'true'
- name: Restore archived build
uses: actions/download-artifact@v4
with:
pattern: _build_*
path: build
merge-multiple: true
- name: Display structure of build
run: ls -R build
- run: node --version
- run: yarn test --build --project=devtools -r=experimental --shard=${{ matrix.shard }} --ci
process_artifacts_combined:
name: Process artifacts combined
needs: [build_and_lint, runtime_node_modules_cache]
@@ -517,8 +483,10 @@ jobs:
with:
path: |
**/node_modules
key: runtime-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
# Don't use restore-keys here. Otherwise the cache grows indefinitely.
key: runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
restore-keys: |
runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-
runtime-node_modules-v6-
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
@@ -580,8 +548,10 @@ jobs:
with:
path: |
**/node_modules
key: runtime-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
# Don't use restore-keys here. Otherwise the cache grows indefinitely.
key: runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
restore-keys: |
runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-
runtime-node_modules-v6-
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
@@ -597,7 +567,7 @@ jobs:
- name: Search build artifacts for unminified errors
run: |
yarn extract-errors
git diff --exit-code || (echo "Found unminified errors. Either update the error codes map or disable error minification for the affected build, if appropriate." && false)
git diff --quiet || (echo "Found unminified errors. Either update the error codes map or disable error minification for the affected build, if appropriate." && false)
check_release_dependencies:
name: Check release dependencies
@@ -618,8 +588,10 @@ jobs:
with:
path: |
**/node_modules
key: runtime-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
# Don't use restore-keys here. Otherwise the cache grows indefinitely.
key: runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
restore-keys: |
runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-
runtime-node_modules-v6-
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
@@ -768,8 +740,10 @@ jobs:
with:
path: |
**/node_modules
key: runtime-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
# Don't use restore-keys here. Otherwise the cache grows indefinitely.
key: runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
restore-keys: |
runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-
runtime-node_modules-v6-
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
@@ -792,11 +766,6 @@ jobs:
name: react-devtools-${{ matrix.browser }}-extension
path: build/devtools/${{ matrix.browser }}-extension.zip
if-no-files-found: error
- name: Archive ${{ matrix.browser }} metadata
uses: actions/upload-artifact@v4
with:
name: react-devtools-${{ matrix.browser }}-metadata
path: build/devtools/webpack-stats.*.json
merge_devtools_artifacts:
name: Merge DevTools artifacts
@@ -807,7 +776,7 @@ jobs:
uses: actions/upload-artifact/merge@v4
with:
name: react-devtools
pattern: react-devtools-*
pattern: react-devtools-*-extension
run_devtools_e2e_tests:
name: Run DevTools e2e tests
@@ -828,8 +797,10 @@ jobs:
with:
path: |
**/node_modules
key: runtime-node_modules-v7-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
# Don't use restore-keys here. Otherwise the cache grows indefinitely.
key: runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
restore-keys: |
runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-
runtime-node_modules-v6-
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
@@ -855,12 +826,6 @@ jobs:
- run: ./scripts/ci/run_devtools_e2e_tests.js
env:
RELEASE_CHANNEL: experimental
- name: Archive Playwright report
uses: actions/upload-artifact@v4
with:
name: devtools-playwright-artifacts
path: tmp/playwright-artifacts
if-no-files-found: warn
# ----- SIZEBOT -----
sizebot:

View File

@@ -162,13 +162,10 @@ jobs:
mv build/facebook-react-native/react-is/cjs/ $BASE_FOLDER/RKJSModules/vendor/react/react-is/
mv build/facebook-react-native/react-test-renderer/cjs/ $BASE_FOLDER/RKJSModules/vendor/react/react-test-renderer/
# Delete the OSS renderers, these are sync'd to RN separately.
# Delete OSS renderer. OSS renderer is synced through internal script.
RENDERER_FOLDER=$BASE_FOLDER/react-native-github/Libraries/Renderer/implementations/
rm $RENDERER_FOLDER/ReactFabric-{dev,prod,profiling}.js
# Delete the legacy renderer shim, this is not sync'd and will get deleted in the future.
SHIM_FOLDER=$BASE_FOLDER/react-native-github/Libraries/Renderer/shims/
rm $SHIM_FOLDER/ReactNative.js
rm $RENDERER_FOLDER/ReactNativeRenderer-{dev,prod,profiling}.js
# Copy eslint-plugin-react-hooks
# NOTE: This is different from www, here we include the full package

View File

@@ -4,10 +4,8 @@ on:
pull_request_target:
types: [opened, ready_for_review]
paths-ignore:
- packages/react-devtools**
- compiler/**
- .github/workflows/compiler_**.yml
- .github/workflows/devtools**.yml
permissions: {}

View File

@@ -82,6 +82,7 @@ jobs:
run: |
scripts/release/publish.js \
--ci \
--skipTests \
--tags=${{ inputs.dist_tag }} \
--onlyPackages=${{ inputs.only_packages }} ${{ (inputs.dry && '') || '\'}}
${{ inputs.dry && '--dry' || '' }}
@@ -90,10 +91,11 @@ jobs:
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 }}'
- if: '${{ !(inputs.skip_packages && inputs.only_packages) }}'
name: 'Publish all packages'
run: |
scripts/release/publish.js \

1
.gitignore vendored
View File

@@ -23,7 +23,6 @@ chrome-user-data
.vscode
*.swp
*.swo
/tmp
packages/react-devtools-core/dist
packages/react-devtools-extensions/chrome/build

View File

@@ -1,88 +1,3 @@
## 19.2.1 (Dec 3, 2025)
### React Server Components
- Bring React Server Component fixes to Server Actions (@sebmarkbage [#35277](https://github.com/facebook/react/pull/35277))
## 19.2.0 (October 1st, 2025)
Below is a list of all new features, APIs, and bug fixes.
Read the [React 19.2 release post](https://react.dev/blog/2025/10/01/react-19-2) for more information.
### New React Features
- [`<Activity>`](https://react.dev/reference/react/Activity): A new API to hide and restore the UI and internal state of its children.
- [`useEffectEvent`](https://react.dev/reference/react/useEffectEvent) is a React Hook that lets you extract non-reactive logic into an [Effect Event](https://react.dev/learn/separating-events-from-effects#declaring-an-effect-event).
- [`cacheSignal`](https://react.dev/reference/react/cacheSignal) (for RSCs) lets your know when the `cache()` lifetime is over.
- [React Performance tracks](https://react.dev/reference/dev-tools/react-performance-tracks) appear on the Performance panels timeline in your browser developer tools
### New React DOM Features
- Added resume APIs for partial pre-rendering with Web Streams:
- [`resume`](https://react.dev/reference/react-dom/server/resume): to resume a prerender to a stream.
- [`resumeAndPrerender`](https://react.dev/reference/react-dom/static/resumeAndPrerender): to resume a prerender to HTML.
- Added resume APIs for partial pre-rendering with Node Streams:
- [`resumeToPipeableStream`](https://react.dev/reference/react-dom/server/resumeToPipeableStream): to resume a prerender to a stream.
- [`resumeAndPrerenderToNodeStream`](https://react.dev/reference/react-dom/static/resumeAndPrerenderToNodeStream): to resume a prerender to HTML.
- Updated [`prerender`](https://react.dev/reference/react-dom/static/prerender) APIs to return a `postponed` state that can be passed to the `resume` APIs.
### Notable changes
- React DOM now batches suspense boundary reveals, matching the behavior of client side rendering. This change is especially noticeable when animating the reveal of Suspense boundaries e.g. with the upcoming `<ViewTransition>` Component. React will batch as much reveals as possible before the first paint while trying to hit popular first-contentful paint metrics.
- Add Node Web Streams (`prerender`, `renderToReadableStream`) to server-side-rendering APIs for Node.js
- Use underscore instead of `:` IDs generated by useId
### All Changes
#### React
- `<Activity />` was developed over many years, starting before `ClassComponent.setState` (@acdlite @sebmarkbage and many others)
- Stringify context as "SomeContext" instead of "SomeContext.Provider" (@kassens [#33507](https://github.com/facebook/react/pull/33507))
- Include stack of cause of React instrumentation errors with `%o` placeholder (@eps1lon [#34198](https://github.com/facebook/react/pull/34198))
- Fix infinite `useDeferredValue` loop in popstate event (@acdlite [#32821](https://github.com/facebook/react/pull/32821))
- Fix a bug when an initial value was passed to `useDeferredValue` (@acdlite [#34376](https://github.com/facebook/react/pull/34376))
- Fix a crash when submitting forms with Client Actions (@sebmarkbage [#33055](https://github.com/facebook/react/pull/33055))
- Hide/unhide the content of dehydrated suspense boundaries if they resuspend (@sebmarkbage [#32900](https://github.com/facebook/react/pull/32900))
- Avoid stack overflow on wide trees during Hot Reload (@sophiebits [#34145](https://github.com/facebook/react/pull/34145))
- Improve Owner and Component stacks in various places (@sebmarkbage, @eps1lon: [#33629](https://github.com/facebook/react/pull/33629), [#33724](https://github.com/facebook/react/pull/33724), [#32735](https://github.com/facebook/react/pull/32735), [#33723](https://github.com/facebook/react/pull/33723))
- Add `cacheSignal` (@sebmarkbage [#33557](https://github.com/facebook/react/pull/33557))
#### React DOM
- Block on Suspensey Fonts during reveal of server-side-rendered content (@sebmarkbage [#33342](https://github.com/facebook/react/pull/33342))
- Use underscore instead of `:` for IDs generated by `useId` (@sebmarkbage, @eps1lon: [#32001](https://github.com/facebook/react/pull/32001), [https://github.com/facebook/react/pull/33342](https://github.com/facebook/react/pull/33342)[#33099](https://github.com/facebook/react/pull/33099), [#33422](https://github.com/facebook/react/pull/33422))
- Stop warning when ARIA 1.3 attributes are used (@Abdul-Omira [#34264](https://github.com/facebook/react/pull/34264))
- Allow `nonce` to be used on hoistable styles (@Andarist [#32461](https://github.com/facebook/react/pull/32461))
- Warn for using a React owned node as a Container if it also has text content (@sebmarkbage [#32774](https://github.com/facebook/react/pull/32774))
- s/HTML/text for for error messages if text hydration mismatches (@rickhanlonii [#32763](https://github.com/facebook/react/pull/32763))
- Fix a bug with `React.use` inside `React.lazy`\-ed Component (@hi-ogawa [#33941](https://github.com/facebook/react/pull/33941))
- Enable the `progressiveChunkSize` option for server-side-rendering APIs (@sebmarkbage [#33027](https://github.com/facebook/react/pull/33027))
- Fix a bug with deeply nested Suspense inside Suspense fallback when server-side-rendering (@gnoff [#33467](https://github.com/facebook/react/pull/33467))
- Avoid hanging when suspending after aborting while rendering (@gnoff [#34192](https://github.com/facebook/react/pull/34192))
- Add Node Web Streams to server-side-rendering APIs for Node.js (@sebmarkbage [#33475](https://github.com/facebook/react/pull/33475))
#### React Server Components
- Preload `<img>` and `<link>` using hints before they're rendered (@sebmarkbage [#34604](https://github.com/facebook/react/pull/34604))
- Log error if production elements are rendered during development (@eps1lon [#34189](https://github.com/facebook/react/pull/34189))
- Fix a bug when returning a Temporary reference (e.g. a Client Reference) from Server Functions (@sebmarkbage [#34084](https://github.com/facebook/react/pull/34084), @denk0403 [#33761](https://github.com/facebook/react/pull/33761))
- Pass line/column to `filterStackFrame` (@eps1lon [#33707](https://github.com/facebook/react/pull/33707))
- Support Async Modules in Turbopack Server References (@lubieowoce [#34531](https://github.com/facebook/react/pull/34531))
- Add support for .mjs file extension in Webpack (@jennyscript [#33028](https://github.com/facebook/react/pull/33028))
- Fix a wrong missing key warning (@unstubbable [#34350](https://github.com/facebook/react/pull/34350))
- Make console log resolve in predictable order (@sebmarkbage [#33665](https://github.com/facebook/react/pull/33665))
#### React Reconciler
- [createContainer](https://github.com/facebook/react/blob/v19.2.0/packages/react-reconciler/src/ReactFiberReconciler.js#L255-L261) and [createHydrationContainer](https://github.com/facebook/react/blob/v19.2.0/packages/react-reconciler/src/ReactFiberReconciler.js#L305-L312) had their parameter order adjusted after `on*` handlers to account for upcoming experimental APIs
## 19.1.2 (Dec 3, 2025)
### React Server Components
- Bring React Server Component fixes to Server Actions (@sebmarkbage [#35277](https://github.com/facebook/react/pull/35277))
## 19.1.1 (July 28, 2025)
### React
@@ -135,12 +50,6 @@ An Owner Stack is a string representing the components that are directly respons
* Exposed `registerServerReference` in client builds to handle server references in different environments. [#32534](https://github.com/facebook/react/pull/32534)
* Added react-server-dom-parcel package which integrates Server Components with the [Parcel bundler](https://parceljs.org/) [#31725](https://github.com/facebook/react/pull/31725), [#32132](https://github.com/facebook/react/pull/32132), [#31799](https://github.com/facebook/react/pull/31799), [#32294](https://github.com/facebook/react/pull/32294), [#31741](https://github.com/facebook/react/pull/31741)
## 19.0.1 (Dec 3, 2025)
### React Server Components
- Bring React Server Component fixes to Server Actions (@sebmarkbage [#35277](https://github.com/facebook/react/pull/35277))
## 19.0.0 (December 5, 2024)
Below is a list of all new features, APIs, deprecations, and breaking changes. Read [React 19 release post](https://react.dev/blog/2024/04/25/react-19) and [React 19 upgrade guide](https://react.dev/blog/2024/04/25/react-19-upgrade-guide) for more information.

View File

@@ -1,6 +1,5 @@
acdlite
eps1lon
EugeneChoi4
gaearon
gnoff
unstubbable

View File

@@ -7,18 +7,18 @@
//
// The @latest channel uses the version as-is, e.g.:
//
// 19.3.0
// 19.1.0
//
// The @canary channel appends additional information, with the scheme
// <version>-<label>-<commit_sha>, e.g.:
//
// 19.3.0-canary-a1c2d3e4
// 19.1.0-canary-a1c2d3e4
//
// The @experimental channel doesn't include a version, only a date and a sha, e.g.:
//
// 0.0.0-experimental-241c4467e-20200129
const ReactVersion = '19.3.0';
const ReactVersion = '19.2.0';
// The label used by the @canary channel. Represents the upcoming release's
// stability. Most of the time, this will be "canary", but we may temporarily
@@ -33,8 +33,8 @@ const canaryChannelLabel = 'canary';
const rcNumber = 0;
const stablePackages = {
'eslint-plugin-react-hooks': '7.1.0',
'jest-react': '0.18.0',
'eslint-plugin-react-hooks': '6.1.0',
'jest-react': '0.17.0',
react: ReactVersion,
'react-art': ReactVersion,
'react-dom': ReactVersion,
@@ -42,12 +42,12 @@ const stablePackages = {
'react-server-dom-turbopack': ReactVersion,
'react-server-dom-parcel': ReactVersion,
'react-is': ReactVersion,
'react-reconciler': '0.34.0',
'react-refresh': '0.19.0',
'react-reconciler': '0.33.0',
'react-refresh': '0.18.0',
'react-test-renderer': ReactVersion,
'use-subscription': '1.13.0',
'use-sync-external-store': '1.7.0',
scheduler: '0.28.0',
'use-subscription': '1.12.0',
'use-sync-external-store': '1.6.0',
scheduler: '0.27.0',
};
// These packages do not exist in the @canary or @latest channel, only

View File

@@ -1,9 +0,0 @@
{
"permissions": {
"allow": [
"Bash(node scripts/enable-feature-flag.js:*)"
],
"deny": [],
"ask": []
}
}

View File

@@ -12,7 +12,6 @@
# next.js
/.next/
/out/
/next-env.d.ts
# production
/build

View File

@@ -1,4 +1,5 @@
import { c as _c } from "react/compiler-runtime"; // @compilationMode:"all"
import { c as _c } from "react/compiler-runtime"; // 
@compilationMode:"all"
function nonReactFn() {
  const $ = _c(1);
  let t0;

View File

@@ -1,5 +0,0 @@
import type { PluginOptions } from 
'babel-plugin-react-compiler/dist';
({
  //compilationMode: "all"
} satisfies PluginOptions);

View File

@@ -1,14 +0,0 @@
import { c as _c } from "react/compiler-runtime";
export default function TestComponent(t0) {
const $ = _c(2);
const { x } = t0;
let t1;
if ($[0] !== x || true) {
t1 = <Button>{x}</Button>;
$[0] = x;
$[1] = t1;
} else {
t1 = $[1];
}
return t1;
}

View File

@@ -5,9 +5,8 @@
* LICENSE file in the root directory of this source tree.
*/
import {expect, test, type Page} from '@playwright/test';
import {expect, test} from '@playwright/test';
import {encodeStore, type Store} from '../../lib/stores';
import {defaultConfig} from '../../lib/defaultStore';
import {format} from 'prettier';
function isMonacoLoaded(): boolean {
@@ -21,16 +20,6 @@ function formatPrint(data: Array<string>): Promise<string> {
return format(data.join(''), {parser: 'babel'});
}
async function expandConfigs(page: Page): Promise<void> {
const expandButton = page.locator('[title="Expand config editor"]');
await expandButton.click();
await page.waitForSelector('.monaco-editor-config', {state: 'visible'});
}
const TEST_SOURCE = `export default function TestComponent({ x }) {
return <Button>{x}</Button>;
}`;
const TEST_CASE_INPUTS = [
{
name: 'module-scope-use-memo',
@@ -132,9 +121,10 @@ test('editor should open successfully', async ({page}) => {
test('editor should compile from hash successfully', async ({page}) => {
const store: Store = {
source: TEST_SOURCE,
config: defaultConfig,
showInternals: false,
source: `export default function TestComponent({ x }) {
return <Button>{x}</Button>;
}
`,
};
const hash = encodeStore(store);
await page.goto(`/#${hash}`, {waitUntil: 'networkidle'});
@@ -146,7 +136,7 @@ test('editor should compile from hash successfully', async ({page}) => {
path: 'test-results/01-compiles-from-hash.png',
});
const text =
(await page.locator('.monaco-editor-output').allInnerTexts()) ?? [];
(await page.locator('.monaco-editor').nth(1).allInnerTexts()) ?? [];
const output = await formatPrint(text);
expect(output).not.toEqual('');
@@ -155,9 +145,10 @@ test('editor should compile from hash successfully', async ({page}) => {
test('reset button works', async ({page}) => {
const store: Store = {
source: TEST_SOURCE,
config: defaultConfig,
showInternals: false,
source: `export default function TestComponent({ x }) {
return <Button>{x}</Button>;
}
`,
};
const hash = encodeStore(store);
await page.goto(`/#${hash}`, {waitUntil: 'networkidle'});
@@ -166,201 +157,33 @@ test('reset button works', async ({page}) => {
// Reset button works
page.on('dialog', dialog => dialog.accept());
await page.getByRole('button', {name: 'Reset'}).click();
await expandConfigs(page);
await page.screenshot({
fullPage: true,
path: 'test-results/02-reset-button-works.png',
});
const text =
(await page.locator('.monaco-editor-output').allInnerTexts()) ?? [];
(await page.locator('.monaco-editor').nth(1).allInnerTexts()) ?? [];
const output = await formatPrint(text);
const configText =
(await page.locator('.monaco-editor-config').allInnerTexts()) ?? [];
const configOutput = configText.join('');
expect(output).not.toEqual('');
expect(output).toMatchSnapshot('02-default-output.txt');
expect(configOutput).not.toEqual('');
expect(configOutput).toMatchSnapshot('default-config.txt');
});
test('defaults load when only source is in Store', async ({page}) => {
// Test for backwards compatibility
const partial = {
source: TEST_SOURCE,
};
const hash = encodeStore(partial as Store);
await page.goto(`/#${hash}`, {waitUntil: 'networkidle'});
await page.waitForFunction(isMonacoLoaded);
await expandConfigs(page);
await page.screenshot({
fullPage: true,
path: 'test-results/03-missing-defaults.png',
});
// Config editor has default config
const configText =
(await page.locator('.monaco-editor-config').allInnerTexts()) ?? [];
const configOutput = configText.join('');
expect(configOutput).not.toEqual('');
expect(configOutput).toMatchSnapshot('default-config.txt');
const checkbox = page.locator('label.show-internals');
await expect(checkbox).not.toBeChecked();
const ssaTab = page.locator('text=SSA');
await expect(ssaTab).not.toBeVisible();
});
test('show internals button toggles correctly', async ({page}) => {
await page.goto(`/`, {waitUntil: 'networkidle'});
await page.waitForFunction(isMonacoLoaded);
// show internals should be off
const checkbox = page.locator('label.show-internals');
await checkbox.click();
await page.screenshot({
fullPage: true,
path: 'test-results/04-show-internals-on.png',
});
await expect(checkbox).toBeChecked();
const ssaTab = page.locator('text=SSA');
await expect(ssaTab).toBeVisible();
});
test('error is displayed when config has syntax error', async ({page}) => {
const store: Store = {
source: TEST_SOURCE,
config: `compilationMode: `,
showInternals: false,
};
const hash = encodeStore(store);
await page.goto(`/#${hash}`, {waitUntil: 'networkidle'});
await page.waitForFunction(isMonacoLoaded);
await expandConfigs(page);
await page.screenshot({
fullPage: true,
path: 'test-results/05-config-syntax-error.png',
});
const text =
(await page.locator('.monaco-editor-output').allInnerTexts()) ?? [];
const output = text.join('');
// Remove hidden chars
expect(output.replace(/\s+/g, ' ')).toContain('Invalid override format');
});
test('error is displayed when config has validation error', async ({page}) => {
const store: Store = {
source: TEST_SOURCE,
config: `import type { PluginOptions } from 'babel-plugin-react-compiler/dist';
({
compilationMode: "123"
} satisfies PluginOptions);`,
showInternals: false,
};
const hash = encodeStore(store);
await page.goto(`/#${hash}`, {waitUntil: 'networkidle'});
await page.waitForFunction(isMonacoLoaded);
await expandConfigs(page);
await page.screenshot({
fullPage: true,
path: 'test-results/06-config-validation-error.png',
});
const text =
(await page.locator('.monaco-editor-output').allInnerTexts()) ?? [];
const output = text.join('');
expect(output.replace(/\s+/g, ' ')).toContain('Unexpected compilationMode');
});
test('disableMemoizationForDebugging flag works as expected', async ({
page,
}) => {
const store: Store = {
source: TEST_SOURCE,
config: `import type { PluginOptions } from 'babel-plugin-react-compiler/dist';
({
environment: {
disableMemoizationForDebugging: true
}
} satisfies PluginOptions);`,
showInternals: false,
};
const hash = encodeStore(store);
await page.goto(`/#${hash}`, {waitUntil: 'networkidle'});
await page.waitForFunction(isMonacoLoaded);
await expandConfigs(page);
await page.screenshot({
fullPage: true,
path: 'test-results/07-config-disableMemoizationForDebugging-flag.png',
});
const text =
(await page.locator('.monaco-editor-output').allInnerTexts()) ?? [];
const output = await formatPrint(text);
expect(output).not.toEqual('');
expect(output).toMatchSnapshot('disableMemoizationForDebugging-output.txt');
});
test('error is displayed when source has syntax error', async ({page}) => {
const syntaxErrorSource = `function TestComponent(props) {
const oops = props.
return (
<>{oops}</>
);
}`;
const store: Store = {
source: syntaxErrorSource,
config: defaultConfig,
showInternals: false,
};
const hash = encodeStore(store);
await page.goto(`/#${hash}`);
await page.waitForFunction(isMonacoLoaded);
await expandConfigs(page);
await page.screenshot({
fullPage: true,
path: 'test-results/08-source-syntax-error.png',
});
const text =
(await page.locator('.monaco-editor-output').allInnerTexts()) ?? [];
const output = text.join('');
expect(output.replace(/\s+/g, ' ')).toContain(
'Expected identifier to be defined before being used',
);
});
TEST_CASE_INPUTS.forEach((t, idx) =>
test(`playground compiles: ${t.name}`, async ({page}) => {
const store: Store = {
source: t.input,
config: defaultConfig,
showInternals: false,
};
const hash = encodeStore(store);
await page.goto(`/#${hash}`, {waitUntil: 'networkidle'});
await page.waitForFunction(isMonacoLoaded);
await page.screenshot({
fullPage: true,
path: `test-results/08-0${idx}-${t.name}.png`,
path: `test-results/03-0${idx}-${t.name}.png`,
});
const text =
(await page.locator('.monaco-editor-output').allInnerTexts()) ?? [];
(await page.locator('.monaco-editor').nth(1).allInnerTexts()) ?? [];
let output: string;
if (t.noFormat) {
output = text.join('');

View File

@@ -0,0 +1,56 @@
/**
* 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 type {NextPage} from 'next';
import Head from 'next/head';
import {SnackbarProvider} from 'notistack';
import {Editor, Header, StoreProvider} from '../components';
import MessageSnackbar from '../components/Message';
const Home: NextPage = () => {
return (
<div className="flex flex-col w-screen h-screen font-light">
<Head>
<title>
{process.env.NODE_ENV === 'development'
? '[DEV] React Compiler Playground'
: 'React Compiler Playground'}
</title>
<meta
name="viewport"
content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0"></meta>
<link rel="icon" href="/favicon.ico" />
<link rel="manifest" href="/site.webmanifest" />
<link
rel="preload"
href="/fonts/Source-Code-Pro-Regular.woff2"
as="font"
type="font/woff2"
crossOrigin="anonymous"
/>
<link
rel="preload"
href="/fonts/Optimistic_Display_W_Lt.woff2"
as="font"
type="font/woff2"
crossOrigin="anonymous"
/>
</Head>
<StoreProvider>
<SnackbarProvider
preventDuplicate
maxSnack={10}
Components={{message: MessageSnackbar}}>
<Header />
<Editor />
</SnackbarProvider>
</StoreProvider>
</div>
);
};
export default Home;

View File

@@ -1,126 +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 {Resizable} from 're-resizable';
import React, {
useId,
unstable_ViewTransition as ViewTransition,
unstable_addTransitionType as addTransitionType,
startTransition,
} from 'react';
import {EXPAND_ACCORDION_TRANSITION} from '../lib/transitionTypes';
type TabsRecord = Map<string, React.ReactNode>;
export default function AccordionWindow(props: {
defaultTab: string | null;
tabs: TabsRecord;
tabsOpen: Set<string>;
setTabsOpen: (newTab: Set<string>) => void;
changedPasses: Set<string>;
}): React.ReactElement {
return (
<div className="flex-1 min-w-[550px] sm:min-w-0">
<div className="flex flex-row h-full">
{Array.from(props.tabs.keys()).map(name => {
return (
<AccordionWindowItem
name={name}
key={name}
tabs={props.tabs}
tabsOpen={props.tabsOpen}
setTabsOpen={props.setTabsOpen}
hasChanged={props.changedPasses.has(name)}
/>
);
})}
</div>
</div>
);
}
function AccordionWindowItem({
name,
tabs,
tabsOpen,
setTabsOpen,
hasChanged,
}: {
name: string;
tabs: TabsRecord;
tabsOpen: Set<string>;
setTabsOpen: (newTab: Set<string>) => void;
hasChanged: boolean;
isFailure: boolean;
}): React.ReactElement {
const id = useId();
const isShow = tabsOpen.has(name);
const transitionName = `accordion-window-item-${id}`;
const toggleTabs = (): void => {
startTransition(() => {
addTransitionType(EXPAND_ACCORDION_TRANSITION);
const nextState = new Set(tabsOpen);
if (nextState.has(name)) {
nextState.delete(name);
} else {
nextState.add(name);
}
setTabsOpen(nextState);
});
};
// Replace spaces with non-breaking spaces
const displayName = name.replace(/ /g, '\u00A0');
return (
<div key={name} className="flex flex-row">
{isShow ? (
<ViewTransition
name={transitionName}
update={{
[EXPAND_ACCORDION_TRANSITION]: 'expand-accordion',
default: 'none',
}}>
<Resizable className="border-r" minWidth={550} enable={{right: true}}>
<h2
title="Minimize tab"
aria-label="Minimize tab"
onClick={toggleTabs}
className={`p-4 duration-150 ease-in border-b cursor-pointer border-grey-200 ${
hasChanged ? 'font-bold' : 'font-light'
} text-secondary hover:text-link`}>
- {displayName}
</h2>
{tabs.get(name) ?? <div>No output for {name}</div>}
</Resizable>
</ViewTransition>
) : (
<ViewTransition
name={transitionName}
update={{
[EXPAND_ACCORDION_TRANSITION]: 'expand-accordion',
default: 'none',
}}>
<div className="relative items-center h-full px-1 py-6 align-middle border-r border-grey-200">
<button
title={`Expand compiler tab: ${name}`}
aria-label={`Expand compiler tab: ${name}`}
style={{transform: 'rotate(90deg) translate(-50%)'}}
onClick={toggleTabs}
className={`flex-grow-0 w-5 transition-colors duration-150 ease-in ${
hasChanged ? 'font-bold' : 'font-light'
} text-secondary hover:text-link`}>
{displayName}
</button>
</div>
</ViewTransition>
)}
</div>
);
}

View File

@@ -8,97 +8,84 @@
import MonacoEditor, {loader, type Monaco} from '@monaco-editor/react';
import type {editor} from 'monaco-editor';
import * as monaco from 'monaco-editor';
import React, {
useState,
useRef,
unstable_ViewTransition as ViewTransition,
unstable_addTransitionType as addTransitionType,
startTransition,
} from 'react';
import React, {useState, useCallback} from 'react';
import {Resizable} from 're-resizable';
import {useSnackbar} from 'notistack';
import {useStore, useStoreDispatch} from '../StoreContext';
import {monacoConfigOptions} from './monacoOptions';
import {IconChevron} from '../Icons/IconChevron';
import {CONFIG_PANEL_TRANSITION} from '../../lib/transitionTypes';
import {monacoOptions} from './monacoOptions';
import {
ConfigError,
generateOverridePragmaFromConfig,
updateSourceWithOverridePragma,
} from '../../lib/configUtils';
// @ts-expect-error - webpack asset/source loader handles .d.ts files as strings
import compilerTypeDefs from 'babel-plugin-react-compiler/dist/index.d.ts';
loader.config({monaco});
export default function ConfigEditor({
formattedAppliedConfig,
}: {
formattedAppliedConfig: string;
}): React.ReactElement {
export default function ConfigEditor(): React.ReactElement {
const [isExpanded, setIsExpanded] = useState(false);
// TODO: Add back <Activity> after upgrading next.js
return (
<>
<div
style={{
display: isExpanded ? 'block' : 'none',
}}>
{/* <Activity mode={isExpanded ? 'visible' : 'hidden'}> */}
<ExpandedEditor
onToggle={() => {
startTransition(() => {
addTransitionType(CONFIG_PANEL_TRANSITION);
setIsExpanded(false);
});
}}
formattedAppliedConfig={formattedAppliedConfig}
/>
</div>
<div
style={{
display: !isExpanded ? 'block' : 'none',
}}>
{/* </Activity>
<Activity mode={isExpanded ? 'hidden' : 'visible'}></Activity> */}
<CollapsedEditor
onToggle={() => {
startTransition(() => {
addTransitionType(CONFIG_PANEL_TRANSITION);
setIsExpanded(true);
});
}}
/>
</div>
{/* </Activity> */}
</>
);
}
function ExpandedEditor({
onToggle,
formattedAppliedConfig,
}: {
onToggle: (expanded: boolean) => void;
formattedAppliedConfig: string;
}): React.ReactElement {
const store = useStore();
const dispatchStore = useStoreDispatch();
const debounceTimerRef = useRef<NodeJS.Timeout | null>(null);
const {enqueueSnackbar} = useSnackbar();
const handleChange: (value: string | undefined) => void = (
value: string | undefined,
) => {
if (value === undefined) return;
const toggleExpanded = useCallback(() => {
setIsExpanded(prev => !prev);
}, []);
if (debounceTimerRef.current) {
clearTimeout(debounceTimerRef.current);
}
const handleApplyConfig: () => Promise<void> = async () => {
try {
const config = store.config || '';
if (!config.trim()) {
enqueueSnackbar(
'Config is empty. Please add configuration options first.',
{
variant: 'warning',
},
);
return;
}
const newPragma = await generateOverridePragmaFromConfig(config);
const updatedSource = updateSourceWithOverridePragma(
store.source,
newPragma,
);
debounceTimerRef.current = setTimeout(() => {
dispatchStore({
type: 'updateConfig',
type: 'updateFile',
payload: {
config: value,
source: updatedSource,
config: config,
},
});
}, 500); // 500ms debounce delay
} catch (error) {
console.error('Failed to apply config:', error);
if (error instanceof ConfigError && error.message.trim()) {
enqueueSnackbar(error.message, {
variant: 'error',
});
} else {
enqueueSnackbar('Unexpected error: failed to apply config.', {
variant: 'error',
});
}
}
};
const handleChange: (value: string | undefined) => void = value => {
if (value === undefined) return;
// Only update the config
dispatchStore({
type: 'updateFile',
payload: {
source: store.source,
config: value,
},
});
};
const handleMount: (
@@ -122,100 +109,75 @@ function ExpandedEditor({
allowSyntheticDefaultImports: true,
jsx: monaco.languages.typescript.JsxEmit.React,
});
const uri = monaco.Uri.parse(`file:///config.ts`);
const model = monaco.editor.getModel(uri);
if (model) {
model.updateOptions({tabSize: 2});
}
};
return (
<ViewTransition
update={{[CONFIG_PANEL_TRANSITION]: 'slide-in', default: 'none'}}>
{/* enter={{[CONFIG_PANEL_TRANSITION]: 'slide-in', default: 'none'}}
exit={{[CONFIG_PANEL_TRANSITION]: 'slide-out', default: 'none'}}> */}
<Resizable
minWidth={300}
maxWidth={600}
defaultSize={{width: 350}}
enable={{right: true, bottom: false}}>
<div className="bg-blue-10 relative h-full flex flex-col !h-[calc(100vh_-_3.5rem)] border border-gray-300">
<div
className="absolute w-8 h-16 bg-blue-10 rounded-r-full flex items-center justify-center z-[2] cursor-pointer border border-l-0 border-gray-300"
title="Minimize config editor"
onClick={onToggle}
style={{
top: '50%',
marginTop: '-32px',
right: '-32px',
borderTopLeftRadius: 0,
borderBottomLeftRadius: 0,
}}>
<IconChevron displayDirection="left" className="text-blue-50" />
</div>
<div className="flex-1 flex flex-col m-2 mb-2">
<div className="pb-2">
<h2 className="inline-block text-blue-50 py-1.5 px-1.5 xs:px-3 sm:px-4 text-sm">
Config Overrides
</h2>
</div>
<div className="flex-1 border border-gray-300">
<div className="flex flex-row relative">
{isExpanded ? (
<>
<Resizable
className="border-r"
minWidth={300}
maxWidth={600}
defaultSize={{width: 350, height: 'auto'}}
enable={{right: true}}>
<h2
title="Minimize config editor"
aria-label="Minimize config editor"
onClick={toggleExpanded}
className="p-4 duration-150 ease-in border-b cursor-pointer border-grey-200 font-light text-secondary hover:text-link">
- Config Overrides
</h2>
<div className="h-[calc(100vh_-_3.5rem_-_4rem)]">
<MonacoEditor
path={'config.ts'}
language={'typescript'}
value={store.config}
onMount={handleMount}
onChange={handleChange}
loading={''}
className="monaco-editor-config"
options={monacoConfigOptions}
/>
</div>
</div>
<div className="flex-1 flex flex-col m-2">
<div className="pb-2">
<h2 className="inline-block text-blue-50 py-1.5 px-1.5 xs:px-3 sm:px-4 text-sm">
Applied Configs
</h2>
</div>
<div className="flex-1 border border-gray-300">
<MonacoEditor
path={'applied-config.js'}
language={'javascript'}
value={formattedAppliedConfig}
loading={''}
className="monaco-editor-applied-config"
options={{
...monacoConfigOptions,
readOnly: true,
...monacoOptions,
lineNumbers: 'off',
folding: false,
renderLineHighlight: 'none',
scrollBeyondLastLine: false,
hideCursorInOverviewRuler: true,
overviewRulerBorder: false,
overviewRulerLanes: 0,
fontSize: 12,
}}
/>
</div>
</div>
</Resizable>
<button
onClick={handleApplyConfig}
title="Apply config overrides to input"
aria-label="Apply config overrides to input"
className="absolute right-0 top-1/2 transform -translate-y-1/2 translate-x-1/2 z-10 w-8 h-8 bg-blue-500 hover:bg-blue-600 text-white rounded-full border-2 border-white shadow-lg flex items-center justify-center text-sm font-medium transition-colors duration-150">
</button>
</>
) : (
<div className="relative items-center h-full px-1 py-6 align-middle border-r border-grey-200">
<button
title="Expand config editor"
aria-label="Expand config editor"
style={{
transform: 'rotate(90deg) translate(-50%)',
whiteSpace: 'nowrap',
}}
onClick={toggleExpanded}
className="flex-grow-0 w-5 transition-colors duration-150 ease-in font-light text-secondary hover:text-link">
Config Overrides
</button>
</div>
</Resizable>
</ViewTransition>
);
}
function CollapsedEditor({
onToggle,
}: {
onToggle: () => void;
}): React.ReactElement {
return (
<div
className="w-4 !h-[calc(100vh_-_3.5rem)]"
style={{position: 'relative'}}>
<div
className="absolute w-10 h-16 bg-blue-10 hover:translate-x-2 transition-transform rounded-r-full flex items-center justify-center z-[2] cursor-pointer border border-gray-300"
title="Expand config editor"
onClick={onToggle}
style={{
top: '50%',
marginTop: '-32px',
left: '-8px',
borderTopLeftRadius: 0,
borderBottomLeftRadius: 0,
}}>
<IconChevron displayDirection="right" className="text-blue-50" />
</div>
)}
</div>
);
}

View File

@@ -5,30 +5,324 @@
* LICENSE file in the root directory of this source tree.
*/
import {
import {parse as babelParse, ParseResult} from '@babel/parser';
import * as HermesParser from 'hermes-parser';
import * as t from '@babel/types';
import BabelPluginReactCompiler, {
CompilerError,
CompilerErrorDetail,
CompilerDiagnostic,
Effect,
ErrorCategory,
parseConfigPragmaForTests,
ValueKind,
type Hook,
PluginOptions,
CompilerPipelineValue,
parsePluginOptions,
printReactiveFunctionWithOutlined,
printFunctionWithOutlined,
} from 'babel-plugin-react-compiler';
import {useDeferredValue, useMemo, useState} from 'react';
import {useStore} from '../StoreContext';
import clsx from 'clsx';
import invariant from 'invariant';
import {useSnackbar} from 'notistack';
import {useDeferredValue, useMemo} from 'react';
import {useMountEffect} from '../../hooks';
import {defaultStore} from '../../lib/defaultStore';
import {
createMessage,
initStoreFromUrlOrLocalStorage,
MessageLevel,
MessageSource,
type Store,
} from '../../lib/stores';
import {useStore, useStoreDispatch} from '../StoreContext';
import ConfigEditor from './ConfigEditor';
import Input from './Input';
import {CompilerOutput, default as Output} from './Output';
import {compile} from '../../lib/compilation';
import prettyFormat from 'pretty-format';
import {
CompilerOutput,
CompilerTransformOutput,
default as Output,
PrintedCompilerPipelineValue,
} from './Output';
import {transformFromAstSync} from '@babel/core';
import {LoggerEvent} from 'babel-plugin-react-compiler/dist/Entrypoint';
import {useSearchParams} from 'next/navigation';
function parseInput(
input: string,
language: 'flow' | 'typescript',
): ParseResult<t.File> {
// Extract the first line to quickly check for custom test directives
if (language === 'flow') {
return HermesParser.parse(input, {
babel: true,
flow: 'all',
sourceType: 'module',
enableExperimentalComponentSyntax: true,
});
} else {
return babelParse(input, {
plugins: ['typescript', 'jsx'],
sourceType: 'module',
}) as ParseResult<t.File>;
}
}
function invokeCompiler(
source: string,
language: 'flow' | 'typescript',
options: PluginOptions,
): CompilerTransformOutput {
const ast = parseInput(source, language);
let result = transformFromAstSync(ast, source, {
filename: '_playgroundFile.js',
highlightCode: false,
retainLines: true,
plugins: [[BabelPluginReactCompiler, options]],
ast: true,
sourceType: 'module',
configFile: false,
sourceMaps: true,
babelrc: false,
});
if (result?.ast == null || result?.code == null || result?.map == null) {
throw new Error('Expected successful compilation');
}
return {
code: result.code,
sourceMaps: result.map,
language,
};
}
const COMMON_HOOKS: Array<[string, Hook]> = [
[
'useFragment',
{
valueKind: ValueKind.Frozen,
effectKind: Effect.Freeze,
noAlias: true,
transitiveMixedData: true,
},
],
[
'usePaginationFragment',
{
valueKind: ValueKind.Frozen,
effectKind: Effect.Freeze,
noAlias: true,
transitiveMixedData: true,
},
],
[
'useRefetchableFragment',
{
valueKind: ValueKind.Frozen,
effectKind: Effect.Freeze,
noAlias: true,
transitiveMixedData: true,
},
],
[
'useLazyLoadQuery',
{
valueKind: ValueKind.Frozen,
effectKind: Effect.Freeze,
noAlias: true,
transitiveMixedData: true,
},
],
[
'usePreloadedQuery',
{
valueKind: ValueKind.Frozen,
effectKind: Effect.Freeze,
noAlias: true,
transitiveMixedData: true,
},
],
];
function compile(
source: string,
mode: 'compiler' | 'linter',
): [CompilerOutput, 'flow' | 'typescript'] {
const results = new Map<string, Array<PrintedCompilerPipelineValue>>();
const error = new CompilerError();
const otherErrors: Array<CompilerErrorDetail | CompilerDiagnostic> = [];
const upsert: (result: PrintedCompilerPipelineValue) => void = result => {
const entry = results.get(result.name);
if (Array.isArray(entry)) {
entry.push(result);
} else {
results.set(result.name, [result]);
}
};
let language: 'flow' | 'typescript';
if (source.match(/\@flow/)) {
language = 'flow';
} else {
language = 'typescript';
}
let transformOutput;
try {
// Extract the first line to quickly check for custom test directives
const pragma = source.substring(0, source.indexOf('\n'));
const logIR = (result: CompilerPipelineValue): void => {
switch (result.kind) {
case 'ast': {
break;
}
case 'hir': {
upsert({
kind: 'hir',
fnName: result.value.id,
name: result.name,
value: printFunctionWithOutlined(result.value),
});
break;
}
case 'reactive': {
upsert({
kind: 'reactive',
fnName: result.value.id,
name: result.name,
value: printReactiveFunctionWithOutlined(result.value),
});
break;
}
case 'debug': {
upsert({
kind: 'debug',
fnName: null,
name: result.name,
value: result.value,
});
break;
}
default: {
const _: never = result;
throw new Error(`Unhandled result ${result}`);
}
}
};
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,
environment: {
...parsedOptions.environment,
customHooks: new Map([...COMMON_HOOKS]),
},
logger: {
debugLogIRs: logIR,
logEvent: (_filename: string | null, event: LoggerEvent) => {
if (event.kind === 'CompileError') {
otherErrors.push(event.detail);
}
},
},
});
transformOutput = invokeCompiler(source, language, opts);
} catch (err) {
/**
* error might be an invariant violation or other runtime error
* (i.e. object shape that is not CompilerError)
*/
if (err instanceof CompilerError && err.details.length > 0) {
error.merge(err);
} else {
/**
* Handle unexpected failures by logging (to get a stack trace)
* and reporting
*/
console.error(err);
error.details.push(
new CompilerErrorDetail({
category: ErrorCategory.Invariant,
reason: `Unexpected failure when transforming input! ${err}`,
loc: null,
suggestions: null,
}),
);
}
}
// 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}, language];
}
return [
{kind: 'ok', results, transformOutput, errors: error.details},
language,
];
}
export default function Editor(): JSX.Element {
const store = useStore();
const deferredStore = useDeferredValue(store);
const [compilerOutput, language, appliedOptions] = useMemo(
() => compile(deferredStore.source, 'compiler', deferredStore.config),
[deferredStore.source, deferredStore.config],
const dispatchStore = useStoreDispatch();
const {enqueueSnackbar} = useSnackbar();
const [compilerOutput, language] = useMemo(
() => compile(deferredStore.source, 'compiler'),
[deferredStore.source],
);
const [linterOutput] = useMemo(
() => compile(deferredStore.source, 'linter', deferredStore.config),
[deferredStore.source, deferredStore.config],
() => compile(deferredStore.source, 'linter'),
[deferredStore.source],
);
const [formattedAppliedConfig, setFormattedAppliedConfig] = useState('');
// 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();
} catch (e) {
invariant(e instanceof Error, 'Only Error may be caught.');
enqueueSnackbar(e.message, {
variant: 'warning',
...createMessage(
'Bad URL - fell back to the default Playground.',
MessageLevel.Info,
MessageSource.Playground,
),
});
mountStore = defaultStore;
}
dispatchStore({
type: 'setStore',
payload: {
store: mountStore,
},
});
});
let mergedOutput: CompilerOutput;
let errors: Array<CompilerErrorDetail | CompilerDiagnostic>;
@@ -42,25 +336,14 @@ export default function Editor(): JSX.Element {
mergedOutput = compilerOutput;
errors = compilerOutput.error.details;
}
if (appliedOptions) {
const formatted = prettyFormat(appliedOptions, {
printFunctionName: false,
printBasicPrototype: false,
});
if (formatted !== formattedAppliedConfig) {
setFormattedAppliedConfig(formatted);
}
}
return (
<>
<div className="relative flex top-14">
<div className="flex-shrink-0">
<ConfigEditor formattedAppliedConfig={formattedAppliedConfig} />
</div>
<div className="flex flex-1 min-w-0">
<div className="relative flex basis top-14">
{shouldShowConfig && <ConfigEditor />}
<div className={clsx('relative sm:basis-1/4')}>
<Input language={language} errors={errors} />
</div>
<div className={clsx('flex sm:flex flex-wrap')}>
<Output store={deferredStore} compilerOutput={mergedOutput} />
</div>
</div>

View File

@@ -6,31 +6,23 @@
*/
import MonacoEditor, {loader, type Monaco} from '@monaco-editor/react';
import {
CompilerErrorDetail,
CompilerDiagnostic,
} from 'babel-plugin-react-compiler';
import {CompilerErrorDetail} from 'babel-plugin-react-compiler';
import invariant from 'invariant';
import type {editor} from 'monaco-editor';
import * as monaco from 'monaco-editor';
import {
useEffect,
useState,
unstable_ViewTransition as ViewTransition,
} from 'react';
import {Resizable} from 're-resizable';
import {useEffect, useState} from 'react';
import {renderReactCompilerMarkers} from '../../lib/reactCompilerMonacoDiagnostics';
import {useStore, useStoreDispatch} from '../StoreContext';
import TabbedWindow from '../TabbedWindow';
import {monacoOptions} from './monacoOptions';
import {CONFIG_PANEL_TRANSITION} from '../../lib/transitionTypes';
// @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});
type Props = {
errors: Array<CompilerErrorDetail | CompilerDiagnostic>;
errors: Array<CompilerErrorDetail>;
language: 'flow' | 'typescript';
};
@@ -51,6 +43,11 @@ export default function Input({errors, language}: Props): JSX.Element {
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, store.source]);
useEffect(() => {
@@ -86,10 +83,14 @@ export default function Input({errors, language}: Props): JSX.Element {
const handleChange: (value: string | undefined) => void = async value => {
if (!value) return;
// Parse and format the config
const config = await parseAndFormatConfig(value);
dispatchStore({
type: 'updateSource',
type: 'updateFile',
payload: {
source: value,
config,
},
});
};
@@ -139,42 +140,30 @@ export default function Input({errors, language}: Props): JSX.Element {
});
};
const editorContent = (
<MonacoEditor
path={'index.js'}
/**
* .js and .jsx files are specified to be TS so that Monaco can actually
* check their syntax using its TS language service. They are still JS files
* due to their extensions, so TS language features don't work.
*/
language={'javascript'}
value={store.source}
onMount={handleMount}
onChange={handleChange}
className="monaco-editor-input"
options={monacoOptions}
loading={''}
/>
);
const tabs = new Map([['Input', editorContent]]);
const [activeTab, setActiveTab] = useState('Input');
return (
<ViewTransition
update={{
[CONFIG_PANEL_TRANSITION]: 'container',
default: 'none',
}}>
<div className="flex-1 min-w-[550px] sm:min-w-0">
<div className="flex flex-col h-full !h-[calc(100vh_-_3.5rem)] border-r border-gray-200">
<TabbedWindow
tabs={tabs}
activeTab={activeTab}
onTabChange={setActiveTab}
/>
</div>
</div>
</ViewTransition>
<div className="relative flex flex-col flex-none border-r border-gray-200">
<Resizable
minWidth={650}
enable={{right: true}}
/**
* Restrict MonacoEditor's height, since the config autoLayout:true
* will grow the editor to fit within parent element
*/
className="!h-[calc(100vh_-_3.5rem)]">
<MonacoEditor
path={'index.js'}
/**
* .js and .jsx files are specified to be TS so that Monaco can actually
* check their syntax using its TS language service. They are still JS files
* due to their extensions, so TS language features don't work.
*/
language={'javascript'}
value={store.source}
onMount={handleMount}
onChange={handleChange}
options={monacoOptions}
/>
</Resizable>
</div>
);
}

View File

@@ -19,38 +19,15 @@ import {
import parserBabel from 'prettier/plugins/babel';
import * as prettierPluginEstree from 'prettier/plugins/estree';
import * as prettier from 'prettier/standalone';
import {memo, ReactNode, useEffect, useState} from 'react';
import {type Store} from '../../lib/stores';
import {
memo,
ReactNode,
use,
useState,
Suspense,
unstable_ViewTransition as ViewTransition,
unstable_addTransitionType as addTransitionType,
startTransition,
} from 'react';
import AccordionWindow from '../AccordionWindow';
import TabbedWindow from '../TabbedWindow';
import {monacoOptions} from './monacoOptions';
import {BabelFileResult} from '@babel/core';
import {
CONFIG_PANEL_TRANSITION,
TOGGLE_INTERNALS_TRANSITION,
EXPAND_ACCORDION_TRANSITION,
} from '../../lib/transitionTypes';
import {LRUCache} from 'lru-cache';
const MemoizedOutput = memo(Output);
export default MemoizedOutput;
export const BASIC_OUTPUT_TAB_NAMES = ['Output', 'SourceMap'];
const tabifyCache = new LRUCache<Store, Promise<Map<string, ReactNode>>>({
max: 5,
});
export type PrintedCompilerPipelineValue =
| {
kind: 'hir';
@@ -94,7 +71,7 @@ async function tabify(
const concattedResults = new Map<string, string>();
// Concat all top level function declaration results into a single tab for each pass
for (const [passName, results] of compilerOutput.results) {
if (!showInternals && !BASIC_OUTPUT_TAB_NAMES.includes(passName)) {
if (!showInternals && passName !== 'Output' && passName !== 'SourceMap') {
continue;
}
for (const result of results) {
@@ -219,25 +196,6 @@ ${code}
return reorderedTabs;
}
function tabifyCached(
store: Store,
compilerOutput: CompilerOutput,
): Promise<Map<string, ReactNode>> {
const cached = tabifyCache.get(store);
if (cached) return cached;
const result = tabify(store.source, compilerOutput, store.showInternals);
tabifyCache.set(store, result);
return result;
}
function Fallback(): JSX.Element {
return (
<div className="w-full h-monaco_small sm:h-monaco flex items-center justify-center">
Loading...
</div>
);
}
function utf16ToUTF8(s: string): string {
return unescape(encodeURIComponent(s));
}
@@ -251,18 +209,12 @@ function getSourceMapUrl(code: string, map: string): string | null {
}
function Output({store, compilerOutput}: Props): JSX.Element {
return (
<Suspense fallback={<Fallback />}>
<OutputContent store={store} compilerOutput={compilerOutput} />
</Suspense>
);
}
function OutputContent({store, compilerOutput}: Props): JSX.Element {
const [tabsOpen, setTabsOpen] = useState<Set<string>>(
() => new Set(['Output']),
);
const [activeTab, setActiveTab] = useState<string>('Output');
const [tabs, setTabs] = useState<Map<string, React.ReactNode>>(
() => new Map(),
);
/*
* Update the active tab back to the output or errors tab when the compilation state
@@ -271,19 +223,17 @@ function OutputContent({store, compilerOutput}: Props): JSX.Element {
const [previousOutputKind, setPreviousOutputKind] = useState(
compilerOutput.kind,
);
const isFailure = compilerOutput.kind !== 'ok';
if (compilerOutput.kind !== previousOutputKind) {
setPreviousOutputKind(compilerOutput.kind);
if (isFailure) {
startTransition(() => {
addTransitionType(EXPAND_ACCORDION_TRANSITION);
setTabsOpen(prev => new Set(prev).add('Output'));
setActiveTab('Output');
});
}
setTabsOpen(new Set(['Output']));
}
useEffect(() => {
tabify(store.source, compilerOutput, store.showInternals).then(tabs => {
setTabs(tabs);
});
}, [store.source, compilerOutput, store.showInternals]);
const changedPasses: Set<string> = new Set(['Output', 'HIR']); // Initial and final passes should always be bold
let lastResult: string = '';
for (const [passName, results] of compilerOutput.results) {
@@ -298,40 +248,17 @@ function OutputContent({store, compilerOutput}: Props): JSX.Element {
lastResult = currResult;
}
}
const tabs = use(tabifyCached(store, compilerOutput));
if (!store.showInternals) {
return (
<ViewTransition
update={{
[CONFIG_PANEL_TRANSITION]: 'container',
[TOGGLE_INTERNALS_TRANSITION]: '',
default: 'none',
}}>
<TabbedWindow
tabs={tabs}
activeTab={activeTab}
onTabChange={setActiveTab}
/>
</ViewTransition>
);
}
return (
<ViewTransition
update={{
[CONFIG_PANEL_TRANSITION]: 'accordion-container',
[TOGGLE_INTERNALS_TRANSITION]: '',
default: 'none',
}}>
<AccordionWindow
<>
<TabbedWindow
defaultTab={store.showInternals ? 'HIR' : 'Output'}
setTabsOpen={setTabsOpen}
tabsOpen={tabsOpen}
tabs={tabs}
changedPasses={changedPasses}
/>
</ViewTransition>
</>
);
}
@@ -383,29 +310,20 @@ function TextTabContent({
<DiffEditor
original={diff}
modified={output}
loading={''}
options={{
...monacoOptions,
scrollbar: {
vertical: 'hidden',
},
dimension: {
width: 0,
height: 0,
},
readOnly: true,
lineNumbers: 'off',
glyphMargin: false,
// Undocumented see https://github.com/Microsoft/vscode/issues/30795#issuecomment-410998882
overviewRulerLanes: 0,
lineDecorationsWidth: 0,
lineNumbersMinChars: 0,
}}
/>
) : (
<MonacoEditor
language={language ?? 'javascript'}
value={output}
loading={''}
className="monaco-editor-output"
options={{
...monacoOptions,
readOnly: true,

View File

@@ -29,17 +29,4 @@ export const monacoOptions: Partial<EditorProps['options']> = {
automaticLayout: true,
wordWrap: 'on',
wrappingIndent: 'same',
tabSize: 2,
};
export const monacoConfigOptions: Partial<EditorProps['options']> = {
...monacoOptions,
lineNumbers: 'off',
renderLineHighlight: 'none',
overviewRulerBorder: false,
overviewRulerLanes: 0,
fontSize: 12,
scrollBeyondLastLine: false,
glyphMargin: false,
};

View File

@@ -10,16 +10,11 @@ import {CheckIcon} from '@heroicons/react/solid';
import clsx from 'clsx';
import Link from 'next/link';
import {useSnackbar} from 'notistack';
import {
useState,
startTransition,
unstable_addTransitionType as addTransitionType,
} from 'react';
import {useState} from 'react';
import {defaultStore} from '../lib/defaultStore';
import {IconGitHub} from './Icons/IconGitHub';
import Logo from './Logo';
import {useStore, useStoreDispatch} from './StoreContext';
import {TOGGLE_INTERNALS_TRANSITION} from '../lib/transitionTypes';
export default function Header(): JSX.Element {
const [showCheck, setShowCheck] = useState(false);
@@ -63,16 +58,11 @@ export default function Header(): JSX.Element {
</div>
<div className="flex items-center text-[15px] gap-4">
<div className="flex items-center gap-2">
<label className="show-internals relative inline-block w-[34px] h-5">
<label className="relative inline-block w-[34px] h-5">
<input
type="checkbox"
checked={store.showInternals}
onChange={() =>
startTransition(() => {
addTransitionType(TOGGLE_INTERNALS_TRANSITION);
dispatchStore({type: 'toggleInternals'});
})
}
onChange={() => dispatchStore({type: 'toggleInternals'})}
className="absolute opacity-0 cursor-pointer h-full w-full m-0"
/>
<span
@@ -82,7 +72,7 @@ export default function Header(): JSX.Element {
'before:bg-white before:rounded-full before:transition-transform before:duration-250',
'focus-within:shadow-[0_0_1px_#2196F3]',
store.showInternals
? 'bg-link before:translate-x-3.5'
? 'bg-blue-500 before:translate-x-3.5'
: 'bg-gray-300',
)}></span>
</label>

View File

@@ -1,41 +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 {memo} from 'react';
export const IconChevron = memo<
JSX.IntrinsicElements['svg'] & {
/**
* The direction the arrow should point.
*/
displayDirection: 'right' | 'left';
}
>(function IconChevron({className, displayDirection, ...props}) {
const rotationClass =
displayDirection === 'left' ? 'rotate-90' : '-rotate-90';
const classes = className ? `${rotationClass} ${className}` : rotationClass;
return (
<svg
className={classes}
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 20 20"
{...props}>
<g fill="none" fillRule="evenodd" transform="translate(-446 -398)">
<path
fill="currentColor"
fillRule="nonzero"
d="M95.8838835,240.366117 C95.3957281,239.877961 94.6042719,239.877961 94.1161165,240.366117 C93.6279612,240.854272 93.6279612,241.645728 94.1161165,242.133883 L98.6161165,246.633883 C99.1042719,247.122039 99.8957281,247.122039 100.383883,246.633883 L104.883883,242.133883 C105.372039,241.645728 105.372039,240.854272 104.883883,240.366117 C104.395728,239.877961 103.604272,239.877961 103.116117,240.366117 L99.5,243.982233 L95.8838835,240.366117 Z"
transform="translate(356.5 164.5)"
/>
<polygon points="446 418 466 418 466 398 446 398" />
</g>
</svg>
);
});

View File

@@ -6,14 +6,10 @@
*/
import type {Dispatch, ReactNode} from 'react';
import {useState, useEffect, useReducer} from 'react';
import {useEffect, useReducer} from 'react';
import createContext from '../lib/createContext';
import {emptyStore, defaultStore} from '../lib/defaultStore';
import {
saveStore,
initStoreFromUrlOrLocalStorage,
type Store,
} from '../lib/stores';
import {emptyStore} from '../lib/defaultStore';
import {saveStore, type Store} from '../lib/stores';
const StoreContext = createContext<Store>();
@@ -34,20 +30,6 @@ export const useStoreDispatch = StoreDispatchContext.useContext;
*/
export function StoreProvider({children}: {children: ReactNode}): JSX.Element {
const [store, dispatch] = useReducer(storeReducer, emptyStore);
const [isPageReady, setIsPageReady] = useState<boolean>(false);
useEffect(() => {
let mountStore: Store;
try {
mountStore = initStoreFromUrlOrLocalStorage();
} catch (e) {
console.error('Failed to initialize store from URL or local storage', e);
mountStore = defaultStore;
}
dispatch({type: 'setStore', payload: {store: mountStore}});
setIsPageReady(true);
}, []);
useEffect(() => {
if (store !== emptyStore) {
saveStore(store);
@@ -57,7 +39,7 @@ export function StoreProvider({children}: {children: ReactNode}): JSX.Element {
return (
<StoreContext.Provider value={store}>
<StoreDispatchContext.Provider value={dispatch}>
{isPageReady ? children : null}
{children}
</StoreDispatchContext.Provider>
</StoreContext.Provider>
);
@@ -71,14 +53,9 @@ type ReducerAction =
};
}
| {
type: 'updateSource';
type: 'updateFile';
payload: {
source: string;
};
}
| {
type: 'updateConfig';
payload: {
config: string;
};
}
@@ -92,18 +69,11 @@ function storeReducer(store: Store, action: ReducerAction): Store {
const newStore = action.payload.store;
return newStore;
}
case 'updateSource': {
const source = action.payload.source;
case 'updateFile': {
const {source, config} = action.payload;
const newStore = {
...store,
source,
};
return newStore;
}
case 'updateConfig': {
const config = action.payload.config;
const newStore = {
...store,
config,
};
return newStore;

View File

@@ -4,78 +4,103 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import React, {
startTransition,
useId,
unstable_ViewTransition as ViewTransition,
unstable_addTransitionType as addTransitionType,
} from 'react';
import clsx from 'clsx';
import {TOGGLE_TAB_TRANSITION} from '../lib/transitionTypes';
export default function TabbedWindow({
tabs,
activeTab,
onTabChange,
}: {
tabs: Map<string, React.ReactNode>;
activeTab: string;
onTabChange: (tab: string) => void;
import {Resizable} from 're-resizable';
import React, {useCallback} from 'react';
type TabsRecord = Map<string, React.ReactNode>;
export default function TabbedWindow(props: {
defaultTab: string | null;
tabs: TabsRecord;
tabsOpen: Set<string>;
setTabsOpen: (newTab: Set<string>) => void;
changedPasses: Set<string>;
}): React.ReactElement {
const id = useId();
const transitionName = `tab-highlight-${id}`;
const handleTabChange = (tab: string): void => {
startTransition(() => {
addTransitionType(TOGGLE_TAB_TRANSITION);
onTabChange(tab);
});
};
return (
<div className="flex-1 min-w-[550px] sm:min-w-0">
<div className="flex flex-col h-full max-w-full">
<div className="flex p-2 flex-shrink-0">
{Array.from(tabs.keys()).map(tab => {
const isActive = activeTab === tab;
return (
<button
key={tab}
onClick={() => handleTabChange(tab)}
className={clsx(
'transition-transform py-1.5 px-1.5 xs:px-3 sm:px-4 rounded-full text-sm relative',
isActive ? 'text-link' : 'hover:bg-primary/5',
)}>
{isActive && (
<ViewTransition
name={transitionName}
enter={{default: 'none'}}
exit={{default: 'none'}}
share={{
[TOGGLE_TAB_TRANSITION]: 'tab-highlight',
default: 'none',
}}
update={{default: 'none'}}>
<div className="absolute inset-0 bg-highlight rounded-full" />
</ViewTransition>
)}
<ViewTransition
enter={{default: 'none'}}
exit={{default: 'none'}}
update={{
[TOGGLE_TAB_TRANSITION]: 'tab-text',
default: 'none',
}}>
<span className="relative z-1">{tab}</span>
</ViewTransition>
</button>
);
})}
</div>
<div className="flex-1 overflow-hidden w-full h-full">
{tabs.get(activeTab)}
</div>
if (props.tabs.size === 0) {
return (
<div
className="flex items-center justify-center"
style={{width: 'calc(100vw - 650px)'}}>
No compiler output detected, see errors below
</div>
);
}
return (
<div className="flex flex-row">
{Array.from(props.tabs.keys()).map(name => {
return (
<TabbedWindowItem
name={name}
key={name}
tabs={props.tabs}
tabsOpen={props.tabsOpen}
setTabsOpen={props.setTabsOpen}
hasChanged={props.changedPasses.has(name)}
/>
);
})}
</div>
);
}
function TabbedWindowItem({
name,
tabs,
tabsOpen,
setTabsOpen,
hasChanged,
}: {
name: string;
tabs: TabsRecord;
tabsOpen: Set<string>;
setTabsOpen: (newTab: Set<string>) => void;
hasChanged: boolean;
}): React.ReactElement {
const isShow = tabsOpen.has(name);
const toggleTabs = useCallback(() => {
const nextState = new Set(tabsOpen);
if (nextState.has(name)) {
nextState.delete(name);
} else {
nextState.add(name);
}
setTabsOpen(nextState);
}, [tabsOpen, name, setTabsOpen]);
// Replace spaces with non-breaking spaces
const displayName = name.replace(/ /g, '\u00A0');
return (
<div key={name} className="flex flex-row">
{isShow ? (
<Resizable className="border-r" minWidth={550} enable={{right: true}}>
<h2
title="Minimize tab"
aria-label="Minimize tab"
onClick={toggleTabs}
className={`p-4 duration-150 ease-in border-b cursor-pointer border-grey-200 ${
hasChanged ? 'font-bold' : 'font-light'
} text-secondary hover:text-link`}>
- {displayName}
</h2>
{tabs.get(name) ?? <div>No output for {name}</div>}
</Resizable>
) : (
<div className="relative items-center h-full px-1 py-6 align-middle border-r border-grey-200">
<button
title={`Expand compiler tab: ${name}`}
aria-label={`Expand compiler tab: ${name}`}
style={{transform: 'rotate(90deg) translate(-50%)'}}
onClick={toggleTabs}
className={`flex-grow-0 w-5 transition-colors duration-150 ease-in ${
hasChanged ? 'font-bold' : 'font-light'
} text-secondary hover:text-link`}>
{displayName}
</button>
</div>
)}
</div>
);
}

View File

@@ -1,308 +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 {parse as babelParse, ParseResult} from '@babel/parser';
import * as HermesParser from 'hermes-parser';
import * as t from '@babel/types';
import BabelPluginReactCompiler, {
CompilerError,
CompilerErrorDetail,
CompilerDiagnostic,
Effect,
ErrorCategory,
parseConfigPragmaForTests,
ValueKind,
type Hook,
PluginOptions,
CompilerPipelineValue,
parsePluginOptions,
printReactiveFunctionWithOutlined,
printFunctionWithOutlined,
type LoggerEvent,
} from 'babel-plugin-react-compiler';
import {transformFromAstSync} from '@babel/core';
import type {
CompilerOutput,
CompilerTransformOutput,
PrintedCompilerPipelineValue,
} from '../components/Editor/Output';
function parseInput(
input: string,
language: 'flow' | 'typescript',
): ParseResult<t.File> {
// Extract the first line to quickly check for custom test directives
if (language === 'flow') {
return HermesParser.parse(input, {
babel: true,
flow: 'all',
sourceType: 'module',
enableExperimentalComponentSyntax: true,
});
} else {
return babelParse(input, {
plugins: ['typescript', 'jsx'],
sourceType: 'module',
}) as ParseResult<t.File>;
}
}
function invokeCompiler(
source: string,
language: 'flow' | 'typescript',
options: PluginOptions,
): CompilerTransformOutput {
const ast = parseInput(source, language);
let result = transformFromAstSync(ast, source, {
filename: '_playgroundFile.js',
highlightCode: false,
retainLines: true,
plugins: [[BabelPluginReactCompiler, options]],
ast: true,
sourceType: 'module',
configFile: false,
sourceMaps: true,
babelrc: false,
});
if (result?.ast == null || result?.code == null || result?.map == null) {
throw new Error('Expected successful compilation');
}
return {
code: result.code,
sourceMaps: result.map,
language,
};
}
const COMMON_HOOKS: Array<[string, Hook]> = [
[
'useFragment',
{
valueKind: ValueKind.Frozen,
effectKind: Effect.Freeze,
noAlias: true,
transitiveMixedData: true,
},
],
[
'usePaginationFragment',
{
valueKind: ValueKind.Frozen,
effectKind: Effect.Freeze,
noAlias: true,
transitiveMixedData: true,
},
],
[
'useRefetchableFragment',
{
valueKind: ValueKind.Frozen,
effectKind: Effect.Freeze,
noAlias: true,
transitiveMixedData: true,
},
],
[
'useLazyLoadQuery',
{
valueKind: ValueKind.Frozen,
effectKind: Effect.Freeze,
noAlias: true,
transitiveMixedData: true,
},
],
[
'usePreloadedQuery',
{
valueKind: ValueKind.Frozen,
effectKind: Effect.Freeze,
noAlias: true,
transitiveMixedData: true,
},
],
];
function parseOptions(
source: string,
mode: 'compiler' | 'linter',
configOverrides: string,
): PluginOptions {
// Extract the first line to quickly check for custom test directives
const pragma = source.substring(0, source.indexOf('\n'));
const parsedPragmaOptions = 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 */
},
});
// Parse config overrides from config editor
let configOverrideOptions: any = {};
const configMatch = configOverrides.match(/^\s*import.*?\n\n\((.*)\)/s);
if (configOverrides.trim()) {
if (configMatch && configMatch[1]) {
const configString = configMatch[1].replace(/satisfies.*$/, '').trim();
configOverrideOptions = new Function(`return (${configString})`)();
} else {
throw new Error('Invalid override format');
}
}
const opts: PluginOptions = parsePluginOptions({
...parsedPragmaOptions,
...configOverrideOptions,
environment: {
...parsedPragmaOptions.environment,
...configOverrideOptions.environment,
customHooks: new Map([...COMMON_HOOKS]),
},
});
return opts;
}
export function compile(
source: string,
mode: 'compiler' | 'linter',
configOverrides: string,
): [CompilerOutput, 'flow' | 'typescript', PluginOptions | null] {
const results = new Map<string, Array<PrintedCompilerPipelineValue>>();
const error = new CompilerError();
const otherErrors: Array<CompilerErrorDetail | CompilerDiagnostic> = [];
const upsert: (result: PrintedCompilerPipelineValue) => void = result => {
const entry = results.get(result.name);
if (Array.isArray(entry)) {
entry.push(result);
} else {
results.set(result.name, [result]);
}
};
let language: 'flow' | 'typescript';
if (source.match(/\@flow/)) {
language = 'flow';
} else {
language = 'typescript';
}
let transformOutput;
let baseOpts: PluginOptions | null = null;
try {
baseOpts = parseOptions(source, mode, configOverrides);
} catch (err) {
error.details.push(
new CompilerErrorDetail({
category: ErrorCategory.Config,
reason: `Unexpected failure when transforming configs! \n${err}`,
loc: null,
suggestions: null,
}),
);
}
if (baseOpts) {
try {
const logIR = (result: CompilerPipelineValue): void => {
switch (result.kind) {
case 'ast': {
break;
}
case 'hir': {
upsert({
kind: 'hir',
fnName: result.value.id,
name: result.name,
value: printFunctionWithOutlined(result.value),
});
break;
}
case 'reactive': {
upsert({
kind: 'reactive',
fnName: result.value.id,
name: result.name,
value: printReactiveFunctionWithOutlined(result.value),
});
break;
}
case 'debug': {
upsert({
kind: 'debug',
fnName: null,
name: result.name,
value: result.value,
});
break;
}
default: {
const _: never = result;
throw new Error(`Unhandled result ${result}`);
}
}
};
// Add logger options to the parsed options
const opts = {
...baseOpts,
logger: {
debugLogIRs: logIR,
logEvent: (_filename: string | null, event: LoggerEvent): void => {
if (event.kind === 'CompileError') {
otherErrors.push(event.detail);
}
},
},
};
transformOutput = invokeCompiler(source, language, opts);
} catch (err) {
/**
* error might be an invariant violation or other runtime error
* (i.e. object shape that is not CompilerError)
*/
if (err instanceof CompilerError && err.details.length > 0) {
error.merge(err);
} else {
/**
* Handle unexpected failures by logging (to get a stack trace)
* and reporting
*/
error.details.push(
new CompilerErrorDetail({
category: ErrorCategory.Invariant,
reason: `Unexpected failure when transforming input! \n${err}`,
loc: null,
suggestions: null,
}),
);
}
}
}
// 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() || !transformOutput) {
return [{kind: 'err', results, error}, language, baseOpts];
}
return [
{kind: 'ok', results, transformOutput, errors: error.details},
language,
baseOpts,
];
}

View File

@@ -0,0 +1,120 @@
/**
* 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 {parsePluginOptions} from 'babel-plugin-react-compiler';
import {parseConfigPragmaAsString} from '../../../packages/babel-plugin-react-compiler/src/Utils/TestUtils';
export class ConfigError extends Error {
constructor(message: string) {
super(message);
this.name = 'ConfigError';
}
}
/**
* Parse config from pragma and format it with prettier
*/
export async function parseAndFormatConfig(source: string): Promise<string> {
const pragma = source.substring(0, source.indexOf('\n'));
let configString = parseConfigPragmaAsString(pragma);
if (configString !== '') {
configString = `\
import type { PluginOptions } from 'babel-plugin-react-compiler/dist';
(${configString} satisfies Partial<PluginOptions>)`;
}
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('({') + 1;
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();
}
/**
* Validate that a config string can be parsed as a valid PluginOptions object
* Throws an error if validation fails.
*/
function validateConfigAsPluginOptions(configString: string): void {
// Validate that config can be parse as JS obj
let parsedConfig: unknown;
try {
parsedConfig = new Function(`return (${configString})`)();
} catch (_) {
throw new ConfigError('Config has invalid syntax.');
}
// Validate against PluginOptions schema
try {
parsePluginOptions(parsedConfig);
} catch (_) {
throw new ConfigError('Config does not match the expected schema.');
}
}
/**
* Generate a the override pragma comment from a formatted config object string
*/
export async function generateOverridePragmaFromConfig(
formattedConfigString: string,
): Promise<string> {
const content = extractCurlyBracesContent(formattedConfigString);
const cleanConfig = cleanContent(content);
validateConfigAsPluginOptions(cleanConfig);
// 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;
}
}

View File

@@ -17,8 +17,23 @@ export const defaultConfig = `\
import type { PluginOptions } from 'babel-plugin-react-compiler/dist';
({
//compilationMode: "all"
} satisfies PluginOptions);`;
compilationMode: 'infer',
panicThreshold: 'none',
environment: {},
logger: null,
gating: null,
noEmit: false,
dynamicGating: null,
eslintSuppressionRules: null,
flowSuppressions: true,
ignoreUseNoForget: false,
sources: filename => {
return filename.indexOf('node_modules') === -1;
},
enableReanimatedCheck: true,
customOptOutDirectives: null,
target: '19',
} satisfies Partial<PluginOptions>);`;
export const defaultStore: Store = {
source: index,

View File

@@ -71,7 +71,7 @@ export function initStoreFromUrlOrLocalStorage(): Store {
// Make sure all properties are populated
return {
source: raw.source,
config: 'config' in raw && raw['config'] ? raw.config : defaultConfig,
config: 'config' in raw ? raw.config : defaultConfig,
showInternals: 'showInternals' in raw ? raw.showInternals : false,
};
}

View File

@@ -1,11 +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.
*/
export const CONFIG_PANEL_TRANSITION = 'config-panel';
export const TOGGLE_TAB_TRANSITION = 'toggle-tab';
export const TOGGLE_INTERNALS_TRANSITION = 'toggle-internals';
export const EXPAND_ACCORDION_TRANSITION = 'open-accordion';

View File

@@ -0,0 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
/// <reference path="./.next/types/routes.d.ts" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

View File

@@ -11,7 +11,6 @@ const path = require('path');
const nextConfig = {
experimental: {
reactCompiler: true,
viewTransition: true,
},
reactStrictMode: true,
webpack: (config, options) => {

View File

@@ -26,27 +26,26 @@
"@babel/traverse": "^7.18.9",
"@babel/types": "7.26.3",
"@heroicons/react": "^1.0.6",
"@monaco-editor/react": "^4.8.0-rc.2",
"@playwright/test": "^1.56.1",
"@monaco-editor/react": "^4.4.6",
"@playwright/test": "^1.51.1",
"@use-gesture/react": "^10.2.22",
"hermes-eslint": "^0.25.0",
"hermes-parser": "^0.25.0",
"invariant": "^2.2.4",
"lru-cache": "^11.2.2",
"lz-string": "^1.5.0",
"monaco-editor": "^0.52.0",
"next": "15.5.9",
"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.2.3",
"react-dom": "19.2.3"
"react": "19.1.1",
"react-dom": "19.1.1"
},
"devDependencies": {
"@types/node": "18.11.9",
"@types/react": "19.2",
"@types/react-dom": "19.2",
"@types/react": "19.1.12",
"@types/react-dom": "19.1.9",
"autoprefixer": "^10.4.13",
"clsx": "^1.2.1",
"concurrently": "^7.4.0",
@@ -58,7 +57,7 @@
"wait-on": "^7.2.0"
},
"resolutions": {
"@types/react": "19.2",
"@types/react-dom": "19.2"
"@types/react": "19.1.12",
"@types/react-dom": "19.1.9"
}
}

View File

@@ -55,16 +55,12 @@ export default defineConfig({
// contextOptions: {
// ignoreHTTPSErrors: true,
// },
viewport: {width: 1920, height: 1080},
},
projects: [
{
name: 'chromium',
use: {
...devices['Desktop Chrome'],
viewport: {width: 1920, height: 1080},
},
use: {...devices['Desktop Chrome']},
},
// {
// name: 'Desktop Firefox',

View File

@@ -69,75 +69,3 @@
scrollbar-width: none; /* Firefox */
}
}
::view-transition-old(.slide-in) {
animation-name: slideOutLeft;
}
::view-transition-new(.slide-in) {
animation-name: slideInLeft;
}
::view-transition-group(.slide-in) {
z-index: 1;
}
::view-transition-old(.slide-out) {
animation-name: slideOutLeft;
}
::view-transition-new(.slide-out) {
animation-name: slideInLeft;
}
::view-transition-group(.slide-out) {
z-index: 1;
}
@keyframes slideOutLeft {
from {
transform: translateX(0);
}
to {
transform: translateX(-100%);
}
}
@keyframes slideInLeft {
from {
transform: translateX(-100%);
}
to {
transform: translateX(0);
}
}
::view-transition-old(.container),
::view-transition-new(.container) {
height: 100%;
}
::view-transition-old(.accordion-container),
::view-transition-new(.accordion-container) {
height: 100%;
object-fit: none;
object-position: left;
}
::view-transition-old(.tab-highlight),
::view-transition-new(.tab-highlight) {
height: 100%;
}
::view-transition-group(.tab-text) {
z-index: 1;
}
::view-transition-old(.expand-accordion),
::view-transition-new(.expand-accordion) {
width: auto;
}
::view-transition-group(.expand-accordion) {
overflow: clip;
}
/**
* For some reason, the original Monaco editor is still visible to the
* left of the DiffEditor. This is a workaround for better visual clarity.
*/
.monaco-diff-editor .editor.original{
visibility: hidden !important;
}

View File

@@ -6,9 +6,6 @@
"dom.iterable",
"esnext"
],
"types": [
"react/experimental"
],
"allowJs": true,
"skipLibCheck": true,
"strict": true,

View File

@@ -701,24 +701,24 @@
"@jridgewell/resolve-uri" "^3.1.0"
"@jridgewell/sourcemap-codec" "^1.4.14"
"@monaco-editor/loader@^1.6.1":
version "1.6.1"
resolved "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.6.1.tgz#c99177d87765abf10de31a0086084e714acfbc0f"
integrity sha512-w3tEnj9HYEC73wtjdpR089AqkUPskFRcdkxsiSFt3SoUc3OHpmu+leP94CXBm4mHfefmhsdfI0ZQu6qJ0wgtPg==
"@monaco-editor/loader@^1.4.0":
version "1.4.0"
resolved "https://registry.yarnpkg.com/@monaco-editor/loader/-/loader-1.4.0.tgz#f08227057331ec890fa1e903912a5b711a2ad558"
integrity sha512-00ioBig0x642hytVspPl7DbQyaSWRaolYie/UFNjoTdvoKPzo6xrXLhTk9ixgIKcLH5b5vDOjVNiGyY+uDCUlg==
dependencies:
state-local "^1.0.6"
"@monaco-editor/react@^4.8.0-rc.2":
version "4.8.0-rc.2"
resolved "https://registry.npmjs.org/@monaco-editor/react/-/react-4.8.0-rc.2.tgz#e9acf652e23e9f640671a69875f496dde7f098aa"
integrity sha512-RzFHKBCnRA4RnozaG/EPhKsbkhX5wcApSa5MElR/AD2ojxhMY+QP+G8aJpxALCnIwKs6L0dec5MJ0nAjMUEqnA==
"@monaco-editor/react@^4.4.6":
version "4.6.0"
resolved "https://registry.yarnpkg.com/@monaco-editor/react/-/react-4.6.0.tgz#bcc68671e358a21c3814566b865a54b191e24119"
integrity sha512-RFkU9/i7cN2bsq/iTkurMWOEErmYcY6JiQI3Jn+WeR/FGISH8JbHERjpS9oRuSOPvDMJI0Z8nJeKkbOs9sBYQw==
dependencies:
"@monaco-editor/loader" "^1.6.1"
"@monaco-editor/loader" "^1.4.0"
"@next/env@15.5.9":
version "15.5.9"
resolved "https://registry.yarnpkg.com/@next/env/-/env-15.5.9.tgz#53c2c34dc17cd87b61f70c6cc211e303123b2ab8"
integrity sha512-4GlTZ+EJM7WaW2HEZcyU317tIQDjkQIyENDLxYJfSWlfqguN+dHkZgyQTV/7ykvobU7yEH5gKvreNrH4B6QgIg==
"@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.5.2":
version "15.5.2"
@@ -727,45 +727,45 @@
dependencies:
fast-glob "3.3.1"
"@next/swc-darwin-arm64@15.5.7":
version "15.5.7"
resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.7.tgz#f0c9ccfec2cd87cbd4b241ce4c779a7017aed958"
integrity sha512-IZwtxCEpI91HVU/rAUOOobWSZv4P2DeTtNaCdHqLcTJU4wdNXgAySvKa/qJCgR5m6KI8UsKDXtO2B31jcaw1Yw==
"@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.7":
version "15.5.7"
resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.7.tgz#18009e9fcffc5c0687cc9db24182ddeac56280d9"
integrity sha512-UP6CaDBcqaCBuiq/gfCEJw7sPEoX1aIjZHnBWN9v9qYHQdMKvCKcAVs4OX1vIjeE+tC5EIuwDTVIoXpUes29lg==
"@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.7":
version "15.5.7"
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.7.tgz#fe7c7e08264cf522d4e524299f6d3e63d68d579a"
integrity sha512-NCslw3GrNIw7OgmRBxHtdWFQYhexoUCq+0oS2ccjyYLtcn1SzGzeM54jpTFonIMUjNbHmpKpziXnpxhSWLcmBA==
"@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.7":
version "15.5.7"
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.7.tgz#94228fe293475ec34a5a54284e1056876f43a3cf"
integrity sha512-nfymt+SE5cvtTrG9u1wdoxBr9bVB7mtKTcj0ltRn6gkP/2Nu1zM5ei8rwP9qKQP0Y//umK+TtkKgNtfboBxRrw==
"@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.7":
version "15.5.7"
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.7.tgz#078c71201dfe7fcfb8fa6dc92aae6c94bc011cdc"
integrity sha512-hvXcZvCaaEbCZcVzcY7E1uXN9xWZfFvkNHwbe/n4OkRhFWrs1J1QV+4U1BN06tXLdaS4DazEGXwgqnu/VMcmqw==
"@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.7":
version "15.5.7"
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.7.tgz#72947f5357f9226292353e0bb775643da3c7a182"
integrity sha512-4IUO539b8FmF0odY6/SqANJdgwn1xs1GkPO5doZugwZ3ETF6JUdckk7RGmsfSf7ws8Qb2YB5It33mvNL/0acqA==
"@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.7":
version "15.5.7"
resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.7.tgz#397b912cd51c6a80e32b9c0507ecd82514353941"
integrity sha512-CpJVTkYI3ZajQkC5vajM7/ApKJUOlm6uP4BknM3XKvJ7VXAvCqSjSLmM0LKdYzn6nBJVSjdclx8nYJSa3xlTgQ==
"@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.7":
version "15.5.7"
resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.7.tgz#e02b543d9dc6c1631d4ac239cb1177245dfedfe4"
integrity sha512-gMzgBX164I6DN+9/PGA+9dQiwmTkE4TloBNx8Kv9UiGARsr9Nba7IpcBRA1iTV9vwlYnrE3Uy6I7Aj6qLjQuqw==
"@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"
@@ -798,12 +798,12 @@
resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33"
integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==
"@playwright/test@^1.56.1":
version "1.56.1"
resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.56.1.tgz#6e3bf3d0c90c5cf94bf64bdb56fd15a805c8bd3f"
integrity sha512-vSMYtL/zOcFpvJCW71Q/OEGQb7KYBPAdKh35WNSkaZA75JlAO8ED8UN6GUNTm3drWomcbcqRPFqQbLae8yBTdg==
"@playwright/test@^1.51.1":
version "1.51.1"
resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.51.1.tgz#75357d513221a7be0baad75f01e966baf9c41a2e"
integrity sha512-nM+kEaTSAoVlXmMPH10017vn3FSiFqr/bh4fKg9vmAdMfd9SDqRZNvPSiAHADc/itWak+qPvMPZQOPwCBW7k7Q==
dependencies:
playwright "1.56.1"
playwright "1.51.1"
"@rtsao/scc@^1.1.0":
version "1.1.0"
@@ -854,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.2":
version "19.2.2"
resolved "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.2.tgz#a4cc874797b7ddc9cb180ef0d5dc23f596fc2332"
integrity sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw==
"@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.2":
version "19.2.2"
resolved "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz#ba123a75d4c2a51158697160a4ea2ff70aa6bf36"
integrity sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==
"@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"
@@ -3097,11 +3097,6 @@ lru-cache@^10.2.0:
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.4.3.tgz#410fc8a17b70e598013df257c2446b7f3383f119"
integrity sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==
lru-cache@^11.2.2:
version "11.2.2"
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-11.2.2.tgz#40fd37edffcfae4b2940379c0722dc6eeaa75f24"
integrity sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==
lru-cache@^5.1.1:
version "5.1.1"
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920"
@@ -3204,25 +3199,25 @@ natural-compare@^1.4.0:
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==
next@15.5.9:
version "15.5.9"
resolved "https://registry.yarnpkg.com/next/-/next-15.5.9.tgz#1b80d05865cc27e710fb4dcfc6fd9e726ed12ad4"
integrity sha512-agNLK89seZEtC5zUHwtut0+tNrc0Xw4FT/Dg+B/VLEo9pAcS9rtTKpek3V6kVcVwsB2YlqMaHdfZL4eLEVYuCg==
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.5.9"
"@next/env" "15.5.2"
"@swc/helpers" "0.5.15"
caniuse-lite "^1.0.30001579"
postcss "8.4.31"
styled-jsx "5.1.6"
optionalDependencies:
"@next/swc-darwin-arm64" "15.5.7"
"@next/swc-darwin-x64" "15.5.7"
"@next/swc-linux-arm64-gnu" "15.5.7"
"@next/swc-linux-arm64-musl" "15.5.7"
"@next/swc-linux-x64-gnu" "15.5.7"
"@next/swc-linux-x64-musl" "15.5.7"
"@next/swc-win32-arm64-msvc" "15.5.7"
"@next/swc-win32-x64-msvc" "15.5.7"
"@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:
@@ -3453,17 +3448,17 @@ pirates@^4.0.1:
resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.6.tgz#3018ae32ecfcff6c29ba2267cbf21166ac1f36b9"
integrity sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==
playwright-core@1.56.1:
version "1.56.1"
resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.56.1.tgz#24a66481e5cd33a045632230aa2c4f0cb6b1db3d"
integrity sha512-hutraynyn31F+Bifme+Ps9Vq59hKuUCz7H1kDOcBs+2oGguKkWTU50bBWrtz34OUWmIwpBTWDxaRPXrIXkgvmQ==
playwright-core@1.51.1:
version "1.51.1"
resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.51.1.tgz#d57f0393e02416f32a47cf82b27533656a8acce1"
integrity sha512-/crRMj8+j/Nq5s8QcvegseuyeZPxpQCZb6HNk3Sos3BlZyAknRjoyJPFWkpNn8v0+P3WiwqFF8P+zQo4eqiNuw==
playwright@1.56.1:
version "1.56.1"
resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.56.1.tgz#62e3b99ddebed0d475e5936a152c88e68be55fbf"
integrity sha512-aFi5B0WovBHTEvpM3DzXTUaeN6eN0qWnTkKx4NQaH4Wvcmc153PdaY2UBdSYKaGYw+UyWXSVyxDUg5DoPEttjw==
playwright@1.51.1:
version "1.51.1"
resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.51.1.tgz#ae1467ee318083968ad28d6990db59f47a55390f"
integrity sha512-kkx+MB2KQRkyxjYPc3a0wLZZoDczmppyGJIvQ43l+aZihkaVvmu/21kiyaHeHjiFxjxNNFnUncKmcGIyOojsaw==
dependencies:
playwright-core "1.56.1"
playwright-core "1.51.1"
optionalDependencies:
fsevents "2.3.2"
@@ -3582,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.2.3:
version "19.2.3"
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-19.2.3.tgz#f0b61d7e5c4a86773889fcc1853af3ed5f215b17"
integrity sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==
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.27.0"
scheduler "^0.26.0"
react-is@^16.13.1:
version "16.13.1"
@@ -3599,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.2.3:
version "19.2.3"
resolved "https://registry.yarnpkg.com/react/-/react-19.2.3.tgz#d83e5e8e7a258cf6b4fe28640515f99b87cd19b8"
integrity sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==
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"
@@ -3778,10 +3773,10 @@ safe-regex-test@^1.1.0:
es-errors "^1.3.0"
is-regex "^1.2.1"
scheduler@^0.27.0:
version "0.27.0"
resolved "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz#0c4ef82d67d1e5c1e359e8fc76d3a87f045fe5bd"
integrity sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==
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"

View File

@@ -52,8 +52,8 @@
"react-dom": "0.0.0-experimental-4beb1fd8-20241118",
"ts-jest": "^29.1.1",
"ts-node": "^10.9.2",
"zod": "^3.25.0 || ^4.0.0",
"zod-validation-error": "^3.5.0 || ^4.0.0"
"zod": "^3.22.4",
"zod-validation-error": "^2.1.0"
},
"resolutions": {
"./**/@babel/parser": "7.7.4",

View File

@@ -17,7 +17,7 @@ export function runBabelPluginReactCompiler(
text: string,
file: string,
language: 'flow' | 'typescript',
options: PluginOptions | null,
options: Partial<PluginOptions> | null,
includeAst: boolean = false,
): BabelCore.BabelFileResult {
const ast = BabelParser.parse(text, {

View File

@@ -12,28 +12,6 @@ import {Err, Ok, Result} from './Utils/Result';
import {assertExhaustive} from './Utils/utils';
import invariant from 'invariant';
// Number of context lines to display above the source of an error
const CODEFRAME_LINES_ABOVE = 2;
// Number of context lines to display below the source of an error
const CODEFRAME_LINES_BELOW = 3;
/*
* Max number of lines for the _source_ of an error, before we abbreviate
* the display of the source portion
*/
const CODEFRAME_MAX_LINES = 10;
/*
* When the error source exceeds the above threshold, how many lines of
* the source should be displayed? We show:
* - CODEFRAME_LINES_ABOVE context lines
* - CODEFRAME_ABBREVIATED_SOURCE_LINES of the error
* - '...' ellipsis
* - CODEFRAME_ABBREVIATED_SOURCE_LINES of the error
* - CODEFRAME_LINES_BELOW context lines
*
* This value must be at least 2 or else we'll cut off important parts of the error message
*/
const CODEFRAME_ABBREVIATED_SOURCE_LINES = 5;
export enum ErrorSeverity {
/**
* An actionable error that the developer can fix. For example, product code errors should be
@@ -304,30 +282,6 @@ export class CompilerError extends Error {
disabledDetails: Array<CompilerErrorDetail | CompilerDiagnostic> = [];
printedMessage: string | null = null;
static simpleInvariant(
condition: unknown,
options: {
reason: CompilerDiagnosticOptions['reason'];
description?: CompilerDiagnosticOptions['description'];
loc: SourceLocation;
},
): asserts condition {
if (!condition) {
const errors = new CompilerError();
errors.pushDiagnostic(
CompilerDiagnostic.create({
reason: options.reason,
description: options.description ?? null,
category: ErrorCategory.Invariant,
}).withDetails({
kind: 'error',
loc: options.loc,
message: options.reason,
}),
);
throw errors;
}
}
static invariant(
condition: unknown,
options: Omit<CompilerDiagnosticOptions, 'category'>,
@@ -542,7 +496,7 @@ function printCodeFrame(
loc: t.SourceLocation,
message: string,
): string {
const printed = codeFrameColumns(
return codeFrameColumns(
source,
{
start: {
@@ -556,25 +510,8 @@ function printCodeFrame(
},
{
message,
linesAbove: CODEFRAME_LINES_ABOVE,
linesBelow: CODEFRAME_LINES_BELOW,
},
);
const lines = printed.split(/\r?\n/);
if (loc.end.line - loc.start.line < CODEFRAME_MAX_LINES) {
return printed;
}
const pipeIndex = lines[0].indexOf('|');
return [
...lines.slice(
0,
CODEFRAME_LINES_ABOVE + CODEFRAME_ABBREVIATED_SOURCE_LINES,
),
' '.repeat(pipeIndex) + '…',
...lines.slice(
-(CODEFRAME_LINES_BELOW + CODEFRAME_ABBREVIATED_SOURCE_LINES),
),
].join('\n');
}
function printErrorSummary(category: ErrorCategory, message: string): string {
@@ -599,10 +536,7 @@ function printErrorSummary(category: ErrorCategory, message: string): string {
case ErrorCategory.StaticComponents:
case ErrorCategory.Suppression:
case ErrorCategory.Syntax:
case ErrorCategory.UseMemo:
case ErrorCategory.VoidUseMemo:
case ErrorCategory.MemoDependencies:
case ErrorCategory.EffectExhaustiveDependencies: {
case ErrorCategory.UseMemo: {
heading = 'Error';
break;
}
@@ -648,10 +582,6 @@ export enum ErrorCategory {
* Checking for valid usage of manual memoization
*/
UseMemo = 'UseMemo',
/**
* Checking that useMemos always return a value
*/
VoidUseMemo = 'VoidUseMemo',
/**
* Checking for higher order functions acting as factories for components/hooks
*/
@@ -660,10 +590,6 @@ export enum ErrorCategory {
* Checks that manual memoization is preserved
*/
PreserveManualMemo = 'PreserveManualMemo',
/**
* Checks for exhaustive useMemo/useCallback dependencies without extraneous values
*/
MemoDependencies = 'MemoDependencies',
/**
* Checks for known incompatible libraries
*/
@@ -684,10 +610,6 @@ export enum ErrorCategory {
* Checks for memoized effect deps
*/
EffectDependencies = 'EffectDependencies',
/**
* Checks for exhaustive and extraneous effect dependencies
*/
EffectExhaustiveDependencies = 'EffectExhaustiveDependencies',
/**
* Checks for no setState in effect bodies
*/
@@ -747,21 +669,6 @@ export enum ErrorCategory {
FBT = 'FBT',
}
export enum LintRulePreset {
/**
* Rules that are stable and included in the `recommended` preset.
*/
Recommended = 'recommended',
/**
* Rules that are more experimental and only included in the `recommended-latest` preset.
*/
RecommendedLatest = 'recommended-latest',
/**
* Rules that are disabled.
*/
Off = 'off',
}
export type LintRule = {
// Stores the category the rule corresponds to, used to filter errors when reporting
category: ErrorCategory;
@@ -782,14 +689,15 @@ export type LintRule = {
description: string;
/**
* Configures the preset in which the rule is enabled. If 'off', the rule will not be included in
* any preset.
* 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!
*/
preset: LintRulePreset;
recommended: boolean;
};
const RULE_NAME_PATTERN = /^[a-z]+(-[a-z]+)*$/;
@@ -812,7 +720,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
name: 'automatic-effect-dependencies',
description:
'Verifies that automatic effect dependencies are compiled if opted-in',
preset: LintRulePreset.Off,
recommended: false,
};
}
case ErrorCategory.CapitalizedCalls: {
@@ -822,7 +730,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
name: 'capitalized-calls',
description:
'Validates against calling capitalized functions/methods instead of using JSX',
preset: LintRulePreset.Off,
recommended: false,
};
}
case ErrorCategory.Config: {
@@ -831,7 +739,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
severity: ErrorSeverity.Error,
name: 'config',
description: 'Validates the compiler configuration options',
preset: LintRulePreset.Recommended,
recommended: true,
};
}
case ErrorCategory.EffectDependencies: {
@@ -840,17 +748,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
severity: ErrorSeverity.Error,
name: 'memoized-effect-dependencies',
description: 'Validates that effect dependencies are memoized',
preset: LintRulePreset.Off,
};
}
case ErrorCategory.EffectExhaustiveDependencies: {
return {
category,
severity: ErrorSeverity.Error,
name: 'exhaustive-effect-dependencies',
description:
'Validates that effect dependencies are exhaustive and without extraneous values',
preset: LintRulePreset.Off,
recommended: false,
};
}
case ErrorCategory.EffectDerivationsOfState: {
@@ -860,7 +758,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
name: 'no-deriving-state-in-effects',
description:
'Validates against deriving values from state in an effect',
preset: LintRulePreset.Off,
recommended: false,
};
}
case ErrorCategory.EffectSetState: {
@@ -869,10 +767,8 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
severity: ErrorSeverity.Error,
name: 'set-state-in-effect',
description:
'Validates against calling setState synchronously in an effect. ' +
'This can indicate non-local derived data, a derived event pattern, or ' +
'improper external data synchronization.',
preset: LintRulePreset.Recommended,
'Validates against calling setState synchronously in an effect, which can lead to re-renders that degrade performance',
recommended: true,
};
}
case ErrorCategory.ErrorBoundaries: {
@@ -882,7 +778,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
name: 'error-boundaries',
description:
'Validates usage of error boundaries instead of try/catch for errors in child components',
preset: LintRulePreset.Recommended,
recommended: true,
};
}
case ErrorCategory.Factories: {
@@ -893,7 +789,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
description:
'Validates against higher order functions defining nested components or hooks. ' +
'Components and hooks should be defined at the module level',
preset: LintRulePreset.Recommended,
recommended: true,
};
}
case ErrorCategory.FBT: {
@@ -902,7 +798,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
severity: ErrorSeverity.Error,
name: 'fbt',
description: 'Validates usage of fbt',
preset: LintRulePreset.Off,
recommended: false,
};
}
case ErrorCategory.Fire: {
@@ -911,7 +807,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
severity: ErrorSeverity.Error,
name: 'fire',
description: 'Validates usage of `fire`',
preset: LintRulePreset.Off,
recommended: false,
};
}
case ErrorCategory.Gating: {
@@ -921,7 +817,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
name: 'gating',
description:
'Validates configuration of [gating mode](https://react.dev/reference/react-compiler/gating)',
preset: LintRulePreset.Recommended,
recommended: true,
};
}
case ErrorCategory.Globals: {
@@ -932,7 +828,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
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)',
preset: LintRulePreset.Recommended,
recommended: true,
};
}
case ErrorCategory.Hooks: {
@@ -946,7 +842,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
* We need to dedeupe these (moving the remaining bits into the compiler) and then enable
* this rule.
*/
preset: LintRulePreset.Off,
recommended: false,
};
}
case ErrorCategory.Immutability: {
@@ -956,7 +852,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
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)',
preset: LintRulePreset.Recommended,
recommended: true,
};
}
case ErrorCategory.Invariant: {
@@ -965,7 +861,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
severity: ErrorSeverity.Error,
name: 'invariant',
description: 'Internal invariants',
preset: LintRulePreset.Off,
recommended: false,
};
}
case ErrorCategory.PreserveManualMemo: {
@@ -977,7 +873,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
'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)',
preset: LintRulePreset.Recommended,
recommended: true,
};
}
case ErrorCategory.Purity: {
@@ -987,7 +883,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
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',
preset: LintRulePreset.Recommended,
recommended: true,
};
}
case ErrorCategory.Refs: {
@@ -997,7 +893,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
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)',
preset: LintRulePreset.Recommended,
recommended: true,
};
}
case ErrorCategory.RenderSetState: {
@@ -1007,7 +903,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
name: 'set-state-in-render',
description:
'Validates against setting state during render, which can trigger additional renders and potential infinite render loops',
preset: LintRulePreset.Recommended,
recommended: true,
};
}
case ErrorCategory.StaticComponents: {
@@ -1017,7 +913,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
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',
preset: LintRulePreset.Recommended,
recommended: true,
};
}
case ErrorCategory.Suppression: {
@@ -1026,7 +922,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
severity: ErrorSeverity.Error,
name: 'rule-suppression',
description: 'Validates against suppression of other rules',
preset: LintRulePreset.Off,
recommended: false,
};
}
case ErrorCategory.Syntax: {
@@ -1035,7 +931,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
severity: ErrorSeverity.Error,
name: 'syntax',
description: 'Validates against invalid syntax',
preset: LintRulePreset.Off,
recommended: false,
};
}
case ErrorCategory.Todo: {
@@ -1044,7 +940,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
severity: ErrorSeverity.Hint,
name: 'todo',
description: 'Unimplemented features',
preset: LintRulePreset.Off,
recommended: false,
};
}
case ErrorCategory.UnsupportedSyntax: {
@@ -1054,7 +950,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
name: 'unsupported-syntax',
description:
'Validates against syntax that we do not plan to support in React Compiler',
preset: LintRulePreset.Recommended,
recommended: true,
};
}
case ErrorCategory.UseMemo: {
@@ -1064,35 +960,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
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.',
preset: LintRulePreset.Recommended,
};
}
case ErrorCategory.VoidUseMemo: {
return {
category,
severity: ErrorSeverity.Error,
name: 'void-use-memo',
description:
'Validates that useMemos always return a value and that the result of the useMemo is used by the component/hook. See [`useMemo()` docs](https://react.dev/reference/react/useMemo) for more information.',
preset: LintRulePreset.RecommendedLatest,
};
}
case ErrorCategory.MemoDependencies: {
return {
category,
severity: ErrorSeverity.Error,
name: 'memo-dependencies',
description:
'Validates that useMemo() and useCallback() specify comprehensive dependencies without extraneous values. See [`useMemo()` docs](https://react.dev/reference/react/useMemo) for more information.',
/**
* TODO: the "MemoDependencies" rule largely reimplements the "exhaustive-deps" non-compiler rule,
* allowing the compiler to ensure it does not regress change behavior due to different dependencies.
* We previously relied on the source having ESLint suppressions for any exhaustive-deps violations,
* but it's more reliable to verify it within the compiler.
*
* Long-term we should de-duplicate these implementations.
*/
preset: LintRulePreset.Off,
recommended: true,
};
}
case ErrorCategory.IncompatibleLibrary: {
@@ -1102,7 +970,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
name: 'incompatible-library',
description:
'Validates against usage of libraries which are incompatible with memoization (manual or automatic)',
preset: LintRulePreset.Recommended,
recommended: true,
};
}
default: {

View File

@@ -18,7 +18,7 @@ import {
import {getOrInsertWith} from '../Utils/utils';
import {ExternalFunction, isHookName} from '../HIR/Environment';
import {Err, Ok, Result} from '../Utils/Result';
import {LoggerEvent, ParsedPluginOptions} from './Options';
import {LoggerEvent, PluginOptions} from './Options';
import {BabelFn, getReactCompilerRuntimeModule} from './Program';
import {SuppressionRange} from './Suppression';
@@ -56,7 +56,7 @@ export function validateRestrictedImports(
type ProgramContextOptions = {
program: NodePath<t.Program>;
suppressions: Array<SuppressionRange>;
opts: ParsedPluginOptions;
opts: PluginOptions;
filename: string | null;
code: string | null;
hasModuleScopeOptOut: boolean;
@@ -66,7 +66,7 @@ export class ProgramContext {
* Program and environment context
*/
scope: BabelScope;
opts: ParsedPluginOptions;
opts: PluginOptions;
filename: string | null;
code: string | null;
reactRuntimeModule: string;
@@ -240,7 +240,7 @@ export function addImportsToProgram(
programContext: ProgramContext,
): void {
const existingImports = getExistingImports(path);
const stmts: Array<t.ImportDeclaration | t.VariableDeclaration> = [];
const stmts: Array<t.ImportDeclaration> = [];
const sortedModules = [...programContext.imports.entries()].sort(([a], [b]) =>
a.localeCompare(b),
);
@@ -303,29 +303,9 @@ export function addImportsToProgram(
if (maybeExistingImports != null) {
maybeExistingImports.pushContainer('specifiers', importSpecifiers);
} else {
if (path.node.sourceType === 'module') {
stmts.push(
t.importDeclaration(importSpecifiers, t.stringLiteral(moduleName)),
);
} else {
stmts.push(
t.variableDeclaration('const', [
t.variableDeclarator(
t.objectPattern(
sortedImport.map(specifier => {
return t.objectProperty(
t.identifier(specifier.imported),
t.identifier(specifier.name),
);
}),
),
t.callExpression(t.identifier('require'), [
t.stringLiteral(moduleName),
]),
),
]),
);
}
stmts.push(
t.importDeclaration(importSpecifiers, t.stringLiteral(moduleName)),
);
}
}
path.unshiftContainer('body', stmts);

View File

@@ -6,7 +6,7 @@
*/
import * as t from '@babel/types';
import {z} from 'zod/v4';
import {z} from 'zod';
import {
CompilerDiagnostic,
CompilerError,
@@ -20,7 +20,7 @@ import {
tryParseExternalFunction,
} from '../HIR/Environment';
import {hasOwnProperty} from '../Utils/utils';
import {fromZodError} from 'zod-validation-error/v4';
import {fromZodError} from 'zod-validation-error';
import {CompilerPipelineValue} from './Pipeline';
const PanicThresholdOptionsSchema = z.enum([
@@ -51,8 +51,8 @@ const CustomOptOutDirectiveSchema = z
.default(null);
type CustomOptOutDirective = z.infer<typeof CustomOptOutDirectiveSchema>;
export type PluginOptions = Partial<{
environment: Partial<EnvironmentConfig>;
export type PluginOptions = {
environment: EnvironmentConfig;
logger: Logger | null;
@@ -102,25 +102,14 @@ export type PluginOptions = Partial<{
panicThreshold: PanicThresholdOptions;
/**
* @deprecated
*
/*
* When enabled, Forget will continue statically analyzing and linting code, but skip over codegen
* passes.
*
* NOTE: ignored if `outputMode` is specified
*
* Defaults to false
*/
noEmit: boolean;
/**
* If specified, overrides `noEmit` and controls the output mode of the compiler.
*
* Defaults to null
*/
outputMode: CompilerOutputMode | null;
/*
* Determines the strategy for determining which functions to compile. Note that regardless of
* which mode is enabled, a component can be opted out by adding the string literal
@@ -177,11 +166,7 @@ export type PluginOptions = Partial<{
* a userspace approximation of runtime APIs.
*/
target: CompilerReactTarget;
}>;
export type ParsedPluginOptions = Required<
Omit<PluginOptions, 'environment'>
> & {environment: EnvironmentConfig};
};
const CompilerReactTargetSchema = z.union([
z.literal('17'),
@@ -223,19 +208,6 @@ const CompilationModeSchema = z.enum([
export type CompilationMode = z.infer<typeof CompilationModeSchema>;
const CompilerOutputModeSchema = z.enum([
// Build optimized for SSR, with client features removed
'ssr',
// Build optimized for the client, with auto memoization
'client',
// Build optimized for the client without auto memo
'client-no-memo',
// Lint mode, the output is unused but validations should run
'lint',
]);
export type CompilerOutputMode = z.infer<typeof CompilerOutputModeSchema>;
/**
* Represents 'events' that may occur during compilation. Events are only
* recorded when a logger is set (through the config).
@@ -310,14 +282,13 @@ export type Logger = {
debugLogIRs?: (value: CompilerPipelineValue) => void;
};
export const defaultOptions: ParsedPluginOptions = {
export const defaultOptions: PluginOptions = {
compilationMode: 'infer',
panicThreshold: 'none',
environment: parseEnvironmentConfig({}).unwrap(),
logger: null,
gating: null,
noEmit: false,
outputMode: null,
dynamicGating: null,
eslintSuppressionRules: null,
flowSuppressions: true,
@@ -328,9 +299,9 @@ export const defaultOptions: ParsedPluginOptions = {
enableReanimatedCheck: true,
customOptOutDirectives: null,
target: '19',
};
} as const;
export function parsePluginOptions(obj: unknown): ParsedPluginOptions {
export function parsePluginOptions(obj: unknown): PluginOptions {
if (obj == null || typeof obj !== 'object') {
return defaultOptions;
}

View File

@@ -8,7 +8,7 @@
import {NodePath} from '@babel/traverse';
import * as t from '@babel/types';
import prettyFormat from 'pretty-format';
import {CompilerOutputMode, Logger, ProgramContext} from '.';
import {Logger, ProgramContext} from '.';
import {
HIRFunction,
ReactiveFunction,
@@ -24,6 +24,7 @@ import {
pruneUnusedLabelsHIR,
} from '../HIR';
import {
CompilerMode,
Environment,
EnvironmentConfig,
ReactFunctionType,
@@ -102,11 +103,7 @@ import {validateNoFreezingKnownMutableFunctions} from '../Validation/ValidateNoF
import {inferMutationAliasingEffects} from '../Inference/InferMutationAliasingEffects';
import {inferMutationAliasingRanges} from '../Inference/InferMutationAliasingRanges';
import {validateNoDerivedComputationsInEffects} from '../Validation/ValidateNoDerivedComputationsInEffects';
import {validateNoDerivedComputationsInEffects_exp} from '../Validation/ValidateNoDerivedComputationsInEffects_exp';
import {nameAnonymousFunctions} from '../Transform/NameAnonymousFunctions';
import {optimizeForSSR} from '../Optimization/OptimizeForSSR';
import {validateExhaustiveDependencies} from '../Validation/ValidateExhaustiveDependencies';
import {validateSourceLocations} from '../Validation/ValidateSourceLocations';
export type CompilerPipelineValue =
| {kind: 'ast'; name: string; value: CodegenFunction}
@@ -120,7 +117,7 @@ function run(
>,
config: EnvironmentConfig,
fnType: ReactFunctionType,
mode: CompilerOutputMode,
mode: CompilerMode,
programContext: ProgramContext,
logger: Logger | null,
filename: string | null,
@@ -170,7 +167,7 @@ function runWithEnvironment(
validateUseMemo(hir).unwrap();
if (
env.enableDropManualMemoization &&
env.isInferredMemoEnabled &&
!env.config.enablePreserveExistingManualUseMemo &&
!env.config.disableMemoizationForDebugging &&
!env.config.enableChangeDetectionForDebugging
@@ -206,7 +203,7 @@ function runWithEnvironment(
inferTypes(hir);
log({kind: 'hir', name: 'InferTypes', value: hir});
if (env.enableValidations) {
if (env.isInferredMemoEnabled) {
if (env.config.validateHooksUsage) {
validateHooksUsage(hir).unwrap();
}
@@ -232,17 +229,12 @@ function runWithEnvironment(
const mutabilityAliasingErrors = inferMutationAliasingEffects(hir);
log({kind: 'hir', name: 'InferMutationAliasingEffects', value: hir});
if (env.enableValidations) {
if (env.isInferredMemoEnabled) {
if (mutabilityAliasingErrors.isErr()) {
throw mutabilityAliasingErrors.unwrapErr();
}
}
if (env.outputMode === 'ssr') {
optimizeForSSR(hir);
log({kind: 'hir', name: 'OptimizeForSSR', value: hir});
}
// Note: Has to come after infer reference effects because "dead" code may still affect inference
deadCodeElimination(hir);
log({kind: 'hir', name: 'DeadCodeElimination', value: hir});
@@ -259,14 +251,14 @@ function runWithEnvironment(
isFunctionExpression: false,
});
log({kind: 'hir', name: 'InferMutationAliasingRanges', value: hir});
if (env.enableValidations) {
if (env.isInferredMemoEnabled) {
if (mutabilityAliasingRangeErrors.isErr()) {
throw mutabilityAliasingRangeErrors.unwrapErr();
}
validateLocalsNotReassignedAfterRender(hir);
}
if (env.enableValidations) {
if (env.isInferredMemoEnabled) {
if (env.config.assertValidMutableRanges) {
assertValidMutableRanges(hir);
}
@@ -279,20 +271,15 @@ function runWithEnvironment(
validateNoSetStateInRender(hir).unwrap();
}
if (
env.config.validateNoDerivedComputationsInEffects_exp &&
env.outputMode === 'lint'
) {
env.logErrors(validateNoDerivedComputationsInEffects_exp(hir));
} else if (env.config.validateNoDerivedComputationsInEffects) {
if (env.config.validateNoDerivedComputationsInEffects) {
validateNoDerivedComputationsInEffects(hir);
}
if (env.config.validateNoSetStateInEffects && env.outputMode === 'lint') {
env.logErrors(validateNoSetStateInEffects(hir, env));
if (env.config.validateNoSetStateInEffects) {
env.logErrors(validateNoSetStateInEffects(hir));
}
if (env.config.validateNoJSXInTryStatements && env.outputMode === 'lint') {
if (env.config.validateNoJSXInTryStatements) {
env.logErrors(validateNoJSXInTryStatement(hir));
}
@@ -306,16 +293,6 @@ function runWithEnvironment(
inferReactivePlaces(hir);
log({kind: 'hir', name: 'InferReactivePlaces', value: hir});
if (env.enableValidations) {
if (
env.config.validateExhaustiveMemoizationDependencies ||
env.config.validateExhaustiveEffectDependencies
) {
// NOTE: this relies on reactivity inference running first
validateExhaustiveDependencies(hir).unwrap();
}
}
rewriteInstructionKindsBasedOnReassignment(hir);
log({
kind: 'hir',
@@ -323,15 +300,11 @@ function runWithEnvironment(
value: hir,
});
if (
env.enableValidations &&
env.config.validateStaticComponents &&
env.outputMode === 'lint'
) {
env.logErrors(validateStaticComponents(hir));
}
if (env.isInferredMemoEnabled) {
if (env.config.validateStaticComponents) {
env.logErrors(validateStaticComponents(hir));
}
if (env.enableMemoization) {
/**
* Only create reactive scopes (which directly map to generated memo blocks)
* if inferred memoization is enabled. This makes all later passes which
@@ -581,10 +554,6 @@ function runWithEnvironment(
log({kind: 'ast', name: 'Codegen (outlined)', value: outlined.fn});
}
if (env.config.validateSourceLocations) {
validateSourceLocations(func, ast).unwrap();
}
/**
* This flag should be only set for unit / fixture tests to check
* that Forget correctly handles unexpected errors (e.g. exceptions
@@ -603,7 +572,7 @@ export function compileFn(
>,
config: EnvironmentConfig,
fnType: ReactFunctionType,
mode: CompilerOutputMode,
mode: CompilerMode,
programContext: ProgramContext,
logger: Logger | null,
filename: string | null,

View File

@@ -23,12 +23,7 @@ import {
ProgramContext,
validateRestrictedImports,
} from './Imports';
import {
CompilerOutputMode,
CompilerReactTarget,
ParsedPluginOptions,
PluginOptions,
} from './Options';
import {CompilerReactTarget, PluginOptions} from './Options';
import {compileFn} from './Pipeline';
import {
filterSuppressionsThatAffectFunction,
@@ -39,7 +34,7 @@ import {GeneratedSource} from '../HIR';
import {Err, Ok, Result} from '../Utils/Result';
export type CompilerPass = {
opts: ParsedPluginOptions;
opts: PluginOptions;
filename: string | null;
comments: Array<t.CommentBlock | t.CommentLine>;
code: string | null;
@@ -50,7 +45,7 @@ const DYNAMIC_GATING_DIRECTIVE = new RegExp('^use memo if\\(([^\\)]*)\\)$');
export function tryFindDirectiveEnablingMemoization(
directives: Array<t.Directive>,
opts: ParsedPluginOptions,
opts: PluginOptions,
): Result<t.Directive | null, CompilerError> {
const optIn = directives.find(directive =>
OPT_IN_DIRECTIVES.has(directive.value.value),
@@ -86,7 +81,7 @@ export function findDirectiveDisablingMemoization(
}
function findDirectivesDynamicGating(
directives: Array<t.Directive>,
opts: ParsedPluginOptions,
opts: PluginOptions,
): Result<
{
gating: ExternalFunction;
@@ -400,15 +395,7 @@ export function compileProgram(
*/
const suppressions = findProgramSuppressions(
pass.comments,
/*
* If the compiler is validating hooks rules and exhaustive memo dependencies, we don't need to check
* for React ESLint suppressions
*/
pass.opts.environment.validateExhaustiveMemoizationDependencies &&
pass.opts.environment.validateHooksUsage
? null
: (pass.opts.eslintSuppressionRules ?? DEFAULT_ESLINT_SUPPRESSIONS),
// Always bail on Flow suppressions
pass.opts.eslintSuppressionRules ?? DEFAULT_ESLINT_SUPPRESSIONS,
pass.opts.flowSuppressions,
);
@@ -430,17 +417,9 @@ export function compileProgram(
);
const compiledFns: Array<CompileResult> = [];
// outputMode takes precedence if specified
const outputMode: CompilerOutputMode =
pass.opts.outputMode ?? (pass.opts.noEmit ? 'lint' : 'client');
while (queue.length !== 0) {
const current = queue.shift()!;
const compiled = processFn(
current.fn,
current.fnType,
programContext,
outputMode,
);
const compiled = processFn(current.fn, current.fnType, programContext);
if (compiled != null) {
for (const outlined of compiled.outlined) {
@@ -598,7 +577,6 @@ function processFn(
fn: BabelFn,
fnType: ReactFunctionType,
programContext: ProgramContext,
outputMode: CompilerOutputMode,
): null | CodegenFunction {
let directives: {
optIn: t.Directive | null;
@@ -634,27 +612,18 @@ function processFn(
}
let compiledFn: CodegenFunction;
const compileResult = tryCompileFunction(
fn,
fnType,
programContext,
outputMode,
);
const compileResult = tryCompileFunction(fn, fnType, programContext);
if (compileResult.kind === 'error') {
if (directives.optOut != null) {
logError(compileResult.error, programContext, fn.node.loc ?? null);
} else {
handleError(compileResult.error, programContext, fn.node.loc ?? null);
}
if (outputMode === 'client') {
const retryResult = retryCompileFunction(fn, fnType, programContext);
if (retryResult == null) {
return null;
}
compiledFn = retryResult;
} else {
const retryResult = retryCompileFunction(fn, fnType, programContext);
if (retryResult == null) {
return null;
}
compiledFn = retryResult;
} else {
compiledFn = compileResult.compiledFn;
}
@@ -690,7 +659,7 @@ function processFn(
if (programContext.hasModuleScopeOptOut) {
return null;
} else if (programContext.opts.outputMode === 'lint') {
} else if (programContext.opts.noEmit) {
/**
* inferEffectDependencies + noEmit is currently only used for linting. In
* this mode, add source locations for where the compiler *can* infer effect
@@ -720,7 +689,6 @@ function tryCompileFunction(
fn: BabelFn,
fnType: ReactFunctionType,
programContext: ProgramContext,
outputMode: CompilerOutputMode,
):
| {kind: 'compile'; compiledFn: CodegenFunction}
| {kind: 'error'; error: unknown} {
@@ -747,7 +715,7 @@ function tryCompileFunction(
fn,
programContext.opts.environment,
fnType,
outputMode,
'all_features',
programContext,
programContext.opts.logger,
programContext.filename,
@@ -785,7 +753,7 @@ function retryCompileFunction(
fn,
environment,
fnType,
'client-no-memo',
'no_inferred_memo',
programContext,
programContext.opts.logger,
programContext.filename,

View File

@@ -7,7 +7,7 @@
import type * as BabelCore from '@babel/core';
import {hasOwnProperty} from '../Utils/utils';
import {ParsedPluginOptions} from './Options';
import {PluginOptions} from './Options';
function hasModule(name: string): boolean {
if (typeof require === 'undefined') {
@@ -52,9 +52,7 @@ export function pipelineUsesReanimatedPlugin(
return hasModule('react-native-reanimated');
}
export function injectReanimatedFlag(
options: ParsedPluginOptions,
): ParsedPluginOptions {
export function injectReanimatedFlag(options: PluginOptions): PluginOptions {
return {
...options,
environment: {

View File

@@ -78,7 +78,7 @@ export function filterSuppressionsThatAffectFunction(
export function findProgramSuppressions(
programComments: Array<t.Comment>,
ruleNames: Array<string> | null,
ruleNames: Array<string>,
flowSuppressions: boolean,
): Array<SuppressionRange> {
const suppressionRanges: Array<SuppressionRange> = [];
@@ -89,7 +89,7 @@ export function findProgramSuppressions(
let disableNextLinePattern: RegExp | null = null;
let disablePattern: RegExp | null = null;
let enablePattern: RegExp | null = null;
if (ruleNames != null && ruleNames.length !== 0) {
if (ruleNames.length !== 0) {
const rulePattern = `(${ruleNames.join('|')})`;
disableNextLinePattern = new RegExp(
`eslint-disable-next-line ${rulePattern}`,

View File

@@ -1568,6 +1568,20 @@ function lowerObjectPropertyKey(
name: key.node.value,
};
} else if (property.node.computed && key.isExpression()) {
if (!key.isIdentifier() && !key.isMemberExpression()) {
/*
* NOTE: allowing complex key expressions can trigger a bug where a mutation is made conditional
* see fixture
* error.object-expression-computed-key-modified-during-after-construction.js
*/
builder.errors.push({
reason: `(BuildHIR::lowerExpression) Expected Identifier, got ${key.type} key in ObjectExpression`,
category: ErrorCategory.Todo,
loc: key.node.loc ?? null,
suggestions: null,
});
return null;
}
const place = lowerExpressionToTemporary(builder, key);
return {
kind: 'computed',
@@ -3067,12 +3081,6 @@ function isReorderableExpression(
return true;
}
}
case 'TSInstantiationExpression': {
const innerExpr = (expr as NodePath<t.TSInstantiationExpression>).get(
'expression',
) as NodePath<t.Expression>;
return isReorderableExpression(builder, innerExpr, allowLocalIdentifiers);
}
case 'RegExpLiteral':
case 'StringLiteral':
case 'NumericLiteral':
@@ -4026,7 +4034,6 @@ function lowerAssignment(
pattern: {
kind: 'ArrayPattern',
items,
loc: lvalue.node.loc ?? GeneratedSource,
},
},
value,
@@ -4204,7 +4211,6 @@ function lowerAssignment(
pattern: {
kind: 'ObjectPattern',
properties,
loc: lvalue.node.loc ?? GeneratedSource,
},
},
value,

View File

@@ -454,32 +454,6 @@ function collectNonNullsInBlocks(
assumedNonNullObjects.add(entry);
}
}
} else if (
fn.env.config.enablePreserveExistingMemoizationGuarantees &&
instr.value.kind === 'StartMemoize' &&
instr.value.deps != null
) {
for (const dep of instr.value.deps) {
if (dep.root.kind === 'NamedLocal') {
if (
!isImmutableAtInstr(dep.root.value.identifier, instr.id, context)
) {
continue;
}
for (let i = 0; i < dep.path.length; i++) {
const pathEntry = dep.path[i]!;
if (pathEntry.optional) {
break;
}
const depNode = context.registry.getOrCreateProperty({
identifier: dep.root.value.identifier,
path: dep.path.slice(0, i),
reactive: dep.root.value.reactive,
});
assumedNonNullObjects.add(depNode);
}
}
}
}
}

View File

@@ -86,24 +86,6 @@ export function defaultModuleTypeProvider(
},
};
}
case '@tanstack/react-virtual': {
return {
kind: 'object',
properties: {
/*
* Many of the properties of `useVirtualizer()`'s return value are incompatible, so we mark the entire hook
* as incompatible
*/
useVirtualizer: {
kind: 'hook',
positionalParams: [],
restParam: Effect.Read,
returnType: {kind: 'type', name: 'Any'},
knownIncompatible: `TanStack Virtual's \`useVirtualizer()\` API returns functions that cannot be memoized safely`,
},
},
};
}
}
return null;
}

View File

@@ -6,10 +6,10 @@
*/
import * as t from '@babel/types';
import {ZodError, z} from 'zod/v4';
import {fromZodError} from 'zod-validation-error/v4';
import {ZodError, z} from 'zod';
import {fromZodError} from 'zod-validation-error';
import {CompilerError} from '../CompilerError';
import {CompilerOutputMode, Logger, ProgramContext} from '../Entrypoint';
import {Logger, ProgramContext} from '../Entrypoint';
import {Err, Ok, Result} from '../Utils/Result';
import {
DEFAULT_GLOBALS,
@@ -51,7 +51,6 @@ import {Scope as BabelScope, NodePath} from '@babel/traverse';
import {TypeSchema} from './TypeSchema';
import {FlowTypeEnv} from '../Flood/Types';
import {defaultModuleTypeProvider} from './DefaultModuleTypeProvider';
import {assertExhaustive} from '../Utils/utils';
export const ReactElementSymbolSchema = z.object({
elementSymbol: z.union([
@@ -84,11 +83,21 @@ export type ExternalFunction = z.infer<typeof ExternalFunctionSchema>;
export const USE_FIRE_FUNCTION_NAME = 'useFire';
export const EMIT_FREEZE_GLOBAL_GATING = '__DEV__';
export const MacroSchema = z.string();
export const MacroMethodSchema = z.union([
z.object({type: z.literal('wildcard')}),
z.object({type: z.literal('name'), name: z.string()}),
]);
// Would like to change this to drop the string option, but breaks compatibility with existing configs
export const MacroSchema = z.union([
z.string(),
z.tuple([z.string(), z.array(MacroMethodSchema)]),
]);
export type CompilerMode = 'all_features' | 'no_inferred_memo';
export type Macro = z.infer<typeof MacroSchema>;
export type MacroMethod = z.infer<typeof MacroMethodSchema>;
const HookSchema = z.object({
/*
@@ -150,7 +159,7 @@ export const EnvironmentConfigSchema = z.object({
* A function that, given the name of a module, can optionally return a description
* of that module's type signature.
*/
moduleTypeProvider: z.nullable(z.any()).default(null),
moduleTypeProvider: z.nullable(z.function().args(z.string())).default(null),
/**
* A list of functions which the application compiles as macros, where
@@ -201,7 +210,7 @@ export const EnvironmentConfigSchema = z.object({
* that if a useEffect or useCallback references a function value, that function value will be
* considered frozen, and in turn all of its referenced variables will be considered frozen as well.
*/
enablePreserveExistingMemoizationGuarantees: z.boolean().default(true),
enablePreserveExistingMemoizationGuarantees: z.boolean().default(false),
/**
* Validates that all useMemo/useCallback values are also memoized by Forget. This mode can be
@@ -218,23 +227,6 @@ export const EnvironmentConfigSchema = z.object({
*/
validatePreserveExistingMemoizationGuarantees: z.boolean().default(true),
/**
* Validate that dependencies supplied to manual memoization calls are exhaustive.
*/
validateExhaustiveMemoizationDependencies: z.boolean().default(true),
/**
* Validate that dependencies supplied to effect hooks are exhaustive.
* Can be:
* - 'off': No validation (default)
* - 'all': Validate and report both missing and extra dependencies
* - 'missing-only': Only report missing dependencies
* - 'extra-only': Only report extra/unnecessary dependencies
*/
validateExhaustiveEffectDependencies: z
.enum(['off', 'all', 'missing-only', 'extra-only'])
.default('off'),
/**
* When this is true, rather than pruning existing manual memoization but ensuring or validating
* that the memoized values remain memoized, the compiler will simply not prune existing calls to
@@ -257,7 +249,7 @@ export const EnvironmentConfigSchema = z.object({
* Allows specifying a function that can populate HIR with type information from
* Flow
*/
flowTypeProvider: z.nullable(z.any()).default(null),
flowTypeProvider: z.nullable(z.function().args(z.string())).default(null),
/**
* Enables inference of optional dependency chains. Without this flag
@@ -330,12 +322,6 @@ export const EnvironmentConfigSchema = z.object({
*/
validateNoSetStateInRender: z.boolean().default(true),
/**
* When enabled, changes the behavior of validateNoSetStateInRender to recommend
* using useKeyedState instead of the manual pattern for resetting state.
*/
enableUseKeyedState: z.boolean().default(false),
/**
* Validates that setState is not called synchronously within an effect (useEffect and friends).
* Scheduling a setState (with an event listener, subscription, etc) is valid.
@@ -348,12 +334,6 @@ export const EnvironmentConfigSchema = z.object({
*/
validateNoDerivedComputationsInEffects: z.boolean().default(false),
/**
* Experimental: Validates that effects are not used to calculate derived data which could instead be computed
* during render. Generates a custom error message for each type of violation.
*/
validateNoDerivedComputationsInEffects_exp: z.boolean().default(false),
/**
* Validates against creating JSX within a try block and recommends using an error boundary
* instead.
@@ -388,13 +368,6 @@ export const EnvironmentConfigSchema = z.object({
validateNoCapitalizedCalls: z.nullable(z.array(z.string())).default(null),
validateBlocklistedImports: z.nullable(z.array(z.string())).default(null),
/**
* Validates that AST nodes generated during codegen have proper source locations.
* This is useful for debugging issues with source maps and Istanbul coverage.
* When enabled, the compiler will error if important source locations are missing in the generated AST.
*/
validateSourceLocations: z.boolean().default(false),
/**
* Validate against impure functions called during render
*/
@@ -648,13 +621,6 @@ export const EnvironmentConfigSchema = z.object({
*/
enableTreatRefLikeIdentifiersAsRefs: z.boolean().default(true),
/**
* Treat identifiers as SetState type if both
* - they are named with a "set-" prefix
* - they are called somewhere
*/
enableTreatSetIdentifiersAsStateSetters: z.boolean().default(false),
/*
* If specified a value, the compiler lowers any calls to `useContext` to use
* this value as the callee.
@@ -686,7 +652,7 @@ export const EnvironmentConfigSchema = z.object({
* Invalid:
* useMemo(() => { ... }, [...]);
*/
validateNoVoidUseMemo: z.boolean().default(true),
validateNoVoidUseMemo: z.boolean().default(false),
/**
* Validates that Components/Hooks are always defined at module level. This prevents scope
@@ -694,37 +660,6 @@ export const EnvironmentConfigSchema = z.object({
* while its parent function remains uncompiled.
*/
validateNoDynamicallyCreatedComponentsOrHooks: z.boolean().default(false),
/**
* When enabled, allows setState calls in effects based on valid patterns involving refs:
* - Allow setState where the value being set is derived from a ref. This is useful where
* state needs to take into account layer information, and a layout effect reads layout
* data from a ref and sets state.
* - Allow conditionally calling setState after manually comparing previous/new values
* for changes via a ref. Relying on effect deps is insufficient for non-primitive values,
* so a ref is generally required to manually track previous values and compare prev/next
* for meaningful changes before setting state.
*/
enableAllowSetStateFromRefsInEffects: z.boolean().default(true),
/**
* When enabled, provides verbose error messages for setState calls within effects,
* presenting multiple possible fixes to the user/agent since we cannot statically
* determine which specific use-case applies:
* 1. Non-local derived data - requires restructuring state ownership
* 2. Derived event pattern - detecting when a prop changes
* 3. Force update / external sync - should use useSyncExternalStore
*/
enableVerboseNoSetStateInEffect: z.boolean().default(false),
/**
* Enables inference of event handler types for JSX props on built-in DOM elements.
* When enabled, functions passed to event handler props (props starting with "on")
* on primitive JSX tags are inferred to have the BuiltinEventHandlerId type, which
* allows ref access within those functions since DOM event handlers are guaranteed
* by React to only execute in response to events, not during render.
*/
enableInferEventHandlers: z.boolean().default(false),
});
export type EnvironmentConfig = z.infer<typeof EnvironmentConfigSchema>;
@@ -764,7 +699,7 @@ export class Environment {
code: string | null;
config: EnvironmentConfig;
fnType: ReactFunctionType;
outputMode: CompilerOutputMode;
compilerMode: CompilerMode;
programContext: ProgramContext;
hasFireRewrite: boolean;
hasInferredEffect: boolean;
@@ -779,7 +714,7 @@ export class Environment {
constructor(
scope: BabelScope,
fnType: ReactFunctionType,
outputMode: CompilerOutputMode,
compilerMode: CompilerMode,
config: EnvironmentConfig,
contextIdentifiers: Set<t.Identifier>,
parentFunction: NodePath<t.Function>, // the outermost function being compiled
@@ -790,7 +725,7 @@ export class Environment {
) {
this.#scope = scope;
this.fnType = fnType;
this.outputMode = outputMode;
this.compilerMode = compilerMode;
this.config = config;
this.filename = filename;
this.code = code;
@@ -886,65 +821,8 @@ export class Environment {
return this.#flowTypeEnvironment;
}
get enableDropManualMemoization(): boolean {
switch (this.outputMode) {
case 'lint': {
// linting drops to be more compatible with compiler analysis
return true;
}
case 'client':
case 'ssr': {
return true;
}
case 'client-no-memo': {
return false;
}
default: {
assertExhaustive(
this.outputMode,
`Unexpected output mode '${this.outputMode}'`,
);
}
}
}
get enableMemoization(): boolean {
switch (this.outputMode) {
case 'client':
case 'lint': {
// linting also enables memoization so that we can check if manual memoization is preserved
return true;
}
case 'ssr':
case 'client-no-memo': {
return false;
}
default: {
assertExhaustive(
this.outputMode,
`Unexpected output mode '${this.outputMode}'`,
);
}
}
}
get enableValidations(): boolean {
switch (this.outputMode) {
case 'client':
case 'lint':
case 'ssr': {
return true;
}
case 'client-no-memo': {
return false;
}
default: {
assertExhaustive(
this.outputMode,
`Unexpected output mode '${this.outputMode}'`,
);
}
}
get isInferredMemoEnabled(): boolean {
return this.compilerMode !== 'no_inferred_memo';
}
get nextIdentifierId(): IdentifierId {
@@ -1014,12 +892,6 @@ export class Environment {
if (moduleTypeProvider == null) {
return null;
}
if (typeof moduleTypeProvider !== 'function') {
CompilerError.throwInvalidConfig({
reason: `Expected a function for \`moduleTypeProvider\``,
loc,
});
}
const unparsedModuleConfig = moduleTypeProvider(moduleName);
if (unparsedModuleConfig != null) {
const parsedModuleConfig = TypeSchema.safeParse(unparsedModuleConfig);

View File

@@ -23,14 +23,13 @@ import {
BuiltInUseInsertionEffectHookId,
BuiltInUseLayoutEffectHookId,
BuiltInUseOperatorId,
BuiltInUseOptimisticId,
BuiltInUseReducerId,
BuiltInUseRefId,
BuiltInUseStateId,
BuiltInUseTransitionId,
BuiltInWeakMapId,
BuiltInWeakSetId,
BuiltInEffectEventId,
BuiltinEffectEventId,
ReanimatedSharedValueId,
ShapeRegistry,
addFunction,
@@ -819,18 +818,6 @@ const REACT_APIS: Array<[string, BuiltInType]> = [
returnValueKind: ValueKind.Frozen,
}),
],
[
'useOptimistic',
addHook(DEFAULT_SHAPES, {
positionalParams: [],
restParam: Effect.Freeze,
returnType: {kind: 'Object', shapeId: BuiltInUseOptimisticId},
calleeEffect: Effect.Read,
hookKind: 'useOptimistic',
returnValueKind: ValueKind.Frozen,
returnValueReason: ValueReason.State,
}),
],
[
'use',
addFunction(
@@ -876,7 +863,7 @@ const REACT_APIS: Array<[string, BuiltInType]> = [
returnType: {
kind: 'Function',
return: {kind: 'Poly'},
shapeId: BuiltInEffectEventId,
shapeId: BuiltinEffectEventId,
isConstructor: false,
},
calleeEffect: Effect.Read,

View File

@@ -16,7 +16,7 @@ import {assertExhaustive} from '../Utils/utils';
import {Environment, ReactFunctionType} from './Environment';
import type {HookKind} from './ObjectShape';
import {Type, makeType} from './Types';
import {z} from 'zod/v4';
import {z} from 'zod';
import type {AliasingEffect} from '../Inference/AliasingEffects';
import {isReservedWord} from '../Utils/Keyword';
import {Err, Ok, Result} from '../Utils/Result';
@@ -694,13 +694,11 @@ export type SpreadPattern = {
export type ArrayPattern = {
kind: 'ArrayPattern';
items: Array<Place | SpreadPattern | Hole>;
loc: SourceLocation;
};
export type ObjectPattern = {
kind: 'ObjectPattern';
properties: Array<ObjectProperty | SpreadPattern>;
loc: SourceLocation;
};
export type ObjectPropertyKey =
@@ -805,11 +803,9 @@ export type ManualMemoDependency = {
| {
kind: 'NamedLocal';
value: Place;
constant: boolean;
}
| {kind: 'Global'; identifierName: string};
path: DependencyPath;
loc: SourceLocation;
};
export type StartMemoize = {
@@ -821,11 +817,6 @@ export type StartMemoize = {
* (e.g. useMemo without a second arg)
*/
deps: Array<ManualMemoDependency> | null;
/**
* The source location of the dependencies argument. Used for
* emitting diagnostics with a suggested replacement
*/
depsLoc: SourceLocation | null;
loc: SourceLocation;
};
export type FinishMemoize = {
@@ -1689,28 +1680,6 @@ export function areEqualPaths(a: DependencyPath, b: DependencyPath): boolean {
)
);
}
export function isSubPath(
subpath: DependencyPath,
path: DependencyPath,
): boolean {
return (
subpath.length <= path.length &&
subpath.every(
(item, ix) =>
item.property === path[ix].property &&
item.optional === path[ix].optional,
)
);
}
export function isSubPathIgnoringOptionals(
subpath: DependencyPath,
path: DependencyPath,
): boolean {
return (
subpath.length <= path.length &&
subpath.every((item, ix) => item.property === path[ix].property)
);
}
export function getPlaceScope(
id: InstructionId,
@@ -1854,10 +1823,6 @@ export function isPrimitiveType(id: Identifier): boolean {
return id.type.kind === 'Primitive';
}
export function isPlainObjectType(id: Identifier): boolean {
return id.type.kind === 'Object' && id.type.shapeId === 'BuiltInObject';
}
export function isArrayType(id: Identifier): boolean {
return id.type.kind === 'Object' && id.type.shapeId === 'BuiltInArray';
}
@@ -1922,18 +1887,6 @@ export function isStartTransitionType(id: Identifier): boolean {
);
}
export function isUseOptimisticType(id: Identifier): boolean {
return (
id.type.kind === 'Object' && id.type.shapeId === 'BuiltInUseOptimistic'
);
}
export function isSetOptimisticType(id: Identifier): boolean {
return (
id.type.kind === 'Function' && id.type.shapeId === 'BuiltInSetOptimistic'
);
}
export function isSetActionStateType(id: Identifier): boolean {
return (
id.type.kind === 'Function' && id.type.shapeId === 'BuiltInSetActionState'
@@ -1967,8 +1920,7 @@ export function isStableType(id: Identifier): boolean {
isSetActionStateType(id) ||
isDispatcherType(id) ||
isUseRefType(id) ||
isStartTransitionType(id) ||
isSetOptimisticType(id)
isStartTransitionType(id)
);
}
@@ -1979,9 +1931,8 @@ export function isStableTypeContainer(id: Identifier): boolean {
}
return (
isUseStateType(id) || // setState
isUseActionStateType(id) || // setActionState
type_.shapeId === 'BuiltInUseActionState' || // setActionState
isUseReducerType(id) || // dispatcher
isUseOptimisticType(id) || // setOptimistic
type_.shapeId === 'BuiltInUseTransition' // startTransition
);
}
@@ -2001,7 +1952,6 @@ export function evaluatesToStableTypeOrContainer(
case 'useActionState':
case 'useRef':
case 'useTransition':
case 'useOptimistic':
return true;
}
}
@@ -2025,11 +1975,6 @@ export function isUseInsertionEffectHookType(id: Identifier): boolean {
id.type.shapeId === 'BuiltInUseInsertionEffectHook'
);
}
export function isUseEffectEventType(id: Identifier): boolean {
return (
id.type.kind === 'Function' && id.type.shapeId === 'BuiltInUseEffectEvent'
);
}
export function isUseContextHookType(id: Identifier): boolean {
return (

View File

@@ -304,7 +304,6 @@ export type HookKind =
| 'useTransition'
| 'useImperativeHandle'
| 'useEffectEvent'
| 'useOptimistic'
| 'Custom';
/*
@@ -400,15 +399,12 @@ export const BuiltInUseReducerId = 'BuiltInUseReducer';
export const BuiltInDispatchId = 'BuiltInDispatch';
export const BuiltInUseContextHookId = 'BuiltInUseContextHook';
export const BuiltInUseTransitionId = 'BuiltInUseTransition';
export const BuiltInUseOptimisticId = 'BuiltInUseOptimistic';
export const BuiltInSetOptimisticId = 'BuiltInSetOptimistic';
export const BuiltInStartTransitionId = 'BuiltInStartTransition';
export const BuiltInFireId = 'BuiltInFire';
export const BuiltInFireFunctionId = 'BuiltInFireFunction';
export const BuiltInUseEffectEventId = 'BuiltInUseEffectEvent';
export const BuiltInEffectEventId = 'BuiltInEffectEventFunction';
export const BuiltinEffectEventId = 'BuiltInEffectEventFunction';
export const BuiltInAutodepsId = 'BuiltInAutoDepsId';
export const BuiltInEventHandlerId = 'BuiltInEventHandlerId';
// See getReanimatedModuleType() in Globals.ts — this is part of supporting Reanimated's ref-like types
export const ReanimatedSharedValueId = 'ReanimatedSharedValueId';
@@ -1189,25 +1185,6 @@ addObject(BUILTIN_SHAPES, BuiltInUseTransitionId, [
],
]);
addObject(BUILTIN_SHAPES, BuiltInUseOptimisticId, [
['0', {kind: 'Poly'}],
[
'1',
addFunction(
BUILTIN_SHAPES,
[],
{
positionalParams: [],
restParam: Effect.Freeze,
returnType: PRIMITIVE_TYPE,
calleeEffect: Effect.Read,
returnValueKind: ValueKind.Primitive,
},
BuiltInSetOptimisticId,
),
],
]);
addObject(BUILTIN_SHAPES, BuiltInUseActionStateId, [
['0', {kind: 'Poly'}],
[
@@ -1266,20 +1243,7 @@ addFunction(
calleeEffect: Effect.ConditionallyMutate,
returnValueKind: ValueKind.Mutable,
},
BuiltInEffectEventId,
);
addFunction(
BUILTIN_SHAPES,
[],
{
positionalParams: [],
restParam: Effect.ConditionallyMutate,
returnType: {kind: 'Poly'},
calleeEffect: Effect.ConditionallyMutate,
returnValueKind: ValueKind.Mutable,
},
BuiltInEventHandlerId,
BuiltinEffectEventId,
);
/**

View File

@@ -6,7 +6,7 @@
*/
import {isValidIdentifier} from '@babel/types';
import {z} from 'zod/v4';
import {z} from 'zod';
import {Effect, ValueKind} from '..';
import {
EffectSchema,

View File

@@ -11,7 +11,6 @@ import {
BasicBlock,
BlockId,
Instruction,
InstructionKind,
InstructionValue,
makeInstructionId,
Pattern,
@@ -33,32 +32,6 @@ export function* eachInstructionLValue(
yield* eachInstructionValueLValue(instr.value);
}
export function* eachInstructionLValueWithKind(
instr: ReactiveInstruction,
): Iterable<[Place, InstructionKind]> {
switch (instr.value.kind) {
case 'DeclareContext':
case 'StoreContext':
case 'DeclareLocal':
case 'StoreLocal': {
yield [instr.value.lvalue.place, instr.value.lvalue.kind];
break;
}
case 'Destructure': {
const kind = instr.value.lvalue.kind;
for (const place of eachPatternOperand(instr.value.lvalue.pattern)) {
yield [place, kind];
}
break;
}
case 'PostfixUpdate':
case 'PrefixUpdate': {
yield [instr.value.lvalue, InstructionKind.Reassign];
break;
}
}
}
export function* eachInstructionValueLValue(
value: ReactiveValue,
): Iterable<Place> {

View File

@@ -1,114 +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, computePostDominatorTree, HIRFunction, Place} from '../HIR';
import {PostDominator} from '../HIR/Dominator';
export type ControlDominators = (id: BlockId) => boolean;
/**
* Returns an object that lazily calculates whether particular blocks are controlled
* by values of interest. Which values matter are up to the caller.
*/
export function createControlDominators(
fn: HIRFunction,
isControlVariable: (place: Place) => boolean,
): ControlDominators {
const postDominators = computePostDominatorTree(fn, {
includeThrowsAsExitNode: false,
});
const postDominatorFrontierCache = new Map<BlockId, Set<BlockId>>();
function isControlledBlock(id: BlockId): boolean {
let controlBlocks = postDominatorFrontierCache.get(id);
if (controlBlocks === undefined) {
controlBlocks = postDominatorFrontier(fn, postDominators, id);
postDominatorFrontierCache.set(id, controlBlocks);
}
for (const blockId of controlBlocks) {
const controlBlock = fn.body.blocks.get(blockId)!;
switch (controlBlock.terminal.kind) {
case 'if':
case 'branch': {
if (isControlVariable(controlBlock.terminal.test)) {
return true;
}
break;
}
case 'switch': {
if (isControlVariable(controlBlock.terminal.test)) {
return true;
}
for (const case_ of controlBlock.terminal.cases) {
if (case_.test !== null && isControlVariable(case_.test)) {
return true;
}
}
break;
}
}
}
return false;
}
return isControlledBlock;
}
/*
* Computes the post-dominator frontier of @param block. These are immediate successors of nodes that
* post-dominate @param targetId and from which execution may not reach @param block. Intuitively, these
* are the earliest blocks from which execution branches such that it may or may not reach the target block.
*/
function postDominatorFrontier(
fn: HIRFunction,
postDominators: PostDominator<BlockId>,
targetId: BlockId,
): Set<BlockId> {
const visited = new Set<BlockId>();
const frontier = new Set<BlockId>();
const targetPostDominators = postDominatorsOf(fn, postDominators, targetId);
for (const blockId of [...targetPostDominators, targetId]) {
if (visited.has(blockId)) {
continue;
}
visited.add(blockId);
const block = fn.body.blocks.get(blockId)!;
for (const pred of block.preds) {
if (!targetPostDominators.has(pred)) {
// The predecessor does not always reach this block, we found an item on the frontier!
frontier.add(pred);
}
}
}
return frontier;
}
function postDominatorsOf(
fn: HIRFunction,
postDominators: PostDominator<BlockId>,
targetId: BlockId,
): Set<BlockId> {
const result = new Set<BlockId>();
const visited = new Set<BlockId>();
const queue = [targetId];
while (queue.length) {
const currentId = queue.shift()!;
if (visited.has(currentId)) {
continue;
}
visited.add(currentId);
const current = fn.body.blocks.get(currentId)!;
for (const pred of current.preds) {
const predPostDominator = postDominators.get(pred) ?? pred;
if (predPostDominator === targetId || result.has(predPostDominator)) {
result.add(pred);
}
queue.push(pred);
}
}
return result;
}

View File

@@ -42,7 +42,7 @@ type IdentifierSidemap = {
functions: Map<IdentifierId, TInstruction<FunctionExpression>>;
manualMemos: Map<IdentifierId, ManualMemoCallee>;
react: Set<IdentifierId>;
maybeDepsLists: Map<IdentifierId, {loc: SourceLocation; deps: Array<Place>}>;
maybeDepsLists: Map<IdentifierId, Array<Place>>;
maybeDeps: Map<IdentifierId, ManualMemoDependency>;
optionals: Set<IdentifierId>;
};
@@ -65,7 +65,6 @@ export function collectMaybeMemoDependencies(
identifierName: value.binding.name,
},
path: [],
loc: value.loc,
};
}
case 'PropertyLoad': {
@@ -75,7 +74,6 @@ export function collectMaybeMemoDependencies(
root: object.root,
// TODO: determine if the access is optional
path: [...object.path, {property: value.property, optional}],
loc: value.loc,
};
}
break;
@@ -94,10 +92,8 @@ export function collectMaybeMemoDependencies(
root: {
kind: 'NamedLocal',
value: {...value.place},
constant: false,
},
path: [],
loc: value.place.loc,
};
}
break;
@@ -163,10 +159,10 @@ function collectTemporaries(
}
case 'ArrayExpression': {
if (value.elements.every(e => e.kind === 'Identifier')) {
sidemap.maybeDepsLists.set(instr.lvalue.identifier.id, {
loc: value.loc,
deps: value.elements as Array<Place>,
});
sidemap.maybeDepsLists.set(
instr.lvalue.identifier.id,
value.elements as Array<Place>,
);
}
break;
}
@@ -186,7 +182,6 @@ function makeManualMemoizationMarkers(
fnExpr: Place,
env: Environment,
depsList: Array<ManualMemoDependency> | null,
depsLoc: SourceLocation | null,
memoDecl: Place,
manualMemoId: number,
): [TInstruction<StartMemoize>, TInstruction<FinishMemoize>] {
@@ -202,7 +197,6 @@ function makeManualMemoizationMarkers(
* as dependencies
*/
deps: depsList,
depsLoc,
loc: fnExpr.loc,
},
effects: null,
@@ -293,85 +287,86 @@ function extractManualMemoizationArgs(
sidemap: IdentifierSidemap,
errors: CompilerError,
): {
fnPlace: Place;
fnPlace: Place | null;
depsList: Array<ManualMemoDependency> | null;
depsLoc: SourceLocation | null;
} | null {
} {
const [fnPlace, depsListPlace] = instr.value.args as Array<
Place | SpreadPattern | undefined
>;
if (fnPlace == null || fnPlace.kind !== 'Identifier') {
if (fnPlace == null) {
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.UseMemo,
reason: `Expected a callback function to be passed to ${kind}`,
description:
kind === 'useCallback'
? 'The first argument to useCallback() must be a function to cache'
: 'The first argument to useMemo() must be a function that calculates a result to cache',
description: `Expected a callback function to be passed to ${kind}`,
suggestions: null,
}).withDetails({
kind: 'error',
loc: instr.value.loc,
message:
kind === 'useCallback'
? `Expected a callback function`
: `Expected a memoization function`,
message: `Expected a callback function to be passed to ${kind}`,
}),
);
return null;
return {fnPlace: null, depsList: null};
}
if (depsListPlace == null) {
return {
fnPlace,
depsList: null,
depsLoc: null,
};
}
const maybeDepsList =
depsListPlace.kind === 'Identifier'
? sidemap.maybeDepsLists.get(depsListPlace.identifier.id)
: null;
if (maybeDepsList == null) {
if (fnPlace.kind === 'Spread' || depsListPlace?.kind === 'Spread') {
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.UseMemo,
reason: `Expected the dependency list for ${kind} to be an array literal`,
description: `Expected the dependency list for ${kind} to be an array literal`,
reason: `Unexpected spread argument to ${kind}`,
description: `Unexpected spread argument to ${kind}`,
suggestions: null,
}).withDetails({
kind: 'error',
loc:
depsListPlace?.kind === 'Identifier' ? depsListPlace.loc : instr.loc,
message: `Expected the dependency list for ${kind} to be an array literal`,
loc: instr.value.loc,
message: `Unexpected spread argument to ${kind}`,
}),
);
return null;
return {fnPlace: null, depsList: null};
}
const depsList: Array<ManualMemoDependency> = [];
for (const dep of maybeDepsList.deps) {
const maybeDep = sidemap.maybeDeps.get(dep.identifier.id);
if (maybeDep == null) {
let depsList: Array<ManualMemoDependency> | null = null;
if (depsListPlace != null) {
const maybeDepsList = sidemap.maybeDepsLists.get(
depsListPlace.identifier.id,
);
if (maybeDepsList == null) {
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.UseMemo,
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\`)`,
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,
}).withDetails({
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\`)`,
loc: depsListPlace.loc,
message: `Expected the dependency list for ${kind} to be an array literal`,
}),
);
} else {
depsList.push(maybeDep);
return {fnPlace, depsList: null};
}
depsList = [];
for (const dep of maybeDepsList) {
const maybeDep = sidemap.maybeDeps.get(dep.identifier.id);
if (maybeDep == null) {
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.UseMemo,
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,
}).withDetails({
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 {
fnPlace,
depsList,
depsLoc: maybeDepsList.loc,
};
}
@@ -432,17 +427,50 @@ export function dropManualMemoization(
const manualMemo = sidemap.manualMemos.get(id);
if (manualMemo != null) {
const memoDetails = extractManualMemoizationArgs(
const {fnPlace, depsList} = extractManualMemoizationArgs(
instr as TInstruction<CallExpression> | TInstruction<MethodCall>,
manualMemo.kind,
sidemap,
errors,
);
if (memoDetails == null) {
if (fnPlace == null) {
continue;
}
const {fnPlace, depsList, depsLoc} = memoDetails;
/**
* 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({
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,
}).withDetails({
kind: 'error',
loc: instr.value.loc,
message: 'useMemo() callbacks must return a value',
}),
);
}
}
}
instr.value = getManualMemoizationReplacement(
fnPlace,
@@ -493,7 +521,6 @@ export function dropManualMemoization(
fnPlace,
func.env,
depsList,
depsLoc,
memoDecl,
nextManualMemoId++,
);
@@ -602,3 +629,17 @@ function findOptionalPlaces(fn: HIRFunction): Set<IdentifierId> {
}
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;
}

View File

@@ -19,7 +19,6 @@ import {
Environment,
FunctionExpression,
GeneratedSource,
getHookKind,
HIRFunction,
Hole,
IdentifierId,
@@ -199,7 +198,6 @@ export function inferMutationAliasingEffects(
isFunctionExpression,
fn,
hoistedContextDeclarations,
findNonMutatedDestructureSpreads(fn),
);
let iterationCount = 0;
@@ -289,18 +287,15 @@ class Context {
isFuctionExpression: boolean;
fn: HIRFunction;
hoistedContextDeclarations: Map<DeclarationId, Place | null>;
nonMutatingSpreads: Set<IdentifierId>;
constructor(
isFunctionExpression: boolean,
fn: HIRFunction,
hoistedContextDeclarations: Map<DeclarationId, Place | null>,
nonMutatingSpreads: Set<IdentifierId>,
) {
this.isFuctionExpression = isFunctionExpression;
this.fn = fn;
this.hoistedContextDeclarations = hoistedContextDeclarations;
this.nonMutatingSpreads = nonMutatingSpreads;
}
cacheApplySignature(
@@ -327,161 +322,6 @@ class Context {
}
}
/**
* Finds objects created via ObjectPattern spread destructuring
* (`const {x, ...spread} = ...`) where a) the rvalue is known frozen and
* b) the spread value cannot possibly be directly mutated. The idea is that
* for this set of values, we can treat the spread object as frozen.
*
* The primary use case for this is props spreading:
*
* ```
* function Component({prop, ...otherProps}) {
* const transformedProp = transform(prop, otherProps.foo);
* // pass `otherProps` down:
* return <Foo {...otherProps} prop={transformedProp} />;
* }
* ```
*
* Here we know that since `otherProps` cannot be mutated, we don't have to treat
* it as mutable: `otherProps.foo` only reads a value that must be frozen, so it
* can be treated as frozen too.
*/
function findNonMutatedDestructureSpreads(fn: HIRFunction): Set<IdentifierId> {
const knownFrozen = new Set<IdentifierId>();
if (fn.fnType === 'Component') {
const [props] = fn.params;
if (props != null && props.kind === 'Identifier') {
knownFrozen.add(props.identifier.id);
}
} else {
for (const param of fn.params) {
if (param.kind === 'Identifier') {
knownFrozen.add(param.identifier.id);
}
}
}
// Map of temporaries to identifiers for spread objects
const candidateNonMutatingSpreads = new Map<IdentifierId, IdentifierId>();
for (const block of fn.body.blocks.values()) {
if (candidateNonMutatingSpreads.size !== 0) {
for (const phi of block.phis) {
for (const operand of phi.operands.values()) {
const spread = candidateNonMutatingSpreads.get(operand.identifier.id);
if (spread != null) {
candidateNonMutatingSpreads.delete(spread);
}
}
}
}
for (const instr of block.instructions) {
const {lvalue, value} = instr;
switch (value.kind) {
case 'Destructure': {
if (
!knownFrozen.has(value.value.identifier.id) ||
!(
value.lvalue.kind === InstructionKind.Let ||
value.lvalue.kind === InstructionKind.Const
) ||
value.lvalue.pattern.kind !== 'ObjectPattern'
) {
continue;
}
for (const item of value.lvalue.pattern.properties) {
if (item.kind !== 'Spread') {
continue;
}
candidateNonMutatingSpreads.set(
item.place.identifier.id,
item.place.identifier.id,
);
}
break;
}
case 'LoadLocal': {
const spread = candidateNonMutatingSpreads.get(
value.place.identifier.id,
);
if (spread != null) {
candidateNonMutatingSpreads.set(lvalue.identifier.id, spread);
}
break;
}
case 'StoreLocal': {
const spread = candidateNonMutatingSpreads.get(
value.value.identifier.id,
);
if (spread != null) {
candidateNonMutatingSpreads.set(lvalue.identifier.id, spread);
candidateNonMutatingSpreads.set(
value.lvalue.place.identifier.id,
spread,
);
}
break;
}
case 'JsxFragment':
case 'JsxExpression': {
// Passing objects created with spread to jsx can't mutate them
break;
}
case 'PropertyLoad': {
// Properties must be frozen since the original value was frozen
break;
}
case 'CallExpression':
case 'MethodCall': {
const callee =
value.kind === 'CallExpression' ? value.callee : value.property;
if (getHookKind(fn.env, callee.identifier) != null) {
// Hook calls have frozen arguments, and non-ref returns are frozen
if (!isRefOrRefValue(lvalue.identifier)) {
knownFrozen.add(lvalue.identifier.id);
}
} else {
// Non-hook calls check their operands, since they are potentially mutable
if (candidateNonMutatingSpreads.size !== 0) {
// Otherwise any reference to the spread object itself may mutate
for (const operand of eachInstructionValueOperand(value)) {
const spread = candidateNonMutatingSpreads.get(
operand.identifier.id,
);
if (spread != null) {
candidateNonMutatingSpreads.delete(spread);
}
}
}
}
break;
}
default: {
if (candidateNonMutatingSpreads.size !== 0) {
// Otherwise any reference to the spread object itself may mutate
for (const operand of eachInstructionValueOperand(value)) {
const spread = candidateNonMutatingSpreads.get(
operand.identifier.id,
);
if (spread != null) {
candidateNonMutatingSpreads.delete(spread);
}
}
}
}
}
}
}
const nonMutatingSpreads = new Set<IdentifierId>();
for (const [key, value] of candidateNonMutatingSpreads) {
if (key === value) {
nonMutatingSpreads.add(key);
}
}
return nonMutatingSpreads;
}
function inferParam(
param: Place | SpreadPattern,
initialState: InferenceState,
@@ -908,14 +748,10 @@ function applyEffect(
case 'Alias':
case 'Capture': {
CompilerError.invariant(
effect.kind === 'Capture' ||
effect.kind === 'MaybeAlias' ||
initialized.has(effect.into.identifier.id),
effect.kind === 'Capture' || initialized.has(effect.into.identifier.id),
{
reason: `Expected destination to already be initialized within this instruction`,
description:
`Destination ${printPlace(effect.into)} is not initialized in this ` +
`instruction for effect ${printAliasingEffect(effect)}`,
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`,
details: [
{
kind: 'error',
@@ -931,68 +767,49 @@ function applyEffect(
* copy-on-write semantics, then we can prune the effect
*/
const intoKind = state.kind(effect.into).kind;
let destinationType: 'context' | 'mutable' | null = null;
let isMutableDesination: boolean;
switch (intoKind) {
case ValueKind.Context: {
destinationType = 'context';
break;
}
case ValueKind.Context:
case ValueKind.Mutable:
case ValueKind.MaybeFrozen: {
destinationType = 'mutable';
isMutableDesination = true;
break;
}
default: {
isMutableDesination = false;
break;
}
}
const fromKind = state.kind(effect.from).kind;
let sourceType: 'context' | 'mutable' | 'frozen' | null = null;
let isMutableReferenceType: boolean;
switch (fromKind) {
case ValueKind.Context: {
sourceType = 'context';
break;
}
case ValueKind.Global:
case ValueKind.Primitive: {
isMutableReferenceType = false;
break;
}
case ValueKind.MaybeFrozen:
case ValueKind.Frozen: {
sourceType = 'frozen';
isMutableReferenceType = false;
applyEffect(
context,
state,
{
kind: 'ImmutableCapture',
from: effect.from,
into: effect.into,
},
initialized,
effects,
);
break;
}
default: {
sourceType = 'mutable';
isMutableReferenceType = true;
break;
}
}
if (sourceType === 'frozen') {
applyEffect(
context,
state,
{
kind: 'ImmutableCapture',
from: effect.from,
into: effect.into,
},
initialized,
effects,
);
} else if (
(sourceType === 'mutable' && destinationType === 'mutable') ||
effect.kind === 'MaybeAlias'
) {
if (isMutableDesination && isMutableReferenceType) {
effects.push(effect);
} else if (
(sourceType === 'context' && destinationType != null) ||
(sourceType === 'mutable' && destinationType === 'context')
) {
applyEffect(
context,
state,
{kind: 'MaybeAlias', from: effect.from, into: effect.into},
initialized,
effects,
);
}
break;
}
@@ -1977,16 +1794,8 @@ function computeSignatureForInstruction(
}
case 'PropertyStore':
case 'ComputedStore': {
/**
* Add a hint about naming as "ref"/"-Ref", but only if we weren't able to infer any
* type for the object. In some cases the variable may be named like a ref, but is
* also used as a ref callback such that we infer the type as a function rather than
* a ref.
*/
const mutationReason: MutationReason | null =
value.kind === 'PropertyStore' &&
value.property === 'current' &&
value.object.identifier.type.kind === 'Type'
value.kind === 'PropertyStore' && value.property === 'current'
? {kind: 'AssignCurrentProperty'}
: null;
effects.push({
@@ -2215,9 +2024,7 @@ function computeSignatureForInstruction(
kind: 'Create',
into: place,
reason: ValueReason.Other,
value: context.nonMutatingSpreads.has(place.identifier.id)
? ValueKind.Frozen
: ValueKind.Mutable,
value: ValueKind.Mutable,
});
effects.push({
kind: 'Capture',
@@ -2452,7 +2259,7 @@ function computeEffectsForLegacySignature(
}),
});
}
if (signature.knownIncompatible != null && state.env.enableValidations) {
if (signature.knownIncompatible != null && state.env.isInferredMemoEnabled) {
const errors = new CompilerError();
errors.pushDiagnostic(
CompilerDiagnostic.create({

View File

@@ -779,13 +779,7 @@ class AliasingState {
if (edge.index >= index) {
break;
}
queue.push({
place: edge.node,
transitive,
direction: 'forwards',
// Traversing a maybeAlias edge always downgrades to conditional mutation
kind: edge.kind === 'maybeAlias' ? MutationKind.Conditional : kind,
});
queue.push({place: edge.node, transitive, direction: 'forwards', kind});
}
for (const [alias, when] of node.createdFrom) {
if (when >= index) {
@@ -813,12 +807,7 @@ class AliasingState {
if (when >= index) {
continue;
}
queue.push({
place: alias,
transitive,
direction: 'backwards',
kind,
});
queue.push({place: alias, transitive, direction: 'backwards', kind});
}
/**
* MaybeAlias indicates potential data flow from unknown function calls,

View File

@@ -7,6 +7,7 @@
import {CompilerError} from '..';
import {
BlockId,
Effect,
Environment,
HIRFunction,
@@ -14,12 +15,14 @@ import {
IdentifierId,
Instruction,
Place,
computePostDominatorTree,
evaluatesToStableTypeOrContainer,
getHookKind,
isStableType,
isStableTypeContainer,
isUseOperator,
} from '../HIR';
import {PostDominator} from '../HIR/Dominator';
import {
eachInstructionLValue,
eachInstructionOperand,
@@ -32,7 +35,6 @@ import {
} from '../ReactiveScopes/InferReactiveScopeVariables';
import DisjointSet from '../Utils/DisjointSet';
import {assertExhaustive} from '../Utils/utils';
import {createControlDominators} from './ControlDominators';
/**
* Side map to track and propagate sources of stability (i.e. hook calls such as
@@ -210,9 +212,45 @@ export function inferReactivePlaces(fn: HIRFunction): void {
reactiveIdentifiers.markReactive(place);
}
const isReactiveControlledBlock = createControlDominators(fn, place =>
reactiveIdentifiers.isReactive(place),
);
const postDominators = computePostDominatorTree(fn, {
includeThrowsAsExitNode: false,
});
const postDominatorFrontierCache = new Map<BlockId, Set<BlockId>>();
function isReactiveControlledBlock(id: BlockId): boolean {
let controlBlocks = postDominatorFrontierCache.get(id);
if (controlBlocks === undefined) {
controlBlocks = postDominatorFrontier(fn, postDominators, id);
postDominatorFrontierCache.set(id, controlBlocks);
}
for (const blockId of controlBlocks) {
const controlBlock = fn.body.blocks.get(blockId)!;
switch (controlBlock.terminal.kind) {
case 'if':
case 'branch': {
if (reactiveIdentifiers.isReactive(controlBlock.terminal.test)) {
return true;
}
break;
}
case 'switch': {
if (reactiveIdentifiers.isReactive(controlBlock.terminal.test)) {
return true;
}
for (const case_ of controlBlock.terminal.cases) {
if (
case_.test !== null &&
reactiveIdentifiers.isReactive(case_.test)
) {
return true;
}
}
break;
}
}
}
return false;
}
do {
for (const [, block] of fn.body.blocks) {
@@ -373,6 +411,61 @@ export function inferReactivePlaces(fn: HIRFunction): void {
propagateReactivityToInnerFunctions(fn, true);
}
/*
* Computes the post-dominator frontier of @param block. These are immediate successors of nodes that
* post-dominate @param targetId and from which execution may not reach @param block. Intuitively, these
* are the earliest blocks from which execution branches such that it may or may not reach the target block.
*/
function postDominatorFrontier(
fn: HIRFunction,
postDominators: PostDominator<BlockId>,
targetId: BlockId,
): Set<BlockId> {
const visited = new Set<BlockId>();
const frontier = new Set<BlockId>();
const targetPostDominators = postDominatorsOf(fn, postDominators, targetId);
for (const blockId of [...targetPostDominators, targetId]) {
if (visited.has(blockId)) {
continue;
}
visited.add(blockId);
const block = fn.body.blocks.get(blockId)!;
for (const pred of block.preds) {
if (!targetPostDominators.has(pred)) {
// The predecessor does not always reach this block, we found an item on the frontier!
frontier.add(pred);
}
}
}
return frontier;
}
function postDominatorsOf(
fn: HIRFunction,
postDominators: PostDominator<BlockId>,
targetId: BlockId,
): Set<BlockId> {
const result = new Set<BlockId>();
const visited = new Set<BlockId>();
const queue = [targetId];
while (queue.length) {
const currentId = queue.shift()!;
if (visited.has(currentId)) {
continue;
}
visited.add(currentId);
const current = fn.body.blocks.get(currentId)!;
for (const pred of current.preds) {
const predPostDominator = postDominators.get(pred) ?? pred;
if (predPostDominator === targetId || result.has(predPostDominator)) {
result.add(pred);
}
queue.push(pred);
}
}
return result;
}
class ReactivityMap {
hasChanges: boolean = false;
reactive: Set<IdentifierId> = new Set();

View File

@@ -609,19 +609,6 @@ function evaluateInstruction(
constantPropagationImpl(value.loweredFunc.func, constants);
return null;
}
case 'StartMemoize': {
if (value.deps != null) {
for (const dep of value.deps) {
if (dep.root.kind === 'NamedLocal') {
const placeValue = read(constants, dep.root.value);
if (placeValue != null && placeValue.kind === 'Primitive') {
dep.root.constant = true;
}
}
}
}
return null;
}
default: {
// TODO: handle more cases
return null;

View File

@@ -7,8 +7,6 @@
import {
BlockId,
Environment,
getHookKind,
HIRFunction,
Identifier,
IdentifierId,
@@ -70,14 +68,9 @@ export function deadCodeElimination(fn: HIRFunction): void {
}
class State {
env: Environment;
named: Set<string> = new Set();
identifiers: Set<IdentifierId> = new Set();
constructor(env: Environment) {
this.env = env;
}
// Mark the identifier as being referenced (not dead code)
reference(identifier: Identifier): void {
this.identifiers.add(identifier.id);
@@ -119,7 +112,7 @@ function findReferencedIdentifiers(fn: HIRFunction): State {
const hasLoop = hasBackEdge(fn);
const reversedBlocks = [...fn.body.blocks.values()].reverse();
const state = new State(fn.env);
const state = new State();
let size = state.count;
do {
size = state.count;
@@ -317,27 +310,12 @@ function pruneableValue(value: InstructionValue, state: State): boolean {
// explicitly retain debugger statements to not break debugging workflows
return false;
}
case 'CallExpression':
case 'MethodCall': {
if (state.env.outputMode === 'ssr') {
const calleee =
value.kind === 'CallExpression' ? value.callee : value.property;
const hookKind = getHookKind(state.env, calleee.identifier);
switch (hookKind) {
case 'useState':
case 'useReducer':
case 'useRef': {
// unused refs can be removed
return true;
}
}
}
return false;
}
case 'Await':
case 'CallExpression':
case 'ComputedDelete':
case 'ComputedStore':
case 'PropertyDelete':
case 'MethodCall':
case 'PropertyStore':
case 'StoreGlobal': {
/*

View File

@@ -1,269 +0,0 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import {CompilerError} from '..';
import {
CallExpression,
getHookKind,
HIRFunction,
IdentifierId,
InstructionValue,
isArrayType,
isPlainObjectType,
isPrimitiveType,
isSetStateType,
isStartTransitionType,
LoadLocal,
StoreLocal,
} from '../HIR';
import {
eachInstructionValueOperand,
eachTerminalOperand,
} from '../HIR/visitors';
import {retainWhere} from '../Utils/utils';
/**
* Optimizes the code for running specifically in an SSR environment. This optimization
* asssumes that setState will not be called during render during initial mount, which
* allows inlining useState/useReducer.
*
* Optimizations:
* - Inline useState/useReducer
* - Remove effects
* - Remove refs where known to be unused during render (eg directly passed to a dom node)
* - Remove event handlers
*
* Note that an earlier pass already inlines useMemo/useCallback
*/
export function optimizeForSSR(fn: HIRFunction): void {
const inlinedState = new Map<IdentifierId, InstructionValue>();
/**
* First pass identifies useState/useReducer which can be safely inlined. Any use
* of the hook return other than destructuring (with a specific pattern) prevents
* inlining.
*
* Supported cases:
* - `const [state, ] = useState( <primitive-array-or-object> )`
* - `const [state, ] = useReducer(..., <value>)`
* - `const [state, ] = useReducer[..., <value>, <init>]`
*/
for (const block of fn.body.blocks.values()) {
for (const instr of block.instructions) {
const {value} = instr;
switch (value.kind) {
case 'Destructure': {
if (
inlinedState.has(value.value.identifier.id) &&
value.lvalue.pattern.kind === 'ArrayPattern' &&
value.lvalue.pattern.items.length >= 1 &&
value.lvalue.pattern.items[0].kind === 'Identifier'
) {
// Allow destructuring of inlined states
continue;
}
break;
}
case 'MethodCall':
case 'CallExpression': {
const calleee =
value.kind === 'CallExpression' ? value.callee : value.property;
const hookKind = getHookKind(fn.env, calleee.identifier);
switch (hookKind) {
case 'useReducer': {
if (
value.args.length === 2 &&
value.args[1].kind === 'Identifier'
) {
const arg = value.args[1];
const replace: LoadLocal = {
kind: 'LoadLocal',
place: arg,
loc: arg.loc,
};
inlinedState.set(instr.lvalue.identifier.id, replace);
} else if (
value.args.length === 3 &&
value.args[1].kind === 'Identifier' &&
value.args[2].kind === 'Identifier'
) {
const arg = value.args[1];
const initializer = value.args[2];
const replace: CallExpression = {
kind: 'CallExpression',
callee: initializer,
args: [arg],
loc: value.loc,
};
inlinedState.set(instr.lvalue.identifier.id, replace);
}
break;
}
case 'useState': {
if (
value.args.length === 1 &&
value.args[0].kind === 'Identifier'
) {
const arg = value.args[0];
if (
isPrimitiveType(arg.identifier) ||
isPlainObjectType(arg.identifier) ||
isArrayType(arg.identifier)
) {
const replace: LoadLocal = {
kind: 'LoadLocal',
place: arg,
loc: arg.loc,
};
inlinedState.set(instr.lvalue.identifier.id, replace);
}
}
break;
}
}
}
}
// Any use of useState/useReducer return besides destructuring prevents inlining
if (inlinedState.size !== 0) {
for (const operand of eachInstructionValueOperand(value)) {
inlinedState.delete(operand.identifier.id);
}
}
}
if (inlinedState.size !== 0) {
for (const operand of eachTerminalOperand(block.terminal)) {
inlinedState.delete(operand.identifier.id);
}
}
}
for (const block of fn.body.blocks.values()) {
for (const instr of block.instructions) {
const {value} = instr;
switch (value.kind) {
case 'FunctionExpression': {
if (hasKnownNonRenderCall(value.loweredFunc.func)) {
instr.value = {
kind: 'Primitive',
value: undefined,
loc: value.loc,
};
}
break;
}
case 'JsxExpression': {
if (
value.tag.kind === 'BuiltinTag' &&
value.tag.name.indexOf('-') === -1
) {
const tag = value.tag.name;
retainWhere(value.props, prop => {
return (
prop.kind === 'JsxSpreadAttribute' ||
(!isKnownEventHandler(tag, prop.name) && prop.name !== 'ref')
);
});
}
break;
}
case 'Destructure': {
if (inlinedState.has(value.value.identifier.id)) {
// Canonical check is part of determining if state can inline, this is for TS
CompilerError.invariant(
value.lvalue.pattern.kind === 'ArrayPattern' &&
value.lvalue.pattern.items.length >= 1 &&
value.lvalue.pattern.items[0].kind === 'Identifier',
{
reason:
'Expected a valid destructuring pattern for inlined state',
description: null,
details: [
{
kind: 'error',
message: 'Expected a valid destructuring pattern',
loc: value.loc,
},
],
},
);
const store: StoreLocal = {
kind: 'StoreLocal',
loc: value.loc,
type: null,
lvalue: {
kind: value.lvalue.kind,
place: value.lvalue.pattern.items[0],
},
value: value.value,
};
instr.value = store;
}
break;
}
case 'MethodCall':
case 'CallExpression': {
const calleee =
value.kind === 'CallExpression' ? value.callee : value.property;
const hookKind = getHookKind(fn.env, calleee.identifier);
switch (hookKind) {
case 'useEffectEvent': {
if (
value.args.length === 1 &&
value.args[0].kind === 'Identifier'
) {
const load: LoadLocal = {
kind: 'LoadLocal',
place: value.args[0],
loc: value.loc,
};
instr.value = load;
}
break;
}
case 'useEffect':
case 'useLayoutEffect':
case 'useInsertionEffect': {
// Drop effects
instr.value = {
kind: 'Primitive',
value: undefined,
loc: value.loc,
};
break;
}
case 'useReducer':
case 'useState': {
const replace = inlinedState.get(instr.lvalue.identifier.id);
if (replace != null) {
instr.value = replace;
}
break;
}
}
}
}
}
}
}
function hasKnownNonRenderCall(fn: HIRFunction): boolean {
for (const block of fn.body.blocks.values()) {
for (const instr of block.instructions) {
if (
instr.value.kind === 'CallExpression' &&
(isSetStateType(instr.value.callee.identifier) ||
isStartTransitionType(instr.value.callee.identifier))
) {
return true;
}
}
}
return false;
}
const EVENT_HANDLER_PATTERN = /^on[A-Z]/;
function isKnownEventHandler(_tag: string, prop: string): boolean {
return EVENT_HANDLER_PATTERN.test(prop);
}

View File

@@ -515,7 +515,6 @@ function emitDestructureProps(
pattern: {
kind: 'ObjectPattern',
properties,
loc: GeneratedSource,
},
kind: InstructionKind.Let,
},

View File

@@ -159,7 +159,7 @@ export function codegenFunction(
const compiled = compileResult.unwrap();
const hookGuard = fn.env.config.enableEmitHookGuards;
if (hookGuard != null && fn.env.outputMode === 'client') {
if (hookGuard != null && fn.env.isInferredMemoEnabled) {
compiled.body = t.blockStatement([
createHookGuard(
hookGuard,
@@ -259,7 +259,7 @@ export function codegenFunction(
if (
emitInstrumentForget != null &&
fn.id != null &&
fn.env.outputMode === 'client'
fn.env.isInferredMemoEnabled
) {
/*
* Technically, this is a conditional hook call. However, we expect
@@ -591,10 +591,7 @@ function codegenBlockNoReset(
}
function wrapCacheDep(cx: Context, value: t.Expression): t.Expression {
if (
cx.env.config.enableEmitFreeze != null &&
cx.env.outputMode === 'client'
) {
if (cx.env.config.enableEmitFreeze != null && cx.env.isInferredMemoEnabled) {
const emitFreezeIdentifier = cx.env.programContext.addImportSpecifier(
cx.env.config.enableEmitFreeze,
).name;
@@ -702,7 +699,7 @@ function codegenReactiveScope(
outputComments.push(name.name);
if (!cx.hasDeclared(identifier)) {
statements.push(
t.variableDeclaration('let', [createVariableDeclarator(name, null)]),
t.variableDeclaration('let', [t.variableDeclarator(name)]),
);
}
cacheLoads.push({name, index, value: wrapCacheDep(cx, name)});
@@ -1362,6 +1359,8 @@ function codegenInstructionNullable(
value = null;
} else {
lvalue = instr.value.lvalue.pattern;
let hasReassign = false;
let hasDeclaration = false;
for (const place of eachPatternOperand(lvalue)) {
if (
kind !== InstructionKind.Reassign &&
@@ -1369,6 +1368,26 @@ function codegenInstructionNullable(
) {
cx.temp.set(place.identifier.declarationId, null);
}
const isDeclared = cx.hasDeclared(place.identifier);
hasReassign ||= isDeclared;
hasDeclaration ||= !isDeclared;
}
if (hasReassign && hasDeclaration) {
CompilerError.invariant(false, {
reason:
'Encountered a destructuring operation where some identifiers are already declared (reassignments) but others are not (declarations)',
description: null,
details: [
{
kind: 'error',
loc: instr.loc,
message: null,
},
],
suggestions: null,
});
} else if (hasReassign) {
kind = InstructionKind.Reassign;
}
value = codegenPlaceToExpression(cx, instr.value.value);
}
@@ -1387,7 +1406,7 @@ function codegenInstructionNullable(
suggestions: null,
});
return createVariableDeclaration(instr.loc, 'const', [
createVariableDeclarator(codegenLValue(cx, lvalue), value),
t.variableDeclarator(codegenLValue(cx, lvalue), value),
]);
}
case InstructionKind.Function: {
@@ -1451,7 +1470,7 @@ function codegenInstructionNullable(
suggestions: null,
});
return createVariableDeclaration(instr.loc, 'let', [
createVariableDeclarator(codegenLValue(cx, lvalue), value),
t.variableDeclarator(codegenLValue(cx, lvalue), value),
]);
}
case InstructionKind.Reassign: {
@@ -1691,9 +1710,6 @@ function withLoc<T extends (...args: Array<any>) => t.Node>(
};
}
const createIdentifier = withLoc(t.identifier);
const createArrayPattern = withLoc(t.arrayPattern);
const createObjectPattern = withLoc(t.objectPattern);
const createBinaryExpression = withLoc(t.binaryExpression);
const createExpressionStatement = withLoc(t.expressionStatement);
const _createLabelledStatement = withLoc(t.labeledStatement);
@@ -1725,31 +1741,6 @@ const createTryStatement = withLoc(t.tryStatement);
const createBreakStatement = withLoc(t.breakStatement);
const createContinueStatement = withLoc(t.continueStatement);
function createVariableDeclarator(
id: t.LVal,
init?: t.Expression | null,
): t.VariableDeclarator {
const node = t.variableDeclarator(id, init);
/*
* The variable declarator location is not preserved in HIR, however, we can use the
* start location of the id and the end location of the init to recreate the
* exact original variable declarator location.
*
* Or if init is null, we likely have a declaration without an initializer, so we can use the id.loc.end as the end location.
*/
if (id.loc && (init === null || init?.loc)) {
node.loc = {
start: id.loc.start,
end: init?.loc?.end ?? id.loc.end,
filename: id.loc.filename,
identifierName: undefined,
};
}
return node;
}
function createHookGuard(
guard: ExternalFunction,
context: ProgramContext,
@@ -1803,7 +1794,7 @@ function createCallExpression(
}
const hookGuard = env.config.enableEmitHookGuards;
if (hookGuard != null && isHook && env.outputMode === 'client') {
if (hookGuard != null && isHook && env.isInferredMemoEnabled) {
const iife = t.functionExpression(
null,
[],
@@ -1857,7 +1848,7 @@ function codegenInstruction(
);
} else {
return createVariableDeclaration(instr.loc, 'const', [
createVariableDeclarator(
t.variableDeclarator(
convertIdentifier(instr.lvalue.identifier),
expressionValue,
),
@@ -2784,7 +2775,7 @@ function codegenArrayPattern(
): t.ArrayPattern {
const hasHoles = !pattern.items.every(e => e.kind !== 'Hole');
if (hasHoles) {
const result = createArrayPattern(pattern.loc, []);
const result = t.arrayPattern([]);
/*
* Older versions of babel have a validation bug fixed by
* https://github.com/babel/babel/pull/10917
@@ -2805,8 +2796,7 @@ function codegenArrayPattern(
}
return result;
} else {
return createArrayPattern(
pattern.loc,
return t.arrayPattern(
pattern.items.map(item => {
if (item.kind === 'Hole') {
return null;
@@ -2826,8 +2816,7 @@ function codegenLValue(
return codegenArrayPattern(cx, pattern);
}
case 'ObjectPattern': {
return createObjectPattern(
pattern.loc,
return t.objectPattern(
pattern.properties.map(property => {
if (property.kind === 'ObjectProperty') {
const key = codegenObjectPropertyKey(cx, property.key);
@@ -2946,7 +2935,7 @@ function convertIdentifier(identifier: Identifier): t.Identifier {
suggestions: null,
},
);
return createIdentifier(identifier.loc, identifier.name.value);
return t.identifier(identifier.name.value);
}
function compareScopeDependency(

View File

@@ -19,11 +19,7 @@ import {
promoteTemporary,
} from '../HIR';
import {clonePlaceToTemporary} from '../HIR/HIRBuilder';
import {
eachInstructionLValueWithKind,
eachPatternOperand,
mapPatternOperands,
} from '../HIR/visitors';
import {eachPatternOperand, mapPatternOperands} from '../HIR/visitors';
import {
ReactiveFunctionTransform,
Transformed,
@@ -117,9 +113,6 @@ class Visitor extends ReactiveFunctionTransform<State> {
): Transformed<ReactiveStatement> {
this.visitInstruction(instruction, state);
let instructionsToProcess: Array<ReactiveInstruction> = [instruction];
let result: Transformed<ReactiveStatement> = {kind: 'keep'};
if (instruction.value.kind === 'Destructure') {
const transformed = transformDestructuring(
state,
@@ -127,8 +120,7 @@ class Visitor extends ReactiveFunctionTransform<State> {
instruction.value,
);
if (transformed) {
instructionsToProcess = transformed;
result = {
return {
kind: 'replace-many',
value: transformed.map(instruction => ({
kind: 'instruction',
@@ -137,17 +129,7 @@ class Visitor extends ReactiveFunctionTransform<State> {
};
}
}
// Update state.declared with declarations from the instruction(s)
for (const instr of instructionsToProcess) {
for (const [place, kind] of eachInstructionLValueWithKind(instr)) {
if (kind !== InstructionKind.Reassign) {
state.declared.add(place.identifier.declarationId);
}
}
}
return result;
return {kind: 'keep'};
}
}
@@ -162,13 +144,10 @@ function transformDestructuring(
const isDeclared = state.declared.has(place.identifier.declarationId);
if (isDeclared) {
reassigned.add(place.identifier.id);
} else {
hasDeclaration = true;
}
hasDeclaration ||= !isDeclared;
}
if (!hasDeclaration) {
// all reassignments
destructure.lvalue.kind = InstructionKind.Reassign;
if (reassigned.size === 0 || !hasDeclaration) {
return null;
}
/*

View File

@@ -389,6 +389,14 @@ export function findDisjointMutableValues(
*/
operand.identifier.mutableRange.start > 0
) {
if (
instr.value.kind === 'FunctionExpression' ||
instr.value.kind === 'ObjectMethod'
) {
if (operand.identifier.type.kind === 'Primitive') {
continue;
}
}
operands.push(operand.identifier);
}
}

View File

@@ -8,41 +8,13 @@
import {
HIRFunction,
IdentifierId,
InstructionValue,
makeInstructionId,
MutableRange,
Place,
ReactiveScope,
ReactiveValue,
} from '../HIR';
import {Macro} from '../HIR/Environment';
import {eachInstructionValueOperand} from '../HIR/visitors';
/**
* Whether a macro requires its arguments to be transitively inlined (eg fbt)
* or just avoid having the top-level values be converted to variables (eg fbt.param)
*/
enum InlineLevel {
Transitive = 'Transitive',
Shallow = 'Shallow',
}
type MacroDefinition = {
level: InlineLevel;
properties: Map<string, MacroDefinition> | null;
};
const SHALLOW_MACRO: MacroDefinition = {
level: InlineLevel.Shallow,
properties: null,
};
const TRANSITIVE_MACRO: MacroDefinition = {
level: InlineLevel.Transitive,
properties: null,
};
const FBT_MACRO: MacroDefinition = {
level: InlineLevel.Transitive,
properties: new Map([['*', SHALLOW_MACRO]]),
};
FBT_MACRO.properties!.set('enum', FBT_MACRO);
import {Macro, MacroMethod} from '../HIR/Environment';
import {eachReactiveValueOperand} from './visitors';
/**
* This pass supports the `fbt` translation system (https://facebook.github.io/fbt/)
@@ -67,210 +39,230 @@ FBT_MACRO.properties!.set('enum', FBT_MACRO);
* ## User-defined macro-like function
*
* Users can also specify their own functions to be treated similarly to fbt via the
* `customMacros` environment configuration. By default, user-supplied custom macros
* have their arguments transitively inlined.
* `customMacros` environment configuration.
*/
export function memoizeFbtAndMacroOperandsInSameScope(
fn: HIRFunction,
): Set<IdentifierId> {
const macroKinds = new Map<Macro, MacroDefinition>([
...Array.from(FBT_TAGS.entries()),
...(fn.env.config.customMacros ?? []).map(
name => [name, TRANSITIVE_MACRO] as [Macro, MacroDefinition],
),
const fbtMacroTags = new Set<Macro>([
...Array.from(FBT_TAGS).map((tag): Macro => [tag, []]),
...(fn.env.config.customMacros ?? []),
]);
/**
* Forward data-flow analysis to identify all macro tags, including
* things like `fbt.foo.bar(...)`
*/
const macroTags = populateMacroTags(fn, macroKinds);
/**
* Reverse data-flow analysis to merge arguments to macro *invocations*
* based on the kind of the macro
*/
const macroValues = mergeMacroArguments(fn, macroTags, macroKinds);
return macroValues;
const fbtValues: Set<IdentifierId> = new Set();
const macroMethods = new Map<IdentifierId, Array<Array<MacroMethod>>>();
while (true) {
let vsize = fbtValues.size;
let msize = macroMethods.size;
visit(fn, fbtMacroTags, fbtValues, macroMethods);
if (vsize === fbtValues.size && msize === macroMethods.size) {
break;
}
}
return fbtValues;
}
const FBT_TAGS: Map<string, MacroDefinition> = new Map([
['fbt', FBT_MACRO],
['fbt:param', SHALLOW_MACRO],
['fbt:enum', FBT_MACRO],
['fbt:plural', SHALLOW_MACRO],
['fbs', FBT_MACRO],
['fbs:param', SHALLOW_MACRO],
['fbs:enum', FBT_MACRO],
['fbs:plural', SHALLOW_MACRO],
export const FBT_TAGS: Set<string> = new Set([
'fbt',
'fbt:param',
'fbs',
'fbs:param',
]);
export const SINGLE_CHILD_FBT_TAGS: Set<string> = new Set([
'fbt:param',
'fbs:param',
]);
function populateMacroTags(
function visit(
fn: HIRFunction,
macroKinds: Map<Macro, MacroDefinition>,
): Map<IdentifierId, MacroDefinition> {
const macroTags = new Map<IdentifierId, MacroDefinition>();
for (const block of fn.body.blocks.values()) {
for (const instr of block.instructions) {
const {lvalue, value} = instr;
switch (value.kind) {
case 'Primitive': {
if (typeof value.value === 'string') {
const macroDefinition = macroKinds.get(value.value);
if (macroDefinition != null) {
/*
* We don't distinguish between tag names and strings, so record
* all `fbt` string literals in case they are used as a jsx tag.
*/
macroTags.set(lvalue.identifier.id, macroDefinition);
fbtMacroTags: Set<Macro>,
fbtValues: Set<IdentifierId>,
macroMethods: Map<IdentifierId, Array<Array<MacroMethod>>>,
): void {
for (const [, block] of fn.body.blocks) {
for (const instruction of block.instructions) {
const {lvalue, value} = instruction;
if (lvalue === null) {
continue;
}
if (
value.kind === 'Primitive' &&
typeof value.value === 'string' &&
matchesExactTag(value.value, fbtMacroTags)
) {
/*
* We don't distinguish between tag names and strings, so record
* all `fbt` string literals in case they are used as a jsx tag.
*/
fbtValues.add(lvalue.identifier.id);
} else if (
value.kind === 'LoadGlobal' &&
matchesExactTag(value.binding.name, fbtMacroTags)
) {
// Record references to `fbt` as a global
fbtValues.add(lvalue.identifier.id);
} else if (
value.kind === 'LoadGlobal' &&
matchTagRoot(value.binding.name, fbtMacroTags) !== null
) {
const methods = matchTagRoot(value.binding.name, fbtMacroTags)!;
macroMethods.set(lvalue.identifier.id, methods);
} else if (
value.kind === 'PropertyLoad' &&
macroMethods.has(value.object.identifier.id)
) {
const methods = macroMethods.get(value.object.identifier.id)!;
const newMethods = [];
for (const method of methods) {
if (
method.length > 0 &&
(method[0].type === 'wildcard' ||
(method[0].type === 'name' && method[0].name === value.property))
) {
if (method.length > 1) {
newMethods.push(method.slice(1));
} else {
fbtValues.add(lvalue.identifier.id);
}
}
break;
}
case 'LoadGlobal': {
let macroDefinition = macroKinds.get(value.binding.name);
if (macroDefinition != null) {
macroTags.set(lvalue.identifier.id, macroDefinition);
}
break;
if (newMethods.length > 0) {
macroMethods.set(lvalue.identifier.id, newMethods);
}
case 'PropertyLoad': {
if (typeof value.property === 'string') {
const macroDefinition = macroTags.get(value.object.identifier.id);
if (macroDefinition != null) {
const propertyDefinition =
macroDefinition.properties != null
? (macroDefinition.properties.get(value.property) ??
macroDefinition.properties.get('*'))
: null;
const propertyMacro = propertyDefinition ?? macroDefinition;
macroTags.set(lvalue.identifier.id, propertyMacro);
}
} else if (isFbtCallExpression(fbtValues, value)) {
const fbtScope = lvalue.identifier.scope;
if (fbtScope === null) {
continue;
}
/*
* if the JSX element's tag was `fbt`, mark all its operands
* to ensure that they end up in the same scope as the jsx element
* itself.
*/
for (const operand of eachReactiveValueOperand(value)) {
operand.identifier.scope = fbtScope;
// Expand the jsx element's range to account for its operands
expandFbtScopeRange(fbtScope.range, operand.identifier.mutableRange);
fbtValues.add(operand.identifier.id);
}
} else if (
isFbtJsxExpression(fbtMacroTags, fbtValues, value) ||
isFbtJsxChild(fbtValues, lvalue, value)
) {
const fbtScope = lvalue.identifier.scope;
if (fbtScope === null) {
continue;
}
/*
* if the JSX element's tag was `fbt`, mark all its operands
* to ensure that they end up in the same scope as the jsx element
* itself.
*/
for (const operand of eachReactiveValueOperand(value)) {
operand.identifier.scope = fbtScope;
// Expand the jsx element's range to account for its operands
expandFbtScopeRange(fbtScope.range, operand.identifier.mutableRange);
/*
* NOTE: we add the operands as fbt values so that they are also
* grouped with this expression
*/
fbtValues.add(operand.identifier.id);
}
} else if (fbtValues.has(lvalue.identifier.id)) {
const fbtScope = lvalue.identifier.scope;
if (fbtScope === null) {
return;
}
for (const operand of eachReactiveValueOperand(value)) {
if (
operand.identifier.name !== null &&
operand.identifier.name.kind === 'named'
) {
/*
* named identifiers were already locals, we only have to force temporaries
* into the same scope
*/
continue;
}
break;
operand.identifier.scope = fbtScope;
// Expand the jsx element's range to account for its operands
expandFbtScopeRange(fbtScope.range, operand.identifier.mutableRange);
}
}
}
}
return macroTags;
}
function mergeMacroArguments(
fn: HIRFunction,
macroTags: Map<IdentifierId, MacroDefinition>,
macroKinds: Map<Macro, MacroDefinition>,
): Set<IdentifierId> {
const macroValues = new Set<IdentifierId>(macroTags.keys());
for (const block of Array.from(fn.body.blocks.values()).reverse()) {
for (let i = block.instructions.length - 1; i >= 0; i--) {
const instr = block.instructions[i]!;
const {lvalue, value} = instr;
switch (value.kind) {
case 'DeclareContext':
case 'DeclareLocal':
case 'Destructure':
case 'LoadContext':
case 'LoadLocal':
case 'PostfixUpdate':
case 'PrefixUpdate':
case 'StoreContext':
case 'StoreLocal': {
// Instructions that never need to be merged
break;
}
case 'CallExpression':
case 'MethodCall': {
const scope = lvalue.identifier.scope;
if (scope == null) {
continue;
}
const callee =
value.kind === 'CallExpression' ? value.callee : value.property;
const macroDefinition =
macroTags.get(callee.identifier.id) ??
macroTags.get(lvalue.identifier.id);
if (macroDefinition != null) {
visitOperands(
macroDefinition,
scope,
lvalue,
value,
macroValues,
macroTags,
);
}
break;
}
case 'JsxExpression': {
const scope = lvalue.identifier.scope;
if (scope == null) {
continue;
}
let macroDefinition;
if (value.tag.kind === 'Identifier') {
macroDefinition = macroTags.get(value.tag.identifier.id);
} else {
macroDefinition = macroKinds.get(value.tag.name);
}
macroDefinition ??= macroTags.get(lvalue.identifier.id);
if (macroDefinition != null) {
visitOperands(
macroDefinition,
scope,
lvalue,
value,
macroValues,
macroTags,
);
}
break;
}
default: {
const scope = lvalue.identifier.scope;
if (scope == null) {
continue;
}
const macroDefinition = macroTags.get(lvalue.identifier.id);
if (macroDefinition != null) {
visitOperands(
macroDefinition,
scope,
lvalue,
value,
macroValues,
macroTags,
);
}
break;
}
}
function matchesExactTag(s: string, tags: Set<Macro>): boolean {
return Array.from(tags).some(macro =>
typeof macro === 'string'
? s === macro
: macro[1].length === 0 && macro[0] === s,
);
}
function matchTagRoot(
s: string,
tags: Set<Macro>,
): Array<Array<MacroMethod>> | null {
const methods: Array<Array<MacroMethod>> = [];
for (const macro of tags) {
if (typeof macro === 'string') {
continue;
}
for (const phi of block.phis) {
const scope = phi.place.identifier.scope;
if (scope == null) {
continue;
}
const macroDefinition = macroTags.get(phi.place.identifier.id);
if (
macroDefinition == null ||
macroDefinition.level === InlineLevel.Shallow
) {
continue;
}
macroValues.add(phi.place.identifier.id);
for (const operand of phi.operands.values()) {
operand.identifier.scope = scope;
expandFbtScopeRange(scope.range, operand.identifier.mutableRange);
macroTags.set(operand.identifier.id, macroDefinition);
macroValues.add(operand.identifier.id);
}
const [tag, rest] = macro;
if (tag === s && rest.length > 0) {
methods.push(rest);
}
}
return macroValues;
if (methods.length > 0) {
return methods;
} else {
return null;
}
}
function isFbtCallExpression(
fbtValues: Set<IdentifierId>,
value: ReactiveValue,
): boolean {
return (
(value.kind === 'CallExpression' &&
fbtValues.has(value.callee.identifier.id)) ||
(value.kind === 'MethodCall' && fbtValues.has(value.property.identifier.id))
);
}
function isFbtJsxExpression(
fbtMacroTags: Set<Macro>,
fbtValues: Set<IdentifierId>,
value: ReactiveValue,
): boolean {
return (
value.kind === 'JsxExpression' &&
((value.tag.kind === 'Identifier' &&
fbtValues.has(value.tag.identifier.id)) ||
(value.tag.kind === 'BuiltinTag' &&
matchesExactTag(value.tag.name, fbtMacroTags)))
);
}
function isFbtJsxChild(
fbtValues: Set<IdentifierId>,
lvalue: Place | null,
value: ReactiveValue,
): boolean {
return (
(value.kind === 'JsxExpression' || value.kind === 'JsxFragment') &&
lvalue !== null &&
fbtValues.has(lvalue.identifier.id)
);
}
function expandFbtScopeRange(
@@ -283,22 +275,3 @@ function expandFbtScopeRange(
);
}
}
function visitOperands(
macroDefinition: MacroDefinition,
scope: ReactiveScope,
lvalue: Place,
value: InstructionValue,
macroValues: Set<IdentifierId>,
macroTags: Map<IdentifierId, MacroDefinition>,
): void {
macroValues.add(lvalue.identifier.id);
for (const operand of eachInstructionValueOperand(value)) {
if (macroDefinition.level === InlineLevel.Transitive) {
operand.identifier.scope = scope;
expandFbtScopeRange(scope.range, operand.identifier.mutableRange);
macroTags.set(operand.identifier.id, macroDefinition);
}
macroValues.add(operand.identifier.id);
}
}

View File

@@ -19,7 +19,7 @@ export function nameAnonymousFunctions(fn: HIRFunction): void {
const parentName = fn.id;
const functions = nameAnonymousFunctionsImpl(fn);
function visit(node: Node, prefix: string): void {
if (node.generatedName != null && node.fn.nameHint == null) {
if (node.generatedName != null) {
/**
* Note that we don't generate a name for functions that already had one,
* so we'll only add the prefix to anonymous functions regardless of
@@ -70,10 +70,6 @@ function nameAnonymousFunctionsImpl(fn: HIRFunction): Array<Node> {
if (name != null && name.kind === 'named') {
names.set(lvalue.identifier.id, name.value);
}
const func = functions.get(value.place.identifier.id);
if (func != null) {
functions.set(lvalue.identifier.id, func);
}
break;
}
case 'PropertyLoad': {
@@ -110,7 +106,6 @@ function nameAnonymousFunctionsImpl(fn: HIRFunction): Array<Node> {
const variableName = value.lvalue.place.identifier.name;
if (
node != null &&
node.generatedName == null &&
variableName != null &&
variableName.kind === 'named'
) {
@@ -142,7 +137,7 @@ function nameAnonymousFunctionsImpl(fn: HIRFunction): Array<Node> {
continue;
}
const node = functions.get(arg.identifier.id);
if (node != null && node.generatedName == null) {
if (node != null) {
const generatedName =
fnArgCount > 1 ? `${calleeName}(arg${i})` : `${calleeName}()`;
node.generatedName = generatedName;
@@ -157,7 +152,7 @@ function nameAnonymousFunctionsImpl(fn: HIRFunction): Array<Node> {
continue;
}
const node = functions.get(attr.place.identifier.id);
if (node != null && node.generatedName == null) {
if (node != null) {
const elementName =
value.tag.kind === 'BuiltinTag'
? value.tag.name

View File

@@ -25,14 +25,12 @@ import {
} from '../HIR/HIR';
import {
BuiltInArrayId,
BuiltInEventHandlerId,
BuiltInFunctionId,
BuiltInJsxId,
BuiltInMixedReadonlyId,
BuiltInObjectId,
BuiltInPropsId,
BuiltInRefValueId,
BuiltInSetStateId,
BuiltInUseRefId,
} from '../HIR/ObjectShape';
import {eachInstructionLValue, eachInstructionOperand} from '../HIR/visitors';
@@ -278,16 +276,9 @@ function* generateInstructionTypes(
* We should change Hook to a subtype of Function or change unifier logic.
* (see https://github.com/facebook/react-forget/pull/1427)
*/
let shapeId: string | null = null;
if (env.config.enableTreatSetIdentifiersAsStateSetters) {
const name = getName(names, value.callee.identifier.id);
if (name.startsWith('set')) {
shapeId = BuiltInSetStateId;
}
}
yield equation(value.callee.identifier.type, {
kind: 'Function',
shapeId,
shapeId: null,
return: returnType,
isConstructor: false,
});
@@ -394,7 +385,7 @@ function* generateInstructionTypes(
shapeId: BuiltInArrayId,
});
} else {
continue;
break;
}
}
} else {
@@ -472,41 +463,6 @@ function* generateInstructionTypes(
}
}
}
if (env.config.enableInferEventHandlers) {
if (
value.kind === 'JsxExpression' &&
value.tag.kind === 'BuiltinTag' &&
!value.tag.name.includes('-')
) {
/*
* Infer event handler types for built-in DOM elements.
* Props starting with "on" (e.g., onClick, onSubmit) on primitive tags
* are inferred as event handlers. This allows functions with ref access
* to be passed to these props, since DOM event handlers are guaranteed
* by React to only execute in response to events, never during render.
*
* We exclude tags with hyphens to avoid web components (custom elements),
* which are required by the HTML spec to contain a hyphen. Web components
* may call event handler props during their lifecycle methods (e.g.,
* connectedCallback), which would be unsafe for ref access.
*/
for (const prop of value.props) {
if (
prop.kind === 'JsxAttribute' &&
prop.name.startsWith('on') &&
prop.name.length > 2 &&
prop.name[2] === prop.name[2].toUpperCase()
) {
yield equation(prop.place.identifier.type, {
kind: 'Function',
shapeId: BuiltInEventHandlerId,
return: makeType(),
isConstructor: false,
});
}
}
}
}
yield equation(left, {kind: 'Object', shapeId: BuiltInJsxId});
break;
}

View File

@@ -5,7 +5,7 @@
* LICENSE file in the root directory of this source tree.
*/
import {fromZodError} from 'zod-validation-error/v4';
import {fromZodError} from 'zod-validation-error';
import {CompilerError} from '../CompilerError';
import {
CompilationMode,
@@ -135,7 +135,16 @@ function parseConfigPragmaEnvironmentForTest(
} else if (val) {
const parsedVal = tryParseTestPragmaValue(val).unwrap();
if (key === 'customMacros' && typeof parsedVal === 'string') {
maybeConfig[key] = [parsedVal.split('.')[0]];
const valSplit = parsedVal.split('.');
const props = [];
for (const elt of valSplit.slice(1)) {
if (elt === '*') {
props.push({type: 'wildcard'});
} else if (elt.length > 0) {
props.push({type: 'name', name: elt});
}
}
maybeConfig[key] = [[valSplit[0], props]];
continue;
}
maybeConfig[key] = parsedVal;
@@ -166,7 +175,7 @@ function parseConfigPragmaEnvironmentForTest(
});
}
const testComplexPluginOptionDefaults: PluginOptions = {
const testComplexPluginOptionDefaults: Partial<PluginOptions> = {
gating: {
source: 'ReactForgetFeatureFlag',
importSpecifierName: 'isForgetEnabled_Fixtures',
@@ -179,6 +188,11 @@ export function parseConfigPragmaForTests(
environment?: PartialEnvironmentConfig;
},
): PluginOptions {
const overridePragma = parseConfigPragmaAsString(pragma);
if (overridePragma !== '') {
return parseConfigStringAsJS(overridePragma, defaults);
}
const environment = parseConfigPragmaEnvironmentForTest(
pragma,
defaults.environment ?? {},
@@ -214,3 +228,100 @@ 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}`,
details: [
{
kind: 'error',
loc: null,
message: null,
},
],
suggestions: null,
});
}
const environment = parseConfigPragmaEnvironmentForTest(
'',
defaults.environment ?? {},
);
const options: Record<keyof PluginOptions, unknown> = {
...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<string, unknown>),
...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)}`,
details: [
{
kind: 'error',
loc: null,
message: 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);
}

View File

@@ -1,862 +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 {Result} from '../Utils/Result';
import {CompilerDiagnostic, CompilerError, Effect} from '..';
import {ErrorCategory} from '../CompilerError';
import {
BlockId,
FunctionExpression,
HIRFunction,
IdentifierId,
isSetStateType,
isUseEffectHookType,
Place,
CallExpression,
Instruction,
isUseStateType,
BasicBlock,
isUseRefType,
SourceLocation,
ArrayExpression,
} from '../HIR';
import {eachInstructionLValue, eachInstructionOperand} from '../HIR/visitors';
import {isMutable} from '../ReactiveScopes/InferReactiveScopeVariables';
import {assertExhaustive} from '../Utils/utils';
type TypeOfValue = 'ignored' | 'fromProps' | 'fromState' | 'fromPropsAndState';
type DerivationMetadata = {
typeOfValue: TypeOfValue;
place: Place;
sourcesIds: Set<IdentifierId>;
isStateSource: boolean;
};
type EffectMetadata = {
effect: HIRFunction;
dependencies: ArrayExpression;
};
type ValidationContext = {
readonly functions: Map<IdentifierId, FunctionExpression>;
readonly candidateDependencies: Map<IdentifierId, ArrayExpression>;
readonly errors: CompilerError;
readonly derivationCache: DerivationCache;
readonly effectsCache: Map<IdentifierId, EffectMetadata>;
readonly setStateLoads: Map<IdentifierId, IdentifierId | null>;
readonly setStateUsages: Map<IdentifierId, Set<SourceLocation>>;
};
const MAX_FIXPOINT_ITERATIONS = 100;
class DerivationCache {
hasChanges: boolean = false;
cache: Map<IdentifierId, DerivationMetadata> = new Map();
private previousCache: Map<IdentifierId, DerivationMetadata> | null = null;
takeSnapshot(): void {
this.previousCache = new Map();
for (const [key, value] of this.cache.entries()) {
this.previousCache.set(key, {
place: value.place,
sourcesIds: new Set(value.sourcesIds),
typeOfValue: value.typeOfValue,
isStateSource: value.isStateSource,
});
}
}
checkForChanges(): void {
if (this.previousCache === null) {
this.hasChanges = true;
return;
}
for (const [key, value] of this.cache.entries()) {
const previousValue = this.previousCache.get(key);
if (
previousValue === undefined ||
!this.isDerivationEqual(previousValue, value)
) {
this.hasChanges = true;
return;
}
}
if (this.cache.size !== this.previousCache.size) {
this.hasChanges = true;
return;
}
this.hasChanges = false;
}
snapshot(): boolean {
const hasChanges = this.hasChanges;
this.hasChanges = false;
return hasChanges;
}
addDerivationEntry(
derivedVar: Place,
sourcesIds: Set<IdentifierId>,
typeOfValue: TypeOfValue,
isStateSource: boolean,
): void {
let finalIsSource = isStateSource;
if (!finalIsSource) {
for (const sourceId of sourcesIds) {
const sourceMetadata = this.cache.get(sourceId);
if (
sourceMetadata?.isStateSource &&
sourceMetadata.place.identifier.name?.kind !== 'named'
) {
finalIsSource = true;
break;
}
}
}
this.cache.set(derivedVar.identifier.id, {
place: derivedVar,
sourcesIds: sourcesIds,
typeOfValue: typeOfValue ?? 'ignored',
isStateSource: finalIsSource,
});
}
private isDerivationEqual(
a: DerivationMetadata,
b: DerivationMetadata,
): boolean {
if (a.typeOfValue !== b.typeOfValue) {
return false;
}
if (a.sourcesIds.size !== b.sourcesIds.size) {
return false;
}
for (const id of a.sourcesIds) {
if (!b.sourcesIds.has(id)) {
return false;
}
}
return true;
}
}
function isNamedIdentifier(place: Place): place is Place & {
identifier: {name: NonNullable<Place['identifier']['name']>};
} {
return (
place.identifier.name !== null && place.identifier.name.kind === 'named'
);
}
/**
* 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_exp(
fn: HIRFunction,
): Result<void, CompilerError> {
const functions: Map<IdentifierId, FunctionExpression> = new Map();
const candidateDependencies: Map<IdentifierId, ArrayExpression> = new Map();
const derivationCache = new DerivationCache();
const errors = new CompilerError();
const effectsCache: Map<IdentifierId, EffectMetadata> = new Map();
const setStateLoads: Map<IdentifierId, IdentifierId> = new Map();
const setStateUsages: Map<IdentifierId, Set<SourceLocation>> = new Map();
const context: ValidationContext = {
functions,
candidateDependencies,
errors,
derivationCache,
effectsCache,
setStateLoads,
setStateUsages,
};
if (fn.fnType === 'Hook') {
for (const param of fn.params) {
if (param.kind === 'Identifier') {
context.derivationCache.cache.set(param.identifier.id, {
place: param,
sourcesIds: new Set(),
typeOfValue: 'fromProps',
isStateSource: true,
});
}
}
} else if (fn.fnType === 'Component') {
const props = fn.params[0];
if (props != null && props.kind === 'Identifier') {
context.derivationCache.cache.set(props.identifier.id, {
place: props,
sourcesIds: new Set(),
typeOfValue: 'fromProps',
isStateSource: true,
});
}
}
let isFirstPass = true;
let iterationCount = 0;
do {
context.derivationCache.takeSnapshot();
for (const block of fn.body.blocks.values()) {
recordPhiDerivations(block, context);
for (const instr of block.instructions) {
recordInstructionDerivations(instr, context, isFirstPass);
}
}
context.derivationCache.checkForChanges();
isFirstPass = false;
iterationCount++;
CompilerError.invariant(iterationCount < MAX_FIXPOINT_ITERATIONS, {
reason:
'[ValidateNoDerivedComputationsInEffects] Fixpoint iteration failed to converge.',
description: `Fixpoint iteration exceeded ${MAX_FIXPOINT_ITERATIONS} iterations while tracking derivations. This suggests a cyclic dependency in the derivation cache.`,
details: [
{
kind: 'error',
loc: fn.loc,
message: `Exceeded ${MAX_FIXPOINT_ITERATIONS} iterations in ValidateNoDerivedComputationsInEffects`,
},
],
});
} while (context.derivationCache.snapshot());
for (const [, effect] of effectsCache) {
validateEffect(effect.effect, effect.dependencies, context);
}
return errors.asResult();
}
function recordPhiDerivations(
block: BasicBlock,
context: ValidationContext,
): void {
for (const phi of block.phis) {
let typeOfValue: TypeOfValue = 'ignored';
let sourcesIds: Set<IdentifierId> = new Set();
for (const operand of phi.operands.values()) {
const operandMetadata = context.derivationCache.cache.get(
operand.identifier.id,
);
if (operandMetadata === undefined) {
continue;
}
typeOfValue = joinValue(typeOfValue, operandMetadata.typeOfValue);
sourcesIds.add(operand.identifier.id);
}
if (typeOfValue !== 'ignored') {
context.derivationCache.addDerivationEntry(
phi.place,
sourcesIds,
typeOfValue,
false,
);
}
}
}
function joinValue(
lvalueType: TypeOfValue,
valueType: TypeOfValue,
): TypeOfValue {
if (lvalueType === 'ignored') return valueType;
if (valueType === 'ignored') return lvalueType;
if (lvalueType === valueType) return lvalueType;
return 'fromPropsAndState';
}
function getRootSetState(
key: IdentifierId,
loads: Map<IdentifierId, IdentifierId | null>,
visited: Set<IdentifierId> = new Set(),
): IdentifierId | null {
if (visited.has(key)) {
return null;
}
visited.add(key);
const parentId = loads.get(key);
if (parentId === undefined) {
return null;
}
if (parentId === null) {
return key;
}
return getRootSetState(parentId, loads, visited);
}
function maybeRecordSetState(
instr: Instruction,
loads: Map<IdentifierId, IdentifierId | null>,
usages: Map<IdentifierId, Set<SourceLocation>>,
): void {
for (const operand of eachInstructionLValue(instr)) {
if (
instr.value.kind === 'LoadLocal' &&
loads.has(instr.value.place.identifier.id)
) {
loads.set(operand.identifier.id, instr.value.place.identifier.id);
} else {
if (isSetStateType(operand.identifier)) {
// this is a root setState
loads.set(operand.identifier.id, null);
}
}
const rootSetState = getRootSetState(operand.identifier.id, loads);
if (rootSetState !== null && usages.get(rootSetState) === undefined) {
usages.set(rootSetState, new Set([operand.loc]));
}
}
}
function recordInstructionDerivations(
instr: Instruction,
context: ValidationContext,
isFirstPass: boolean,
): void {
maybeRecordSetState(instr, context.setStateLoads, context.setStateUsages);
let typeOfValue: TypeOfValue = 'ignored';
let isSource: boolean = false;
const sources: Set<IdentifierId> = new Set();
const {lvalue, value} = instr;
if (value.kind === 'FunctionExpression') {
context.functions.set(lvalue.identifier.id, value);
for (const [, block] of value.loweredFunc.func.body.blocks) {
recordPhiDerivations(block, context);
for (const instr of block.instructions) {
recordInstructionDerivations(instr, context, isFirstPass);
}
}
} else if (value.kind === 'CallExpression' || value.kind === 'MethodCall') {
const callee =
value.kind === 'CallExpression' ? value.callee : value.property;
if (
isUseEffectHookType(callee.identifier) &&
value.args.length === 2 &&
value.args[0].kind === 'Identifier' &&
value.args[1].kind === 'Identifier'
) {
const effectFunction = context.functions.get(value.args[0].identifier.id);
const deps = context.candidateDependencies.get(
value.args[1].identifier.id,
);
if (effectFunction != null && deps != null) {
context.effectsCache.set(value.args[0].identifier.id, {
effect: effectFunction.loweredFunc.func,
dependencies: deps,
});
}
} else if (isUseStateType(lvalue.identifier)) {
typeOfValue = 'fromState';
context.derivationCache.addDerivationEntry(
lvalue,
new Set(),
typeOfValue,
true,
);
return;
}
} else if (value.kind === 'ArrayExpression') {
context.candidateDependencies.set(lvalue.identifier.id, value);
}
for (const operand of eachInstructionOperand(instr)) {
if (context.setStateLoads.has(operand.identifier.id)) {
const rootSetStateId = getRootSetState(
operand.identifier.id,
context.setStateLoads,
);
if (rootSetStateId !== null) {
context.setStateUsages.get(rootSetStateId)?.add(operand.loc);
}
}
const operandMetadata = context.derivationCache.cache.get(
operand.identifier.id,
);
if (operandMetadata === undefined) {
continue;
}
typeOfValue = joinValue(typeOfValue, operandMetadata.typeOfValue);
sources.add(operand.identifier.id);
}
if (typeOfValue === 'ignored') {
return;
}
for (const lvalue of eachInstructionLValue(instr)) {
context.derivationCache.addDerivationEntry(
lvalue,
sources,
typeOfValue,
isSource,
);
}
if (value.kind === 'FunctionExpression') {
/*
* We don't want to record effect mutations of FunctionExpressions the mutations will happen in the
* function body and we will record them there.
*/
return;
}
for (const operand of eachInstructionOperand(instr)) {
switch (operand.effect) {
case Effect.Capture:
case Effect.Store:
case Effect.ConditionallyMutate:
case Effect.ConditionallyMutateIterator:
case Effect.Mutate: {
if (isMutable(instr, operand)) {
if (context.derivationCache.cache.has(operand.identifier.id)) {
const operandMetadata = context.derivationCache.cache.get(
operand.identifier.id,
);
if (operandMetadata !== undefined) {
operandMetadata.typeOfValue = joinValue(
typeOfValue,
operandMetadata.typeOfValue,
);
}
} else {
context.derivationCache.addDerivationEntry(
operand,
sources,
typeOfValue,
false,
);
}
}
break;
}
case Effect.Freeze:
case Effect.Read: {
// no-op
break;
}
case Effect.Unknown: {
CompilerError.invariant(false, {
reason: 'Unexpected unknown effect',
description: null,
details: [
{
kind: 'error',
loc: operand.loc,
message: 'Unexpected unknown effect',
},
],
});
}
default: {
assertExhaustive(
operand.effect,
`Unexpected effect kind \`${operand.effect}\``,
);
}
}
}
}
type TreeNode = {
name: string;
typeOfValue: TypeOfValue;
isSource: boolean;
children: Array<TreeNode>;
};
function buildTreeNode(
sourceId: IdentifierId,
context: ValidationContext,
visited: Set<string> = new Set(),
): Array<TreeNode> {
const sourceMetadata = context.derivationCache.cache.get(sourceId);
if (!sourceMetadata) {
return [];
}
if (sourceMetadata.isStateSource && isNamedIdentifier(sourceMetadata.place)) {
return [
{
name: sourceMetadata.place.identifier.name.value,
typeOfValue: sourceMetadata.typeOfValue,
isSource: sourceMetadata.isStateSource,
children: [],
},
];
}
const children: Array<TreeNode> = [];
const namedSiblings: Set<string> = new Set();
for (const childId of sourceMetadata.sourcesIds) {
CompilerError.invariant(childId !== sourceId, {
reason:
'Unexpected self-reference: a value should not have itself as a source',
description: null,
details: [
{
kind: 'error',
loc: sourceMetadata.place.loc,
message: null,
},
],
});
const childNodes = buildTreeNode(
childId,
context,
new Set([
...visited,
...(isNamedIdentifier(sourceMetadata.place)
? [sourceMetadata.place.identifier.name.value]
: []),
]),
);
if (childNodes) {
for (const childNode of childNodes) {
if (!namedSiblings.has(childNode.name)) {
children.push(childNode);
namedSiblings.add(childNode.name);
}
}
}
}
if (
isNamedIdentifier(sourceMetadata.place) &&
!visited.has(sourceMetadata.place.identifier.name.value)
) {
return [
{
name: sourceMetadata.place.identifier.name.value,
typeOfValue: sourceMetadata.typeOfValue,
isSource: sourceMetadata.isStateSource,
children: children,
},
];
}
return children;
}
function renderTree(
node: TreeNode,
indent: string = '',
isLast: boolean = true,
propsSet: Set<string>,
stateSet: Set<string>,
): string {
const prefix = indent + (isLast ? '└── ' : '├── ');
const childIndent = indent + (isLast ? ' ' : '│ ');
let result = `${prefix}${node.name}`;
if (node.isSource) {
let typeLabel: string;
if (node.typeOfValue === 'fromProps') {
propsSet.add(node.name);
typeLabel = 'Prop';
} else if (node.typeOfValue === 'fromState') {
stateSet.add(node.name);
typeLabel = 'State';
} else {
propsSet.add(node.name);
stateSet.add(node.name);
typeLabel = 'Prop and State';
}
result += ` (${typeLabel})`;
}
if (node.children.length > 0) {
result += '\n';
node.children.forEach((child, index) => {
const isLastChild = index === node.children.length - 1;
result += renderTree(child, childIndent, isLastChild, propsSet, stateSet);
if (index < node.children.length - 1) {
result += '\n';
}
});
}
return result;
}
function getFnLocalDeps(
fn: FunctionExpression | undefined,
): Set<IdentifierId> | undefined {
if (!fn) {
return undefined;
}
const deps: Set<IdentifierId> = new Set();
for (const [, block] of fn.loweredFunc.func.body.blocks) {
for (const instr of block.instructions) {
if (instr.value.kind === 'LoadLocal') {
deps.add(instr.value.place.identifier.id);
}
}
}
return deps;
}
function validateEffect(
effectFunction: HIRFunction,
dependencies: ArrayExpression,
context: ValidationContext,
): void {
const seenBlocks: Set<BlockId> = new Set();
const effectDerivedSetStateCalls: Array<{
value: CallExpression;
id: IdentifierId;
sourceIds: Set<IdentifierId>;
typeOfValue: TypeOfValue;
}> = [];
const effectSetStateUsages: Map<
IdentifierId,
Set<SourceLocation>
> = new Map();
// Consider setStates in the effect's dependency array as being part of effectSetStateUsages
for (const dep of dependencies.elements) {
if (dep.kind === 'Identifier') {
const root = getRootSetState(dep.identifier.id, context.setStateLoads);
if (root !== null) {
effectSetStateUsages.set(root, new Set([dep.loc]));
}
}
}
let cleanUpFunctionDeps: Set<IdentifierId> | undefined;
const globals: Set<IdentifierId> = new Set();
for (const block of effectFunction.body.blocks.values()) {
/*
* if the block is in an effect and is of type return then its an effect's cleanup function
* if the cleanup function depends on a value from which effect-set state is derived then
* we can't validate
*/
if (
block.terminal.kind === 'return' &&
block.terminal.returnVariant === 'Explicit'
) {
cleanUpFunctionDeps = getFnLocalDeps(
context.functions.get(block.terminal.value.identifier.id),
);
}
for (const pred of block.preds) {
if (!seenBlocks.has(pred)) {
// skip if block has a back edge
return;
}
}
for (const instr of block.instructions) {
// Early return if any instruction is deriving a value from a ref
if (isUseRefType(instr.lvalue.identifier)) {
return;
}
maybeRecordSetState(instr, context.setStateLoads, effectSetStateUsages);
for (const operand of eachInstructionOperand(instr)) {
if (context.setStateLoads.has(operand.identifier.id)) {
const rootSetStateId = getRootSetState(
operand.identifier.id,
context.setStateLoads,
);
if (rootSetStateId !== null) {
effectSetStateUsages.get(rootSetStateId)?.add(operand.loc);
}
}
}
if (
instr.value.kind === 'CallExpression' &&
isSetStateType(instr.value.callee.identifier) &&
instr.value.args.length === 1 &&
instr.value.args[0].kind === 'Identifier'
) {
const calleeMetadata = context.derivationCache.cache.get(
instr.value.callee.identifier.id,
);
/*
* If the setState comes from a source other than local state skip
* since the fix is not to calculate in render
*/
if (calleeMetadata?.typeOfValue != 'fromState') {
continue;
}
const argMetadata = context.derivationCache.cache.get(
instr.value.args[0].identifier.id,
);
if (argMetadata !== undefined) {
effectDerivedSetStateCalls.push({
value: instr.value,
id: instr.value.callee.identifier.id,
sourceIds: argMetadata.sourcesIds,
typeOfValue: argMetadata.typeOfValue,
});
}
} else if (instr.value.kind === 'CallExpression') {
const calleeMetadata = context.derivationCache.cache.get(
instr.value.callee.identifier.id,
);
if (
calleeMetadata !== undefined &&
(calleeMetadata.typeOfValue === 'fromProps' ||
calleeMetadata.typeOfValue === 'fromPropsAndState')
) {
// If the callee is a prop we can't confidently say that it should be derived in render
return;
}
if (globals.has(instr.value.callee.identifier.id)) {
// If the callee is a global we can't confidently say that it should be derived in render
return;
}
} else if (instr.value.kind === 'LoadGlobal') {
globals.add(instr.lvalue.identifier.id);
for (const operand of eachInstructionOperand(instr)) {
globals.add(operand.identifier.id);
}
}
}
seenBlocks.add(block.id);
}
for (const derivedSetStateCall of effectDerivedSetStateCalls) {
const rootSetStateCall = getRootSetState(
derivedSetStateCall.id,
context.setStateLoads,
);
if (
rootSetStateCall !== null &&
effectSetStateUsages.has(rootSetStateCall) &&
context.setStateUsages.has(rootSetStateCall) &&
effectSetStateUsages.get(rootSetStateCall)!.size ===
context.setStateUsages.get(rootSetStateCall)!.size - 1
) {
const propsSet = new Set<string>();
const stateSet = new Set<string>();
const rootNodesMap = new Map<string, TreeNode>();
for (const id of derivedSetStateCall.sourceIds) {
const nodes = buildTreeNode(id, context);
for (const node of nodes) {
if (!rootNodesMap.has(node.name)) {
rootNodesMap.set(node.name, node);
}
}
}
const rootNodes = Array.from(rootNodesMap.values());
const trees = rootNodes.map((node, index) =>
renderTree(
node,
'',
index === rootNodes.length - 1,
propsSet,
stateSet,
),
);
for (const dep of derivedSetStateCall.sourceIds) {
if (cleanUpFunctionDeps !== undefined && cleanUpFunctionDeps.has(dep)) {
return;
}
}
const propsArr = Array.from(propsSet);
const stateArr = Array.from(stateSet);
let rootSources = '';
if (propsArr.length > 0) {
rootSources += `Props: [${propsArr.join(', ')}]`;
}
if (stateArr.length > 0) {
if (rootSources) rootSources += '\n';
rootSources += `State: [${stateArr.join(', ')}]`;
}
const description = `Using an effect triggers an additional render which can hurt performance and user experience, potentially briefly showing stale values to the user
This setState call is setting a derived value that depends on the following reactive sources:
${rootSources}
Data Flow Tree:
${trees.join('\n')}
See: https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state`;
context.errors.pushDiagnostic(
CompilerDiagnostic.create({
description: description,
category: ErrorCategory.EffectDerivationsOfState,
reason:
'You might not need an effect. Derive values in render, not effects.',
}).withDetails({
kind: 'error',
loc: derivedSetStateCall.value.callee.loc,
message: 'This should be computed during render, not in an effect',
}),
);
}
}
}

View File

@@ -14,14 +14,12 @@ import {
BlockId,
HIRFunction,
IdentifierId,
Identifier,
Place,
SourceLocation,
getHookKindForType,
isRefValueType,
isUseRefType,
} from '../HIR';
import {BuiltInEventHandlerId} from '../HIR/ObjectShape';
import {
eachInstructionOperand,
eachInstructionValueOperand,
@@ -185,11 +183,6 @@ function refTypeOfType(place: Place): RefAccessType {
}
}
function isEventHandlerType(identifier: Identifier): boolean {
const type = identifier.type;
return type.kind === 'Function' && type.shapeId === BuiltInEventHandlerId;
}
function tyEqual(a: RefAccessType, b: RefAccessType): boolean {
if (a.kind !== b.kind) {
return false;
@@ -526,9 +519,6 @@ function validateNoRefAccessInRenderImpl(
*/
if (!didError) {
const isRefLValue = isUseRefType(instr.lvalue.identifier);
const isEventHandlerLValue = isEventHandlerType(
instr.lvalue.identifier,
);
for (const operand of eachInstructionValueOperand(instr.value)) {
/**
* By default we check that function call operands are not refs,
@@ -536,16 +526,29 @@ function validateNoRefAccessInRenderImpl(
*/
if (
isRefLValue ||
isEventHandlerLValue ||
(hookKind != null &&
hookKind !== 'useState' &&
hookKind !== 'useReducer')
) {
/**
* Allow passing refs or ref-accessing functions when:
* 1. lvalue is a ref (mergeRefs pattern: `mergeRefs(ref1, ref2)`)
* 2. lvalue is an event handler (DOM events execute outside render)
* 3. calling hooks (independently validated for ref safety)
* 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)) {
@@ -636,55 +639,12 @@ function validateNoRefAccessInRenderImpl(
case 'StartMemoize':
case 'FinishMemoize':
break;
case 'LoadGlobal': {
if (instr.value.binding.name === 'undefined') {
env.set(instr.lvalue.identifier.id, {kind: 'Nullable'});
}
break;
}
case 'Primitive': {
if (instr.value.value == null) {
env.set(instr.lvalue.identifier.id, {kind: 'Nullable'});
}
break;
}
case 'UnaryExpression': {
if (instr.value.operator === '!') {
const value = env.get(instr.value.value.identifier.id);
const refId =
value?.kind === 'RefValue' && value.refId != null
? value.refId
: null;
if (refId !== null) {
/*
* Record an error suggesting the `if (ref.current == null)` pattern,
* but also record the lvalue as a guard so that we don't emit a second
* error for the write to the ref
*/
env.set(instr.lvalue.identifier.id, {kind: 'Guard', refId});
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.Refs,
reason: 'Cannot access refs during render',
description: ERROR_DESCRIPTION,
})
.withDetails({
kind: 'error',
loc: instr.value.value.loc,
message: `Cannot access ref value during render`,
})
.withDetails({
kind: 'hint',
message:
'To initialize a ref only once, check that the ref is null with the pattern `if (ref.current == null) { ref.current = ... }`',
}),
);
break;
}
}
validateNoRefValueAccess(errors, env, instr.value.value);
break;
}
case 'BinaryExpression': {
const left = env.get(instr.value.left.identifier.id);
const right = env.get(instr.value.right.identifier.id);

View File

@@ -11,28 +11,16 @@ import {
ErrorCategory,
} from '../CompilerError';
import {
Environment,
HIRFunction,
IdentifierId,
isSetStateType,
isUseEffectHookType,
isUseEffectEventType,
isUseInsertionEffectHookType,
isUseLayoutEffectHookType,
isUseRefType,
isRefValueType,
Place,
Effect,
BlockId,
} from '../HIR';
import {
eachInstructionLValue,
eachInstructionValueOperand,
} from '../HIR/visitors';
import {createControlDominators} from '../Inference/ControlDominators';
import {isMutable} from '../ReactiveScopes/InferReactiveScopeVariables';
import {eachInstructionValueOperand} from '../HIR/visitors';
import {Result} from '../Utils/Result';
import {assertExhaustive, Iterable_some} from '../Utils/utils';
/**
* Validates against calling setState in the body of an effect (useEffect and friends),
@@ -44,7 +32,6 @@ import {assertExhaustive, Iterable_some} from '../Utils/utils';
*/
export function validateNoSetStateInEffects(
fn: HIRFunction,
env: Environment,
): Result<void, CompilerError> {
const setStateFunctions: Map<IdentifierId, Place> = new Map();
const errors = new CompilerError();
@@ -85,7 +72,6 @@ export function validateNoSetStateInEffects(
const callee = getSetStateCall(
instr.value.loweredFunc.func,
setStateFunctions,
env,
);
if (callee !== null) {
setStateFunctions.set(instr.lvalue.identifier.id, callee);
@@ -99,20 +85,7 @@ export function validateNoSetStateInEffects(
instr.value.kind === 'MethodCall'
? instr.value.receiver
: instr.value.callee;
if (isUseEffectEventType(callee.identifier)) {
const arg = instr.value.args[0];
if (arg !== undefined && arg.kind === 'Identifier') {
const setState = setStateFunctions.get(arg.identifier.id);
if (setState !== undefined) {
/**
* This effect event function calls setState synchonously,
* treat it as a setState function for transitive tracking
*/
setStateFunctions.set(instr.lvalue.identifier.id, setState);
}
}
} else if (
if (
isUseEffectHookType(callee.identifier) ||
isUseLayoutEffectHookType(callee.identifier) ||
isUseInsertionEffectHookType(callee.identifier)
@@ -121,58 +94,26 @@ export function validateNoSetStateInEffects(
if (arg !== undefined && arg.kind === 'Identifier') {
const setState = setStateFunctions.get(arg.identifier.id);
if (setState !== undefined) {
const enableVerbose =
env.config.enableVerboseNoSetStateInEffect;
if (enableVerbose) {
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. ' +
'Calling setState synchronously causes cascading renders that hurt performance.\n\n' +
'This pattern may indicate one of several issues:\n\n' +
'**1. Non-local derived data**: If the value being set could be computed from props/state ' +
'but requires data from a parent component, consider restructuring state ownership so the ' +
'derivation can happen during render in the component that owns the relevant state.\n\n' +
"**2. Derived event pattern**: If you're detecting when a prop changes (e.g., `isPlaying` " +
'transitioning from false to true), this often indicates the parent should provide an event ' +
'callback (like `onPlay`) instead of just the current state. Request access to the original event.\n\n' +
"**3. Force update / external sync**: If you're forcing a re-render to sync with an external " +
'data source (mutable values outside React), use `useSyncExternalStore` to properly subscribe ' +
'to external state changes.\n\n' +
'See: https://react.dev/learn/you-might-not-need-an-effect',
suggestions: null,
}).withDetails({
kind: 'error',
loc: setState.loc,
message:
'Avoid calling setState() directly within an effect',
}),
);
} else {
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)',
suggestions: null,
}).withDetails({
kind: 'error',
loc: setState.loc,
message:
'Avoid calling setState() directly within an effect',
}),
);
}
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)',
suggestions: null,
}).withDetails({
kind: 'error',
loc: setState.loc,
message:
'Avoid calling setState() directly within an effect',
}),
);
}
}
}
@@ -188,113 +129,9 @@ export function validateNoSetStateInEffects(
function getSetStateCall(
fn: HIRFunction,
setStateFunctions: Map<IdentifierId, Place>,
env: Environment,
): Place | null {
const enableAllowSetStateFromRefsInEffects =
env.config.enableAllowSetStateFromRefsInEffects;
const refDerivedValues: Set<IdentifierId> = new Set();
const isDerivedFromRef = (place: Place): boolean => {
return (
refDerivedValues.has(place.identifier.id) ||
isUseRefType(place.identifier) ||
isRefValueType(place.identifier)
);
};
const isRefControlledBlock: (id: BlockId) => boolean =
enableAllowSetStateFromRefsInEffects
? createControlDominators(fn, place => isDerivedFromRef(place))
: (): boolean => false;
for (const [, block] of fn.body.blocks) {
if (enableAllowSetStateFromRefsInEffects) {
for (const phi of block.phis) {
if (isDerivedFromRef(phi.place)) {
continue;
}
let isPhiDerivedFromRef = false;
for (const [, operand] of phi.operands) {
if (isDerivedFromRef(operand)) {
isPhiDerivedFromRef = true;
break;
}
}
if (isPhiDerivedFromRef) {
refDerivedValues.add(phi.place.identifier.id);
} else {
for (const [pred] of phi.operands) {
if (isRefControlledBlock(pred)) {
refDerivedValues.add(phi.place.identifier.id);
break;
}
}
}
}
}
for (const instr of block.instructions) {
if (enableAllowSetStateFromRefsInEffects) {
const hasRefOperand = Iterable_some(
eachInstructionValueOperand(instr.value),
isDerivedFromRef,
);
if (hasRefOperand) {
for (const lvalue of eachInstructionLValue(instr)) {
refDerivedValues.add(lvalue.identifier.id);
}
// Ref-derived values can also propagate through mutation
for (const operand of eachInstructionValueOperand(instr.value)) {
switch (operand.effect) {
case Effect.Capture:
case Effect.Store:
case Effect.ConditionallyMutate:
case Effect.ConditionallyMutateIterator:
case Effect.Mutate: {
if (isMutable(instr, operand)) {
refDerivedValues.add(operand.identifier.id);
}
break;
}
case Effect.Freeze:
case Effect.Read: {
// no-op
break;
}
case Effect.Unknown: {
CompilerError.invariant(false, {
reason: 'Unexpected unknown effect',
description: null,
details: [
{
kind: 'error',
loc: operand.loc,
message: null,
},
],
suggestions: null,
});
}
default: {
assertExhaustive(
operand.effect,
`Unexpected effect kind \`${operand.effect}\``,
);
}
}
}
}
if (
instr.value.kind === 'PropertyLoad' &&
instr.value.property === 'current' &&
(isUseRefType(instr.value.object.identifier) ||
isRefValueType(instr.value.object.identifier))
) {
refDerivedValues.add(instr.lvalue.identifier.id);
}
}
switch (instr.value.kind) {
case 'LoadLocal': {
if (setStateFunctions.has(instr.value.place.identifier.id)) {
@@ -324,23 +161,6 @@ function getSetStateCall(
isSetStateType(callee.identifier) ||
setStateFunctions.has(callee.identifier.id)
) {
if (enableAllowSetStateFromRefsInEffects) {
const arg = instr.value.args.at(0);
if (
arg !== undefined &&
arg.kind === 'Identifier' &&
refDerivedValues.has(arg.identifier.id)
) {
/**
* The one special case where we allow setStates in effects is in the very specific
* scenario where the value being set is derived from a ref. For example this may
* be needed when initial layout measurements from refs need to be stored in state.
*/
return null;
} else if (isRefControlledBlock(block.id)) {
continue;
}
}
/*
* TODO: once we support multiple locations per error, we should link to the
* original Place in the case that setStateFunction.has(callee)

View File

@@ -155,40 +155,20 @@ function validateNoSetStateInRenderImpl(
}),
);
} else if (unconditionalBlocks.has(block.id)) {
const enableUseKeyedState = fn.env.config.enableUseKeyedState;
if (enableUseKeyedState) {
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.RenderSetState,
reason: 'Cannot call setState during render',
description:
'Calling setState during render may trigger an infinite loop.\n' +
'* To reset state when other state/props change, use `const [state, setState] = useKeyedState(initialState, key)` to reset `state` when `key` changes.\n' +
'* To derive data from other state/props, compute the derived data during render without using state',
suggestions: null,
}).withDetails({
kind: 'error',
loc: callee.loc,
message: 'Found setState() in render',
}),
);
} else {
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.RenderSetState,
reason: 'Cannot call setState during render',
description:
'Calling setState during render may trigger an infinite loop.\n' +
'* To reset state when other state/props change, store the previous value in state and update conditionally: https://react.dev/reference/react/useState#storing-information-from-previous-renders\n' +
'* To derive data from other state/props, compute the derived data during render without using state',
suggestions: null,
}).withDetails({
kind: 'error',
loc: callee.loc,
message: 'Found setState() in render',
}),
);
}
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)',
suggestions: null,
}).withDetails({
kind: 'error',
loc: callee.loc,
message: 'Found setState() in render',
}),
);
}
}
break;

View File

@@ -242,7 +242,6 @@ function validateInferredDep(
normalizedDep = {
root: maybeNormalizedRoot.root,
path: [...maybeNormalizedRoot.path, ...dep.path],
loc: maybeNormalizedRoot.loc,
};
} else {
CompilerError.invariant(dep.identifier.name?.kind === 'named', {
@@ -268,10 +267,8 @@ function validateInferredDep(
effect: Effect.Read,
reactive: false,
},
constant: false,
},
path: [...dep.path],
loc: GeneratedSource,
};
}
for (const decl of declsWithinMemoBlock) {
@@ -382,10 +379,8 @@ class Visitor extends ReactiveFunctionVisitor<VisitorState> {
root: {
kind: 'NamedLocal',
value: storeTarget,
constant: false,
},
path: [],
loc: storeTarget.loc,
});
}
}
@@ -413,10 +408,8 @@ class Visitor extends ReactiveFunctionVisitor<VisitorState> {
root: {
kind: 'NamedLocal',
value: {...lvalue},
constant: false,
},
path: [],
loc: lvalue.loc,
});
}
}

View File

@@ -1,296 +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 {NodePath} from '@babel/traverse';
import * as t from '@babel/types';
import {CompilerDiagnostic, CompilerError, ErrorCategory} from '..';
import {CodegenFunction} from '../ReactiveScopes';
import {Result} from '../Utils/Result';
/**
* IMPORTANT: This validation is only intended for use in unit tests.
* It is not intended for use in production.
*
* This validation is used to ensure that the generated AST has proper source locations
* for "important" original nodes.
*
* There's one big gotcha with this validation: it only works if the "important" original nodes
* are not optimized away by the compiler.
*
* When that scenario happens, we should just update the fixture to not include a node that has no
* corresponding node in the generated AST due to being completely removed during compilation.
*/
/**
* Some common node types that are important for coverage tracking.
* Based on istanbul-lib-instrument + some other common nodes we expect to be present in the generated AST.
*
* Note: For VariableDeclaration, VariableDeclarator, and Identifier, we enforce stricter validation
* that requires both the source location AND node type to match in the generated AST. This ensures
* that variable declarations maintain their structural integrity through compilation.
*/
const IMPORTANT_INSTRUMENTED_TYPES = new Set([
'ArrowFunctionExpression',
'AssignmentPattern',
'ObjectMethod',
'ExpressionStatement',
'BreakStatement',
'ContinueStatement',
'ReturnStatement',
'ThrowStatement',
'TryStatement',
'VariableDeclarator',
'IfStatement',
'ForStatement',
'ForInStatement',
'ForOfStatement',
'WhileStatement',
'DoWhileStatement',
'SwitchStatement',
'SwitchCase',
'WithStatement',
'FunctionDeclaration',
'FunctionExpression',
'LabeledStatement',
'ConditionalExpression',
'LogicalExpression',
/**
* Note: these aren't important for coverage tracking,
* but we still want to track them to ensure we aren't regressing them when
* we fix the source location tracking for other nodes.
*/
'VariableDeclaration',
'Identifier',
]);
/**
* Check if a node is a manual memoization call that the compiler optimizes away.
* These include useMemo and useCallback calls, which are intentionally removed
* by the DropManualMemoization pass.
*/
function isManualMemoization(node: t.Node): boolean {
// Check if this is a useMemo/useCallback call expression
if (t.isCallExpression(node)) {
const callee = node.callee;
if (t.isIdentifier(callee)) {
return callee.name === 'useMemo' || callee.name === 'useCallback';
}
if (
t.isMemberExpression(callee) &&
t.isIdentifier(callee.property) &&
t.isIdentifier(callee.object)
) {
return (
callee.object.name === 'React' &&
(callee.property.name === 'useMemo' ||
callee.property.name === 'useCallback')
);
}
}
return false;
}
/**
* Create a location key for comparison. We compare by line/column/source,
* not by object identity.
*/
function locationKey(loc: t.SourceLocation): string {
return `${loc.start.line}:${loc.start.column}-${loc.end.line}:${loc.end.column}`;
}
/**
* Validates that important source locations from the original code are preserved
* in the generated AST. This ensures that Istanbul coverage instrumentation can
* properly map back to the original source code.
*
* The validator:
* 1. Collects locations from "important" nodes in the original AST (those that
* Istanbul instruments for coverage tracking)
* 2. Exempts known compiler optimizations (useMemo/useCallback removal)
* 3. Verifies that all important locations appear somewhere in the generated AST
*
* Missing locations can cause Istanbul to fail to track coverage for certain
* code paths, leading to inaccurate coverage reports.
*/
export function validateSourceLocations(
func: NodePath<
t.FunctionDeclaration | t.ArrowFunctionExpression | t.FunctionExpression
>,
generatedAst: CodegenFunction,
): Result<void, CompilerError> {
const errors = new CompilerError();
/*
* Step 1: Collect important locations from the original source
* Note: Multiple node types can share the same location (e.g. VariableDeclarator and Identifier)
*/
const importantOriginalLocations = new Map<
string,
{loc: t.SourceLocation; nodeTypes: Set<string>}
>();
func.traverse({
enter(path) {
const node = path.node;
// Only track node types that Istanbul instruments
if (!IMPORTANT_INSTRUMENTED_TYPES.has(node.type)) {
return;
}
// Skip manual memoization that the compiler intentionally removes
if (isManualMemoization(node)) {
return;
}
// Collect the location if it exists
if (node.loc) {
const key = locationKey(node.loc);
const existing = importantOriginalLocations.get(key);
if (existing) {
existing.nodeTypes.add(node.type);
} else {
importantOriginalLocations.set(key, {
loc: node.loc,
nodeTypes: new Set([node.type]),
});
}
}
},
});
// Step 2: Collect all locations from the generated AST with their node types
const generatedLocations = new Map<string, Set<string>>();
function collectGeneratedLocations(node: t.Node): void {
if (node.loc) {
const key = locationKey(node.loc);
const nodeTypes = generatedLocations.get(key);
if (nodeTypes) {
nodeTypes.add(node.type);
} else {
generatedLocations.set(key, new Set([node.type]));
}
}
// Use Babel's VISITOR_KEYS to traverse only actual node properties
const keys = t.VISITOR_KEYS[node.type as keyof typeof t.VISITOR_KEYS];
if (!keys) {
return;
}
for (const key of keys) {
const value = (node as any)[key];
if (Array.isArray(value)) {
for (const item of value) {
if (t.isNode(item)) {
collectGeneratedLocations(item);
}
}
} else if (t.isNode(value)) {
collectGeneratedLocations(value);
}
}
}
// Collect from main function body
collectGeneratedLocations(generatedAst.body);
// Collect from outlined functions
for (const outlined of generatedAst.outlined) {
collectGeneratedLocations(outlined.fn.body);
}
/*
* Step 3: Validate that all important locations are preserved
* For certain node types, also validate that the node type matches
*/
const strictNodeTypes = new Set([
'VariableDeclaration',
'VariableDeclarator',
'Identifier',
]);
const reportMissingLocation = (
loc: t.SourceLocation,
nodeType: string,
): void => {
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.Todo,
reason: 'Important source location missing in generated code',
description:
`Source location for ${nodeType} is missing in the generated output. This can cause coverage instrumentation ` +
`to fail to track this code properly, resulting in inaccurate coverage reports.`,
}).withDetails({
kind: 'error',
loc,
message: null,
}),
);
};
const reportWrongNodeType = (
loc: t.SourceLocation,
expectedType: string,
actualTypes: Set<string>,
): void => {
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.Todo,
reason:
'Important source location has wrong node type in generated code',
description:
`Source location for ${expectedType} exists in the generated output but with wrong node type(s): ${Array.from(actualTypes).join(', ')}. ` +
`This can cause coverage instrumentation to fail to track this code properly, resulting in inaccurate coverage reports.`,
}).withDetails({
kind: 'error',
loc,
message: null,
}),
);
};
for (const [key, {loc, nodeTypes}] of importantOriginalLocations) {
const generatedNodeTypes = generatedLocations.get(key);
if (!generatedNodeTypes) {
// Location is completely missing
reportMissingLocation(loc, Array.from(nodeTypes).join(', '));
} else {
// Location exists, check each node type
for (const nodeType of nodeTypes) {
if (
strictNodeTypes.has(nodeType) &&
!generatedNodeTypes.has(nodeType)
) {
/*
* For strict node types, the specific node type must be present
* Check if any generated node type is also an important original node type
*/
const hasValidNodeType = Array.from(generatedNodeTypes).some(
genType => nodeTypes.has(genType),
);
if (hasValidNodeType) {
// At least one generated node type is valid (also in original), so this is just missing
reportMissingLocation(loc, nodeType);
} else {
// None of the generated node types are in original - this is wrong node type
reportWrongNodeType(loc, nodeType, generatedNodeTypes);
}
}
}
}
}
return errors.asResult();
}

View File

@@ -10,37 +10,16 @@ import {
CompilerError,
ErrorCategory,
} from '../CompilerError';
import {
FunctionExpression,
HIRFunction,
IdentifierId,
SourceLocation,
} from '../HIR';
import {
eachInstructionValueOperand,
eachTerminalOperand,
} from '../HIR/visitors';
import {FunctionExpression, HIRFunction, IdentifierId} from '../HIR';
import {Result} from '../Utils/Result';
export function validateUseMemo(fn: HIRFunction): Result<void, CompilerError> {
const errors = new CompilerError();
const voidMemoErrors = new CompilerError();
const useMemos = new Set<IdentifierId>();
const react = new Set<IdentifierId>();
const functions = new Map<IdentifierId, FunctionExpression>();
const unusedUseMemos = new Map<IdentifierId, SourceLocation>();
for (const [, block] of fn.body.blocks) {
for (const {lvalue, value} of block.instructions) {
if (unusedUseMemos.size !== 0) {
/**
* Most of the time useMemo results are referenced immediately. Don't bother
* scanning instruction operands for useMemos unless there is an as-yet-unused
* useMemo.
*/
for (const operand of eachInstructionValueOperand(value)) {
unusedUseMemos.delete(operand.identifier.id);
}
}
switch (value.kind) {
case 'LoadGlobal': {
if (value.binding.name === 'useMemo') {
@@ -66,8 +45,10 @@ export function validateUseMemo(fn: HIRFunction): Result<void, CompilerError> {
case 'CallExpression': {
// Is the function being called useMemo, with at least 1 argument?
const callee =
value.kind === 'CallExpression' ? value.callee : value.property;
const isUseMemo = useMemos.has(callee.identifier.id);
value.kind === 'CallExpression'
? value.callee.identifier.id
: value.property.identifier.id;
const isUseMemo = useMemos.has(callee);
if (!isUseMemo || value.args.length === 0) {
continue;
}
@@ -123,106 +104,10 @@ export function validateUseMemo(fn: HIRFunction): Result<void, CompilerError> {
);
}
validateNoContextVariableAssignment(body.loweredFunc.func, errors);
if (fn.env.config.validateNoVoidUseMemo) {
if (!hasNonVoidReturn(body.loweredFunc.func)) {
voidMemoErrors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.VoidUseMemo,
reason: 'useMemo() callbacks must return a value',
description: `This useMemo() callback doesn't return a value. useMemo() is for computing and caching values, not for arbitrary side effects`,
suggestions: null,
}).withDetails({
kind: 'error',
loc: body.loc,
message: 'useMemo() callbacks must return a value',
}),
);
} else {
unusedUseMemos.set(lvalue.identifier.id, callee.loc);
}
}
break;
}
}
}
if (unusedUseMemos.size !== 0) {
for (const operand of eachTerminalOperand(block.terminal)) {
unusedUseMemos.delete(operand.identifier.id);
}
}
}
if (unusedUseMemos.size !== 0) {
/**
* Basic check for unused memos, where the result of the call is never referenced. This runs
* before DCE so it's more of an AST-level check that something, _anything_, cares about the value.
*
* This is easy to defeat with e.g. `const _ = useMemo(...)` but it at least gives us something to teach.
* Even a DCE-based version could be bypassed with `noop(useMemo(...))`.
*/
for (const loc of unusedUseMemos.values()) {
voidMemoErrors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.VoidUseMemo,
reason: 'useMemo() result is unused',
description: `This useMemo() value is unused. useMemo() is for computing and caching values, not for arbitrary side effects`,
suggestions: null,
}).withDetails({
kind: 'error',
loc,
message: 'useMemo() result is unused',
}),
);
}
}
fn.env.logErrors(voidMemoErrors.asResult());
return errors.asResult();
}
function validateNoContextVariableAssignment(
fn: HIRFunction,
errors: CompilerError,
): void {
const context = new Set(fn.context.map(place => place.identifier.id));
for (const block of fn.body.blocks.values()) {
for (const instr of block.instructions) {
const value = instr.value;
switch (value.kind) {
case 'StoreContext': {
if (context.has(value.lvalue.place.identifier.id)) {
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.UseMemo,
reason:
'useMemo() callbacks may not reassign variables declared outside of the callback',
description:
'useMemo() callbacks must be pure functions and cannot reassign variables defined outside of the callback function',
suggestions: null,
}).withDetails({
kind: 'error',
loc: value.lvalue.place.loc,
message: 'Cannot reassign variable',
}),
);
}
break;
}
}
}
}
}
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;
}

View File

@@ -12,5 +12,4 @@ export {validateNoCapitalizedCalls} from './ValidateNoCapitalizedCalls';
export {validateNoRefAccessInRender} from './ValidateNoRefAccessInRender';
export {validateNoSetStateInRender} from './ValidateNoSetStateInRender';
export {validatePreservedManualMemoization} from './ValidatePreservedManualMemoization';
export {validateSourceLocations} from './ValidateSourceLocations';
export {validateUseMemo} from './ValidateUseMemo';

View File

@@ -20,7 +20,7 @@ describe('parseConfigPragma()', () => {
validateHooksUsage: 1,
} as any);
}).toThrowErrorMatchingInlineSnapshot(
`"Error: Could not validate environment config. Update React Compiler config to fix the error. Validation error: Invalid input: 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"."`,
);
});
@@ -38,7 +38,7 @@ describe('parseConfigPragma()', () => {
],
} as any);
}).toThrowErrorMatchingInlineSnapshot(
`"Error: Could not validate environment config. Update React Compiler config to fix the error. Validation error: AutodepsIndex must be > 0 at "inferEffectDependencies[0].autodepsIndex"."`,
`"Error: Could not validate environment config. Update React Compiler config to fix the error. Validation error: autodepsIndex must be > 0 at "inferEffectDependencies[0].autodepsIndex"."`,
);
});

View File

@@ -35,8 +35,10 @@ function Component() {
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
const a = makeObject_Primitives();
const x = [];
x.push(a);
mutate(x);
t0 = [x, a];
$[0] = t0;

View File

@@ -33,6 +33,7 @@ function Component() {
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
const x = [];
x.push(a);
t1 = [x, a];
$[1] = t1;
} else {

View File

@@ -85,10 +85,14 @@ function Component(t0) {
let t1;
if ($[0] !== prop) {
const obj = shallowCopy(prop);
const aliasedObj = identity(obj);
const getId = () => obj.id;
mutate(aliasedObj);
setPropertyByKey(aliasedObj, "id", prop.id + 1);
t1 = <Stringify getId={getId} shouldInvokeFns={true} />;
$[0] = prop;
$[1] = t1;

View File

@@ -181,9 +181,12 @@ function Component(t0) {
if ($[0] !== prop) {
const obj = shallowCopy(prop);
const aliasedObj = identity(obj);
const id = [obj.id];
mutate(aliasedObj);
setPropertyByKey(aliasedObj, "id", prop.id + 1);
t1 = <Stringify id={id} />;
$[0] = prop;
$[1] = t1;

View File

@@ -54,6 +54,7 @@ function Foo(t0) {
let t1;
if ($[0] !== cond1 || $[1] !== cond2) {
const arr = makeArray({ a: 2 }, 2, []);
t1 = cond1 ? (
<>
<div>{identity("foo")}</div>

View File

@@ -2,7 +2,6 @@
## Input
```javascript
// @enablePreserveExistingMemoizationGuarantees:false
// bar(props.b) is an allocating expression that produces a primitive, which means
// that Forget should memoize it.
// Correctness:
@@ -17,8 +16,7 @@ function AllocatingPrimitiveAsDep(props) {
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @enablePreserveExistingMemoizationGuarantees:false
// bar(props.b) is an allocating expression that produces a primitive, which means
import { c as _c } from "react/compiler-runtime"; // bar(props.b) is an allocating expression that produces a primitive, which means
// that Forget should memoize it.
// Correctness:
// - y depends on either bar(props.b) or bar(props.b) + 1

View File

@@ -1,4 +1,3 @@
// @enablePreserveExistingMemoizationGuarantees:false
// bar(props.b) is an allocating expression that produces a primitive, which means
// that Forget should memoize it.
// Correctness:

View File

@@ -2,7 +2,6 @@
## Input
```javascript
// @enablePreserveExistingMemoizationGuarantees:false @validateExhaustiveMemoizationDependencies:false
import {useMemo} from 'react';
const someGlobal = {value: 0};
@@ -33,7 +32,7 @@ export const FIXTURE_ENTRYPOINT = {
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @enablePreserveExistingMemoizationGuarantees:false @validateExhaustiveMemoizationDependencies:false
import { c as _c } from "react/compiler-runtime";
import { useMemo } from "react";
const someGlobal = { value: 0 };

View File

@@ -1,4 +1,3 @@
// @enablePreserveExistingMemoizationGuarantees:false @validateExhaustiveMemoizationDependencies:false
import {useMemo} from 'react';
const someGlobal = {value: 0};

View File

@@ -49,6 +49,7 @@ function Component() {
ref.current = "";
}
};
t0 = () => {
setRef();
};

View File

@@ -49,6 +49,7 @@ function Component() {
ref.current.value = "";
}
};
t0 = () => {
setRef();
};

View File

@@ -1,148 +0,0 @@
## Input
```javascript
// @enableInferEventHandlers
import {useRef} from 'react';
// Simulates react-hook-form's handleSubmit
function handleSubmit<T>(callback: (data: T) => void | Promise<void>) {
return (event: any) => {
event.preventDefault();
callback({} as T);
};
}
// Simulates an upload function
async function upload(file: any): Promise<{blob: {url: string}}> {
return {blob: {url: 'https://example.com/file.jpg'}};
}
interface SignatureRef {
toFile(): any;
}
function Component() {
const ref = useRef<SignatureRef>(null);
const onSubmit = async (value: any) => {
// This should be allowed: accessing ref.current in an async event handler
// that's wrapped and passed to onSubmit prop
let sigUrl: string;
if (value.hasSignature) {
const {blob} = await upload(ref.current?.toFile());
sigUrl = blob?.url || '';
} else {
sigUrl = value.signature;
}
console.log('Signature URL:', sigUrl);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input type="text" name="signature" />
<button type="submit">Submit</button>
</form>
);
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{}],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @enableInferEventHandlers
import { useRef } from "react";
// Simulates react-hook-form's handleSubmit
function handleSubmit(callback) {
const $ = _c(2);
let t0;
if ($[0] !== callback) {
t0 = (event) => {
event.preventDefault();
callback({} as T);
};
$[0] = callback;
$[1] = t0;
} else {
t0 = $[1];
}
return t0;
}
// Simulates an upload function
async function upload(file) {
const $ = _c(1);
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t0 = { blob: { url: "https://example.com/file.jpg" } };
$[0] = t0;
} else {
t0 = $[0];
}
return t0;
}
interface SignatureRef {
toFile(): any;
}
function Component() {
const $ = _c(4);
const ref = useRef(null);
const onSubmit = async (value) => {
let sigUrl;
if (value.hasSignature) {
const { blob } = await upload(ref.current?.toFile());
sigUrl = blob?.url || "";
} else {
sigUrl = value.signature;
}
console.log("Signature URL:", sigUrl);
};
const t0 = handleSubmit(onSubmit);
let t1;
let t2;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t1 = <input type="text" name="signature" />;
t2 = <button type="submit">Submit</button>;
$[0] = t1;
$[1] = t2;
} else {
t1 = $[0];
t2 = $[1];
}
let t3;
if ($[2] !== t0) {
t3 = (
<form onSubmit={t0}>
{t1}
{t2}
</form>
);
$[2] = t0;
$[3] = t3;
} else {
t3 = $[3];
}
return t3;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{}],
};
```
### Eval output
(kind: ok) <form><input type="text" name="signature"><button type="submit">Submit</button></form>

Some files were not shown because too many files have changed in this diff Show More