Compare commits

..

1 Commits

Author SHA1 Message Date
Mofei Zhang
caf09d1e60 [compiler] Fix inferEffectDependencies lint false positives
Currently, inferred effect dependencies are considered a "compiler-required" feature. This means that untransformed callsites should escalate to a build error.

`ValidateNoUntransformedReferences` iterates 'special effect' callsites and checks that the compiler was able to successfully transform them. Prior to this PR, this relied on checking the number of arguments passed to this special effect.

This obviously doesn't work with `noEmit: true`, which is used for our eslint plugin (this avoids mutating the babel program as other linters run with the same ast). This PR adds a set of `babel.SourceLocation`s to do best effort matching in this mode.
2025-03-27 10:56:35 -04:00
2257 changed files with 31712 additions and 133554 deletions

View File

@@ -1,11 +1,10 @@
{
"packages": ["packages/react", "packages/react-dom", "packages/react-server-dom-webpack", "packages/scheduler"],
"packages": ["packages/react", "packages/react-dom", "packages/scheduler"],
"buildCommand": "download-build-in-codesandbox-ci",
"node": "20",
"node": "18",
"publishDirectory": {
"react": "build/oss-experimental/react",
"react-dom": "build/oss-experimental/react-dom",
"react-server-dom-webpack": "build/oss-experimental/react-server-dom-webpack",
"scheduler": "build/oss-experimental/scheduler"
},
"sandboxes": ["new"],

View File

@@ -28,6 +28,3 @@ packages/react-devtools-shared/src/hooks/__tests__/__source__/__untransformed__/
packages/react-devtools-shell/dist
packages/react-devtools-timeline/dist
packages/react-devtools-timeline/static
# Imported third-party Flow types
flow-typed/

View File

@@ -468,14 +468,13 @@ module.exports = {
files: ['packages/react-server-dom-webpack/**/*.js'],
globals: {
__webpack_chunk_load__: 'readonly',
__webpack_get_script_filename__: 'readonly',
__webpack_require__: 'readonly',
},
},
{
files: ['packages/react-server-dom-turbopack/**/*.js'],
globals: {
__turbopack_load_by_url__: 'readonly',
__turbopack_load__: 'readonly',
__turbopack_require__: 'readonly',
},
},
@@ -497,7 +496,6 @@ module.exports = {
'packages/react-devtools-shared/src/devtools/views/**/*.js',
'packages/react-devtools-shared/src/hook.js',
'packages/react-devtools-shared/src/backend/console.js',
'packages/react-devtools-shared/src/backend/fiber/renderer.js',
'packages/react-devtools-shared/src/backend/shared/DevToolsComponentStackFrame.js',
'packages/react-devtools-shared/src/frontend/utils/withPermissionsCheck.js',
],
@@ -506,7 +504,6 @@ module.exports = {
__IS_FIREFOX__: 'readonly',
__IS_EDGE__: 'readonly',
__IS_NATIVE__: 'readonly',
__IS_INTERNAL_MCP_BUILD__: 'readonly',
__IS_INTERNAL_VERSION__: 'readonly',
chrome: 'readonly',
},
@@ -517,14 +514,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'],
@@ -555,10 +544,13 @@ module.exports = {
},
globals: {
$Call: 'readonly',
$ElementType: 'readonly',
$Flow$ModuleRef: 'readonly',
$FlowFixMe: 'readonly',
$Keys: 'readonly',
$NonMaybeType: 'readonly',
$PropertyType: 'readonly',
$ReadOnly: 'readonly',
$ReadOnlyArray: 'readonly',
$ArrayBufferView: 'readonly',
@@ -567,13 +559,11 @@ module.exports = {
ConsoleTask: 'readonly', // TOOD: Figure out what the official name of this will be.
ReturnType: 'readonly',
AnimationFrameID: 'readonly',
WeakRef: 'readonly',
// For Flow type annotation. Only `BigInt` is valid at runtime.
bigint: 'readonly',
BigInt: 'readonly',
BigInt64Array: 'readonly',
BigUint64Array: 'readonly',
CacheType: 'readonly',
Class: 'readonly',
ClientRect: 'readonly',
CopyInspectedElementPath: 'readonly',
@@ -585,19 +575,15 @@ module.exports = {
$AsyncIterator: 'readonly',
Iterator: 'readonly',
AsyncIterator: 'readonly',
IntervalID: 'readonly',
IteratorResult: 'readonly',
JSONValue: 'readonly',
JSResourceReference: 'readonly',
mixin$Animatable: 'readonly',
MouseEventHandler: 'readonly',
NavigateEvent: 'readonly',
PerformanceMeasureOptions: 'readonly',
PropagationPhases: 'readonly',
PropertyDescriptor: 'readonly',
PropertyDescriptorMap: 'readonly',
Proxy$traps: 'readonly',
React$AbstractComponent: 'readonly',
React$Component: 'readonly',
React$ComponentType: 'readonly',
React$Config: 'readonly',
React$Context: 'readonly',
React$Element: 'readonly',
@@ -618,21 +604,17 @@ module.exports = {
symbol: 'readonly',
SyntheticEvent: 'readonly',
SyntheticMouseEvent: 'readonly',
SyntheticPointerEvent: 'readonly',
Thenable: 'readonly',
TimeoutID: 'readonly',
WheelEventHandler: 'readonly',
FinalizationRegistry: 'readonly',
Exclude: 'readonly',
Omit: 'readonly',
Keyframe: 'readonly',
PropertyIndexedKeyframes: 'readonly',
KeyframeAnimationOptions: 'readonly',
GetAnimationsOptions: 'readonly',
Animatable: 'readonly',
ScrollTimeline: 'readonly',
EventListenerOptionsOrUseCapture: 'readonly',
FocusOptions: 'readonly',
OptionalEffectTiming: 'readonly',
spyOnDev: 'readonly',
spyOnDevAndProd: 'readonly',
@@ -650,6 +632,5 @@ module.exports = {
AsyncLocalStorage: 'readonly',
async_hooks: 'readonly',
globalThis: 'readonly',
navigation: 'readonly',
},
};

18
.github/ISSUE_TEMPLATE/19.md vendored Normal file
View File

@@ -0,0 +1,18 @@
---
name: "⚛React 19 beta issue"
about: Report a issue with React 19 beta.
title: '[React 19]'
labels: 'React 19'
---
## Summary
<!--
Please provide a CodeSandbox (https://codesandbox.io/s/new), a link to a
repository on GitHub, or provide a minimal code example that reproduces the
problem. You may provide a screenshot of the application if you think it is
relevant to your bug report. Here are some tips for providing a minimal
example: https://stackoverflow.com/help/mcve.
-->

View File

@@ -10,21 +10,7 @@ on:
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

View File

@@ -57,6 +57,8 @@ jobs:
key: playwright-browsers-v6-${{ runner.arch }}-${{ runner.os }}-${{ steps.playwright_version.outputs.playwright_version }}
- run: npx playwright install --with-deps chromium
if: steps.cache_playwright_browsers.outputs.cache-hit != 'true'
- run: npx playwright install-deps
if: steps.cache_playwright_browsers.outputs.cache-hit == 'true'
- run: CI=true yarn test
- run: ls -R test-results
if: '!cancelled()'

View File

@@ -16,12 +16,6 @@ on:
version_name:
required: true
type: string
tag_version:
required: false
type: string
dry_run:
required: false
type: boolean
secrets:
NPM_TOKEN:
required: true
@@ -58,13 +52,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)
- name: Publish packages to npm
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
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) || '' }}
scripts/release/publish.js --frfr --ci --versionName=${{ inputs.version_name }} --tag ${{ inputs.dist_tag }}

View File

@@ -14,12 +14,6 @@ on:
version_name:
required: true
type: string
tag_version:
required: false
type: string
dry_run:
required: false
type: boolean
permissions: {}
@@ -35,7 +29,5 @@ jobs:
release_channel: ${{ inputs.release_channel }}
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

@@ -0,0 +1,23 @@
name: (Compiler) Publish Prereleases Weekly
on:
schedule:
# At 10 minutes past 9:00 on Mon
- cron: 10 9 * * 1
permissions: {}
env:
TZ: /usr/share/zoneinfo/America/Los_Angeles
jobs:
publish_prerelease_beta:
name: Publish to beta channel
uses: facebook/react/.github/workflows/compiler_prereleases.yml@main
with:
commit_sha: ${{ github.sha }}
release_channel: beta
dist_tag: beta
version_name: '19.0.0'
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

@@ -6,12 +6,6 @@ on:
pull_request:
paths-ignore:
- compiler/**
workflow_dispatch:
inputs:
commit_sha:
required: false
type: string
default: ''
permissions: {}
@@ -34,7 +28,7 @@ jobs:
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 }}
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- name: Check cache hit
uses: actions/cache/restore@v4
id: node_modules
@@ -75,7 +69,7 @@ jobs:
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 }}
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- name: Check cache hit
uses: actions/cache/restore@v4
id: node_modules
@@ -123,7 +117,7 @@ jobs:
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 }}
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- uses: actions/github-script@v7
id: set-matrix
with:
@@ -142,7 +136,7 @@ jobs:
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 }}
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
@@ -172,7 +166,7 @@ jobs:
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 }}
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
@@ -194,7 +188,7 @@ jobs:
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:
@@ -204,7 +198,7 @@ jobs:
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 }}
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
@@ -260,7 +254,7 @@ jobs:
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 }}
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
@@ -286,37 +280,6 @@ jobs:
if: steps.node_modules.outputs.cache-hit != 'true'
- run: yarn test ${{ matrix.params }} --ci --shard=${{ matrix.shard }}
# Hardcoded to improve parallelism
test-linter:
name: Test eslint-plugin-react-hooks
needs: [runtime_compiler_node_modules_cache]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: yarn
cache-dependency-path: |
yarn.lock
compiler/yarn.lock
- name: Restore cached node_modules
uses: actions/cache@v4
id: node_modules
with:
path: |
**/node_modules
key: runtime-and-compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }}
- name: Install runtime dependencies
run: yarn install --frozen-lockfile
if: steps.node_modules.outputs.cache-hit != 'true'
- name: Install compiler dependencies
run: yarn install --frozen-lockfile
working-directory: compiler
if: steps.node_modules.outputs.cache-hit != 'true'
- run: ./scripts/react-compiler/build-compiler.sh && ./scripts/react-compiler/link-compiler.sh
- run: yarn workspace eslint-plugin-react-hooks test
# ----- BUILD -----
build_and_lint:
name: yarn build and lint
@@ -331,7 +294,7 @@ jobs:
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 }}
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
@@ -426,7 +389,7 @@ jobs:
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 }}
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
@@ -471,7 +434,7 @@ jobs:
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 }}
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
@@ -499,7 +462,7 @@ jobs:
merge-multiple: true
- name: Display structure of build
run: ls -R build
- run: echo ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }} >> build/COMMIT_SHA
- run: echo ${{ github.event.pull_request.head.sha || github.sha }} >> build/COMMIT_SHA
- name: Scrape warning messages
run: |
mkdir -p ./build/__test_utils__
@@ -536,7 +499,7 @@ jobs:
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 }}
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
@@ -567,7 +530,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
@@ -576,7 +539,7 @@ jobs:
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 }}
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
@@ -613,7 +576,7 @@ jobs:
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 }}
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
@@ -654,7 +617,7 @@ jobs:
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 }}
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
@@ -728,7 +691,7 @@ jobs:
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 }}
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
@@ -766,11 +729,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
@@ -781,7 +739,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
@@ -790,7 +748,7 @@ jobs:
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 }}
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
@@ -816,27 +774,12 @@ jobs:
pattern: _build_*
path: build
merge-multiple: true
- name: Check Playwright version
id: playwright_version
run: echo "playwright_version=$(npm ls @playwright/test | grep @playwright | sed 's/.*@//' | head -1)" >> "$GITHUB_OUTPUT"
- name: Cache Playwright Browsers for version ${{ steps.playwright_version.outputs.playwright_version }}
id: cache_playwright_browsers
uses: actions/cache@v4
with:
path: ~/.cache/ms-playwright
key: playwright-browsers-v6-${{ runner.arch }}-${{ runner.os }}-${{ steps.playwright_version.outputs.playwright_version }}
- name: Playwright install deps
if: steps.cache_playwright_browsers.outputs.cache-hit != 'true'
run: npx playwright install --with-deps chromium
- run: |
npx playwright install
sudo npx playwright install-deps
- 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:
@@ -850,7 +793,7 @@ jobs:
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 }}
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
@@ -899,7 +842,7 @@ jobs:
node ./scripts/print-warnings/print-warnings.js > build/__test_utils__/ReactAllWarnings.js
- name: Display structure of build for PR
run: ls -R build
- run: echo ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }} >> build/COMMIT_SHA
- run: echo ${{ github.event.pull_request.head.sha || github.sha }} >> build/COMMIT_SHA
- run: node ./scripts/tasks/danger
- name: Archive sizebot results
uses: actions/upload-artifact@v4

View File

@@ -132,9 +132,9 @@ jobs:
mkdir ./compiled/facebook-www/__test_utils__
mv build/__test_utils__/ReactAllWarnings.js ./compiled/facebook-www/__test_utils__/ReactAllWarnings.js
# Copy eslint-plugin-react-hooks
# Move eslint-plugin-react-hooks into eslint-plugin-react-hooks
mkdir ./compiled/eslint-plugin-react-hooks
cp build/oss-experimental/eslint-plugin-react-hooks/cjs/eslint-plugin-react-hooks.development.js \
mv build/oss-experimental/eslint-plugin-react-hooks/cjs/eslint-plugin-react-hooks.development.js \
./compiled/eslint-plugin-react-hooks/index.js
# Move unstable_server-external-runtime.js into facebook-www
@@ -167,12 +167,6 @@ jobs:
rm $RENDERER_FOLDER/ReactFabric-{dev,prod,profiling}.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
# including package.json to include dependencies in fbsource.
mkdir "$BASE_FOLDER/tools"
cp -r build/oss-experimental/eslint-plugin-react-hooks "$BASE_FOLDER/tools"
# Move React Native version file
mv build/facebook-react-native/VERSION_NATIVE_FB ./compiled-rn/VERSION_NATIVE_FB
@@ -332,10 +326,10 @@ jobs:
git --no-pager diff -U0 --cached | grep '^[+-]' | head -n 100
echo "===================="
# Ignore REVISION or lines removing @generated headers.
if git diff --cached ':(exclude)*REVISION' ':(exclude)*/eslint-plugin-react-hooks/package.json' | grep -vE "^(@@|diff|index|\-\-\-|\+\+\+|\- \* @generated SignedSource)" | grep "^[+-]" > /dev/null; then
if git diff --cached ':(exclude)*REVISION' | grep -vE "^(@@|diff|index|\-\-\-|\+\+\+|\- \* @generated SignedSource)" | grep "^[+-]" > /dev/null; then
echo "Changes detected"
echo "===== Changes ====="
git --no-pager diff --cached ':(exclude)*REVISION' ':(exclude)*/eslint-plugin-react-hooks/package.json' | grep -vE "^(@@|diff|index|\-\-\-|\+\+\+|\- \* @generated SignedSource)" | grep "^[+-]" | head -n 50
git --no-pager diff --cached ':(exclude)*REVISION' | grep -vE "^(@@|diff|index|\-\-\-|\+\+\+|\- \* @generated SignedSource)" | grep "^[+-]" | head -n 50
echo "==================="
echo "should_commit=true" >> "$GITHUB_OUTPUT"
else

View File

@@ -4,29 +4,13 @@ on:
pull_request_target:
types: [opened, ready_for_review]
paths-ignore:
- packages/react-devtools**
- compiler/**
- .github/workflows/compiler_**.yml
- .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

View File

@@ -13,25 +13,7 @@ on:
dist_tag:
required: true
type: string
enableFailureNotification:
description: 'Whether to notify the team on Discord when the release fails. Useful if this workflow is called from an automation.'
required: false
type: boolean
only_packages:
description: Packages to publish (space separated)
type: string
skip_packages:
description: Packages to NOT publish (space separated)
type: string
dry:
required: true
description: Dry run instead of publish?
type: boolean
default: true
secrets:
DISCORD_WEBHOOK_URL:
description: 'Discord webhook URL to notify on failure. Only required if enableFailureNotification is true.'
required: false
GH_TOKEN:
required: true
NPM_TOKEN:
@@ -72,39 +54,7 @@ jobs:
if: steps.node_modules.outputs.cache-hit != 'true'
- run: yarn --cwd scripts/release install --frozen-lockfile
if: steps.node_modules.outputs.cache-hit != 'true'
- run: cp ./scripts/release/ci-npmrc ~/.npmrc
- run: |
GH_TOKEN=${{ secrets.GH_TOKEN }} scripts/release/prepare-release-from-ci.js --skipTests -r ${{ inputs.release_channel }} --commit=${{ inputs.commit_sha }}
- name: Check prepared files
run: ls -R build/node_modules
- if: '${{ inputs.only_packages }}'
name: 'Publish ${{ inputs.only_packages }}'
run: |
scripts/release/publish.js \
--ci \
--tags=${{ inputs.dist_tag }} \
--onlyPackages=${{ inputs.only_packages }} ${{ (inputs.dry && '') || '\'}}
${{ inputs.dry && '--dry' || '' }}
- if: '${{ inputs.skip_packages }}'
name: 'Publish all packages EXCEPT ${{ inputs.skip_packages }}'
run: |
scripts/release/publish.js \
--ci \
--tags=${{ inputs.dist_tag }} \
--skipPackages=${{ inputs.skip_packages }} ${{ (inputs.dry && '') || '\'}}
${{ inputs.dry && '--dry' || '' }}
- if: '${{ !inputs.skip_packages && !inputs.only_packages }}'
name: 'Publish all packages'
run: |
scripts/release/publish.js \
--ci \
--tags=${{ inputs.dist_tag }} ${{ (inputs.dry && '') || '\'}}
${{ inputs.dry && '--dry' || '' }}
- name: Notify Discord on failure
if: failure() && inputs.enableFailureNotification == true
uses: tsickert/discord-webhook@86dc739f3f165f16dadc5666051c367efa1692f4
with:
webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL }}
embed-author-name: "GitHub Actions"
embed-title: '[Runtime] Publish of ${{ inputs.release_channel }}@${{ inputs.dist_tag}} release failed'
embed-url: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}/attempts/${{ github.run_attempt }}
cp ./scripts/release/ci-npmrc ~/.npmrc
scripts/release/publish.js --ci --tags ${{ inputs.dist_tag }}

View File

@@ -5,25 +5,6 @@ on:
inputs:
prerelease_commit_sha:
required: true
only_packages:
description: Packages to publish (space separated)
type: string
skip_packages:
description: Packages to NOT publish (space separated)
type: string
dry:
required: true
description: Dry run instead of publish?
type: boolean
default: true
experimental_only:
type: boolean
description: Only publish to the experimental tag
default: false
force_notify:
description: Force a Discord notification?
type: boolean
default: false
permissions: {}
@@ -31,26 +12,8 @@ env:
TZ: /usr/share/zoneinfo/America/Los_Angeles
jobs:
notify:
if: ${{ inputs.force_notify || inputs.dry == false || inputs.dry == 'false' }}
runs-on: ubuntu-latest
steps:
- name: Discord Webhook Action
uses: tsickert/discord-webhook@86dc739f3f165f16dadc5666051c367efa1692f4
with:
webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL }}
embed-author-name: ${{ github.event.sender.login }}
embed-author-url: ${{ github.event.sender.html_url }}
embed-author-icon-url: ${{ github.event.sender.avatar_url }}
embed-title: "⚠️ Publishing ${{ inputs.experimental_only && 'EXPERIMENTAL' || 'CANARY & EXPERIMENTAL' }} release ${{ (inputs.dry && ' (dry run)') || '' }}"
embed-description: |
```json
${{ toJson(inputs) }}
```
embed-url: https://github.com/facebook/react/actions/runs/${{ github.run_id }}
publish_prerelease_canary:
if: ${{ !inputs.experimental_only }}
name: Publish to Canary channel
uses: facebook/react/.github/workflows/runtime_prereleases.yml@main
permissions:
@@ -70,9 +33,6 @@ jobs:
# downstream consumers might still expect that tag. We can remove this
# after some time has elapsed and the change has been communicated.
dist_tag: canary,next
only_packages: ${{ inputs.only_packages }}
skip_packages: ${{ inputs.skip_packages }}
dry: ${{ inputs.dry }}
secrets:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -88,15 +48,10 @@ jobs:
# different versions of the same package, even if they use different
# dist tags.
needs: publish_prerelease_canary
# Ensures the job runs even if canary is skipped
if: always()
with:
commit_sha: ${{ inputs.prerelease_commit_sha }}
release_channel: experimental
dist_tag: experimental
only_packages: ${{ inputs.only_packages }}
skip_packages: ${{ inputs.skip_packages }}
dry: ${{ inputs.dry }}
secrets:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -21,10 +21,7 @@ jobs:
commit_sha: ${{ github.sha }}
release_channel: stable
dist_tag: canary,next
enableFailureNotification: true
dry: false
secrets:
DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -43,9 +40,6 @@ jobs:
commit_sha: ${{ github.sha }}
release_channel: experimental
dist_tag: experimental
enableFailureNotification: true
dry: false
secrets:
DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -110,7 +110,7 @@ jobs:
--tags=${{ inputs.tags }} \
--publishVersion=${{ inputs.version_to_publish }} \
--onlyPackages=${{ inputs.only_packages }} ${{ (inputs.dry && '') || '\'}}
${{ inputs.dry && '--dry' || '' }}
${{ inputs.dry && '--dry'}}
- if: '${{ inputs.skip_packages }}'
name: 'Publish all packages EXCEPT ${{ inputs.skip_packages }}'
run: |
@@ -119,7 +119,7 @@ jobs:
--tags=${{ inputs.tags }} \
--publishVersion=${{ inputs.version_to_publish }} \
--skipPackages=${{ inputs.skip_packages }} ${{ (inputs.dry && '') || '\'}}
${{ inputs.dry && '--dry' || '' }}
${{ inputs.dry && '--dry'}}
- name: Archive released package for debugging
uses: actions/upload-artifact@v4
with:

View File

@@ -18,7 +18,6 @@ jobs:
permissions:
# Used to create a review and close PRs
pull-requests: write
contents: write
steps:
- name: Close PR
uses: actions/github-script@v7

View File

@@ -2,7 +2,6 @@ name: (Shared) Label Core Team PRs
on:
pull_request_target:
types: [opened]
permissions: {}
@@ -12,20 +11,7 @@ env:
SEGMENT_DOWNLOAD_TIMEOUT_MINS: 1
jobs:
check_access:
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

View File

@@ -6,10 +6,7 @@ on:
- cron: '0 * * * *'
workflow_dispatch:
permissions:
# https://github.com/actions/stale/tree/v9/?tab=readme-ov-file#recommended-permissions
issues: write
pull-requests: write
permissions: {}
env:
TZ: /usr/share/zoneinfo/America/Los_Angeles

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

2
.nvmrc
View File

@@ -1 +1 @@
v20.19.0
v18.20.1

View File

@@ -3,12 +3,13 @@
const {esNextPaths} = require('./scripts/shared/pathsByLanguageVersion');
module.exports = {
plugins: ['prettier-plugin-hermes-parser'],
bracketSpacing: false,
singleQuote: true,
bracketSameLine: true,
trailingComma: 'es5',
printWidth: 80,
parser: 'flow',
parser: 'hermes',
arrowParens: 'avoid',
overrides: [
{

18
CHANGELOG-canary.md Normal file
View File

@@ -0,0 +1,18 @@
## March 22, 2024 (18.3.0-canary-670811593-20240322)
## React
- Added `useActionState` to replace `useFormState` and added `pending` value ([#28491](https://github.com/facebook/react/pull/28491)).
## October 5, 2023 (18.3.0-canary-546178f91-20231005)
### React
- Added support for async functions to be passed to `startTransition`.
- `useTransition` now triggers the nearest error boundary instead of a global error.
- Added `useOptimistic`, a new Hook for handling optimistic UI updates. It optimistically updates the UI before receiving confirmation from a server or external source.
### React DOM
- Added support for passing async functions to the `action` prop on `<form>`. When the function passed to `action` is marked with [`'use server'`](https://react.dev/reference/react/use-server), the form is [progressively enhanced](https://developer.mozilla.org/en-US/docs/Glossary/Progressive_Enhancement).
- Added `useFormStatus`, a new Hook for checking the submission state of a form.
- Added `useFormState`, a new Hook for updating state upon form submission. When the function passed to `useFormState` is marked with [`'use server'`](https://react.dev/reference/react/use-server), the update is [progressively enhanced](https://developer.mozilla.org/en-US/docs/Glossary/Progressive_Enhancement).

View File

@@ -1,128 +1,3 @@
## 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/developer-tooling/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.1 (July 28, 2025)
### React
* Fixed Owner Stacks to work with ES2015 function.name semantics ([#33680](https://github.com/facebook/react/pull/33680) by @hoxyq)
## 19.1.0 (March 28, 2025)
### Owner Stack
An Owner Stack is a string representing the components that are directly responsible for rendering a particular component. You can log Owner Stacks when debugging or use Owner Stacks to enhance error overlays or other development tools. Owner Stacks are only available in development builds. Component Stacks in production are unchanged.
* An Owner Stack is a development-only stack trace that helps identify which components are responsible for rendering a particular component. An Owner Stack is distinct from a Component Stacks, which shows the hierarchy of components leading to an error.
* The [captureOwnerStack API](https://react.dev/reference/react/captureOwnerStack) is only available in development mode and returns a Owner Stack, if available. The API can be used to enhance error overlays or log component relationships when debugging. [#29923](https://github.com/facebook/react/pull/29923), [#32353](https://github.com/facebook/react/pull/32353), [#30306](https://github.com/facebook/react/pull/30306),
[#32538](https://github.com/facebook/react/pull/32538), [#32529](https://github.com/facebook/react/pull/32529), [#32538](https://github.com/facebook/react/pull/32538)
### React
* Enhanced support for Suspense boundaries to be used anywhere, including the client, server, and during hydration. [#32069](https://github.com/facebook/react/pull/32069), [#32163](https://github.com/facebook/react/pull/32163), [#32224](https://github.com/facebook/react/pull/32224), [#32252](https://github.com/facebook/react/pull/32252)
* Reduced unnecessary client rendering through improved hydration scheduling [#31751](https://github.com/facebook/react/pull/31751)
* Increased priority of client rendered Suspense boundaries [#31776](https://github.com/facebook/react/pull/31776)
* Fixed frozen fallback states by rendering unfinished Suspense boundaries on the client. [#31620](https://github.com/facebook/react/pull/31620)
* Reduced garbage collection pressure by improving Suspense boundary retries. [#31667](https://github.com/facebook/react/pull/31667)
* Fixed erroneous “Waiting for Paint” log when the passive effect phase was not delayed [#31526](https://github.com/facebook/react/pull/31526)
* Fixed a regression causing key warnings for flattened positional children in development mode. [#32117](https://github.com/facebook/react/pull/32117)
* Updated `useId` to use valid CSS selectors, changing format from `:r123:` to `«r123»`. [#32001](https://github.com/facebook/react/pull/32001)
* Added a dev-only warning for null/undefined created in useEffect, useInsertionEffect, and useLayoutEffect. [#32355](https://github.com/facebook/react/pull/32355)
* Fixed a bug where dev-only methods were exported in production builds. React.act is no longer available in production builds. [#32200](https://github.com/facebook/react/pull/32200)
* Improved consistency across prod and dev to improve compatibility with Google Closure Compiler and bindings [#31808](https://github.com/facebook/react/pull/31808)
* Improve passive effect scheduling for consistent task yielding. [#31785](https://github.com/facebook/react/pull/31785)
* Fixed asserts in React Native when passChildrenWhenCloningPersistedNodes is enabled for OffscreenComponent rendering. [#32528](https://github.com/facebook/react/pull/32528)
* Fixed component name resolution for Portal [#32640](https://github.com/facebook/react/pull/32640)
* Added support for beforetoggle and toggle events on the dialog element. [#32479](https://github.com/facebook/react/pull/32479)
### React DOM
* Fixed double warning when the `href` attribute is an empty string [#31783](https://github.com/facebook/react/pull/31783)
* Fixed an edge case where `getHoistableRoot()` didnt work properly when the container was a Document [#32321](https://github.com/facebook/react/pull/32321)
* Removed support for using HTML comments (e.g. `<!-- -->`) as a DOM container. [#32250](https://github.com/facebook/react/pull/32250)
* Added support for `<script>` and `<template>` tags to be nested within `<select>` tags. [#31837](https://github.com/facebook/react/pull/31837)
* Fixed responsive images to be preloaded as HTML instead of headers [#32445](https://github.com/facebook/react/pull/32445)
### use-sync-external-store
* Added `exports` field to `package.json` for `use-sync-external-store` to support various entrypoints. [#25231](https://github.com/facebook/react/pull/25231)
### React Server Components
* Added `unstable_prerender`, a new experimental API for prerendering React Server Components on the server [#31724](https://github.com/facebook/react/pull/31724)
* Fixed an issue where streams would hang when receiving new chunks after a global error [#31840](https://github.com/facebook/react/pull/31840), [#31851](https://github.com/facebook/react/pull/31851)
* Fixed an issue where pending chunks were counted twice. [#31833](https://github.com/facebook/react/pull/31833)
* Added support for streaming in edge environments [#31852](https://github.com/facebook/react/pull/31852)
* Added support for sending custom error names from a server so that they are available in the client for console replaying. [#32116](https://github.com/facebook/react/pull/32116)
* Updated the server component wire format to remove IDs for hints and console.log because they have no return value [#31671](https://github.com/facebook/react/pull/31671)
* 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.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.1.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.0.0',
'jest-react': '0.18.0',
'eslint-plugin-react-hooks': '5.2.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.32.0',
'react-refresh': '0.17.0',
'react-test-renderer': ReactVersion,
'use-subscription': '1.13.0',
'use-sync-external-store': '1.7.0',
scheduler: '0.28.0',
'use-subscription': '1.11.0',
'use-sync-external-store': '1.5.0',
scheduler: '0.26.0',
};
// These packages do not exist in the @canary or @latest channel, only

View File

@@ -8,7 +8,6 @@ module.exports = {
'@babel/plugin-syntax-jsx',
'@babel/plugin-transform-flow-strip-types',
['@babel/plugin-transform-class-properties', {loose: true}],
['@babel/plugin-transform-private-methods', {loose: true}],
'@babel/plugin-transform-classes',
],
presets: [

14
compiler/.gitignore vendored
View File

@@ -1,14 +1,28 @@
.DS_Store
.spr.yml
# Generated by Cargo
# will have compiled files and executables
debug/
target/
# These are backup files generated by rustfmt
**/*.rs.bk
# MSVC Windows builds of rustc generate these, which store debugging information
*.pdb
node_modules
.watchmanconfig
.watchman-cookie-*
dist
.vscode
!packages/playground/.vscode
.spr.yml
testfilter.txt
bundle-oss.sh
# forgive
*.vsix
.vscode-test

View File

@@ -1,65 +0,0 @@
## 19.1.0-rc.2 (May 14, 2025)
## babel-plugin-react-compiler
* Fix for string attribute values with emoji [#33096](https://github.com/facebook/react/pull/33096) by [@josephsavona](https://github.com/josephsavona)
## 19.1.0-rc.1 (April 21, 2025)
## eslint-plugin-react-hooks
* Temporarily disable ref access in render validation [#32839](https://github.com/facebook/react/pull/32839) by [@poteto](https://github.com/poteto)
* Fix type error with recommended config [#32666](https://github.com/facebook/react/pull/32666) by [@niklasholm](https://github.com/niklasholm)
* Merge rule from eslint-plugin-react-compiler into `react-hooks` plugin [#32416](https://github.com/facebook/react/pull/32416) by [@michaelfaith](https://github.com/michaelfaith)
* Add dev dependencies for typescript migration [#32279](https://github.com/facebook/react/pull/32279) by [@michaelfaith](https://github.com/michaelfaith)
* Support v9 context api [#32045](https://github.com/facebook/react/pull/32045) by [@michaelfaith](https://github.com/michaelfaith)
* Support eslint 8+ flat plugin syntax out of the box for eslint-plugin-react-compiler [#32120](https://github.com/facebook/react/pull/32120) by [@orta](https://github.com/orta)
## babel-plugin-react-compiler
* Support satisfies operator [#32742](https://github.com/facebook/react/pull/32742) by [@rodrigofariow](https://github.com/rodrigofariow)
* Fix inferEffectDependencies lint false positives [#32769](https://github.com/facebook/react/pull/32769) by [@mofeiZ](https://github.com/mofeiZ)
* Fix hoisting of let declarations [#32724](https://github.com/facebook/react/pull/32724) by [@mofeiZ](https://github.com/mofeiZ)
* Avoid failing builds when import specifiers conflict or shadow vars [#32663](https://github.com/facebook/react/pull/32663) by [@mofeiZ](https://github.com/mofeiZ)
* Optimize components declared with arrow function and implicit return and `compilationMode: 'infer'` [#31792](https://github.com/facebook/react/pull/31792) by [@dimaMachina](https://github.com/dimaMachina)
* Validate static components [#32683](https://github.com/facebook/react/pull/32683) by [@josephsavona](https://github.com/josephsavona)
* Hoist dependencies from functions more conservatively [#32616](https://github.com/facebook/react/pull/32616) by [@mofeiZ](https://github.com/mofeiZ)
* Implement NumericLiteral as ObjectPropertyKey [#31791](https://github.com/facebook/react/pull/31791) by [@dimaMachina](https://github.com/dimaMachina)
* Avoid bailouts when inserting gating [#32598](https://github.com/facebook/react/pull/32598) by [@mofeiZ](https://github.com/mofeiZ)
* Stop bailing out early for hoisted gated functions [#32597](https://github.com/facebook/react/pull/32597) by [@mofeiZ](https://github.com/mofeiZ)
* Add shape for Array.from [#32522](https://github.com/facebook/react/pull/32522) by [@mofeiZ](https://github.com/mofeiZ)
* Patch array and argument spread mutability [#32521](https://github.com/facebook/react/pull/32521) by [@mofeiZ](https://github.com/mofeiZ)
* Make CompilerError compatible with reflection [#32539](https://github.com/facebook/react/pull/32539) by [@poteto](https://github.com/poteto)
* Add simple walltime measurement [#32331](https://github.com/facebook/react/pull/32331) by [@poteto](https://github.com/poteto)
* Improve error messages for unhandled terminal and instruction kinds [#32324](https://github.com/facebook/react/pull/32324) by [@inottn](https://github.com/inottn)
* Handle TSInstantiationExpression in lowerExpression [#32302](https://github.com/facebook/react/pull/32302) by [@inottn](https://github.com/inottn)
* Fix invalid Array.map type [#32095](https://github.com/facebook/react/pull/32095) by [@mofeiZ](https://github.com/mofeiZ)
* Patch for JSX escape sequences in @babel/generator [#32131](https://github.com/facebook/react/pull/32131) by [@mofeiZ](https://github.com/mofeiZ)
* `JSXText` emits incorrect with bracket [#32138](https://github.com/facebook/react/pull/32138) by [@himself65](https://github.com/himself65)
* Validation against calling impure functions [#31960](https://github.com/facebook/react/pull/31960) by [@josephsavona](https://github.com/josephsavona)
* Always target node [#32091](https://github.com/facebook/react/pull/32091) by [@poteto](https://github.com/poteto)
* Patch compilationMode:infer object method edge case [#32055](https://github.com/facebook/react/pull/32055) by [@mofeiZ](https://github.com/mofeiZ)
* Generate ts defs [#31994](https://github.com/facebook/react/pull/31994) by [@poteto](https://github.com/poteto)
* Relax react peer dep requirement [#31915](https://github.com/facebook/react/pull/31915) by [@poteto](https://github.com/poteto)
* Allow type cast expressions with refs [#31871](https://github.com/facebook/react/pull/31871) by [@josephsavona](https://github.com/josephsavona)
* Add shape for global Object.keys [#31583](https://github.com/facebook/react/pull/31583) by [@mofeiZ](https://github.com/mofeiZ)
* Optimize method calls w props receiver [#31775](https://github.com/facebook/react/pull/31775) by [@josephsavona](https://github.com/josephsavona)
* Fix dropped ref with spread props in InlineJsxTransform [#31726](https://github.com/facebook/react/pull/31726) by [@jackpope](https://github.com/jackpope)
* Support for non-declatation for in/of iterators [#31710](https://github.com/facebook/react/pull/31710) by [@mvitousek](https://github.com/mvitousek)
* Support for context variable loop iterators [#31709](https://github.com/facebook/react/pull/31709) by [@mvitousek](https://github.com/mvitousek)
* Replace deprecated dependency in `eslint-plugin-react-compiler` [#31629](https://github.com/facebook/react/pull/31629) by [@rakleed](https://github.com/rakleed)
* Support enableRefAsProp in jsx transform [#31558](https://github.com/facebook/react/pull/31558) by [@jackpope](https://github.com/jackpope)
* Fix: ref.current now correctly reactive [#31521](https://github.com/facebook/react/pull/31521) by [@mofeiZ](https://github.com/mofeiZ)
* Outline JSX with non-jsx children [#31442](https://github.com/facebook/react/pull/31442) by [@gsathya](https://github.com/gsathya)
* Outline jsx with duplicate attributes [#31441](https://github.com/facebook/react/pull/31441) by [@gsathya](https://github.com/gsathya)
* Store original and new prop names [#31440](https://github.com/facebook/react/pull/31440) by [@gsathya](https://github.com/gsathya)
* Stabilize compiler output: sort deps and decls by name [#31362](https://github.com/facebook/react/pull/31362) by [@mofeiZ](https://github.com/mofeiZ)
* Bugfix for hoistable deps for nested functions [#31345](https://github.com/facebook/react/pull/31345) by [@mofeiZ](https://github.com/mofeiZ)
* Remove compiler runtime-compat fixture library [#31430](https://github.com/facebook/react/pull/31430) by [@poteto](https://github.com/poteto)
* Wrap inline jsx transform codegen in conditional [#31267](https://github.com/facebook/react/pull/31267) by [@jackpope](https://github.com/jackpope)
* Check if local identifier is a hook when resolving globals [#31384](https://github.com/facebook/react/pull/31384) by [@poteto](https://github.com/poteto)
* Handle member expr as computed property [#31344](https://github.com/facebook/react/pull/31344) by [@gsathya](https://github.com/gsathya)
* Fix to ref access check to ban ref?.current [#31360](https://github.com/facebook/react/pull/31360) by [@mvitousek](https://github.com/mvitousek)
* InlineJSXTransform transforms jsx inside function expressions [#31282](https://github.com/facebook/react/pull/31282) by [@josephsavona](https://github.com/josephsavona)
## Other
* Add shebang to banner [#32225](https://github.com/facebook/react/pull/32225) by [@Jeremy-Hibiki](https://github.com/Jeremy-Hibiki)
* remove terser from react-compiler-runtime build [#31326](https://github.com/facebook/react/pull/31326) by [@henryqdineen](https://github.com/henryqdineen)

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,4 +1,4 @@
// @compilationMode:"infer"
// @compilationMode(infer)
function nonReactFn() {
  return {};
}

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',
@@ -103,7 +92,7 @@ function useFoo(propVal: {+baz: number}) {
},
{
name: 'compilationMode-infer',
input: `// @compilationMode:"infer"
input: `// @compilationMode(infer)
function nonReactFn() {
return {};
}
@@ -112,7 +101,7 @@ function nonReactFn() {
},
{
name: 'compilationMode-all',
input: `// @compilationMode:"all"
input: `// @compilationMode(all)
function nonReactFn() {
return {};
}
@@ -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

@@ -1,215 +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 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 {Resizable} from 're-resizable';
import {useStore, useStoreDispatch} from '../StoreContext';
import {monacoConfigOptions} from './monacoOptions';
import {IconChevron} from '../Icons/IconChevron';
import {CONFIG_PANEL_TRANSITION} from '../../lib/transitionTypes';
// @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 {
const [isExpanded, setIsExpanded] = useState(false);
return (
// TODO: Use <Activity> when it is compatible with Monaco: https://github.com/suren-atoyan/monaco-react/issues/753
<>
<div
style={{
display: isExpanded ? 'block' : 'none',
}}>
<ExpandedEditor
onToggle={() => {
startTransition(() => {
addTransitionType(CONFIG_PANEL_TRANSITION);
setIsExpanded(false);
});
}}
formattedAppliedConfig={formattedAppliedConfig}
/>
</div>
<div
style={{
display: !isExpanded ? 'block' : 'none',
}}>
<CollapsedEditor
onToggle={() => {
startTransition(() => {
addTransitionType(CONFIG_PANEL_TRANSITION);
setIsExpanded(true);
});
}}
/>
</div>
</>
);
}
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 handleChange: (value: string | undefined) => void = (
value: string | undefined,
) => {
if (value === undefined) return;
if (debounceTimerRef.current) {
clearTimeout(debounceTimerRef.current);
}
debounceTimerRef.current = setTimeout(() => {
dispatchStore({
type: 'updateConfig',
payload: {
config: value,
},
});
}, 500); // 500ms debounce delay
};
const handleMount: (
_: editor.IStandaloneCodeEditor,
monaco: Monaco,
) => void = (_, monaco) => {
// Add the babel-plugin-react-compiler type definitions to Monaco
monaco.languages.typescript.typescriptDefaults.addExtraLib(
//@ts-expect-error - compilerTypeDefs is a string
compilerTypeDefs,
'file:///node_modules/babel-plugin-react-compiler/dist/index.d.ts',
);
monaco.languages.typescript.typescriptDefaults.setCompilerOptions({
target: monaco.languages.typescript.ScriptTarget.Latest,
allowNonTsExtensions: true,
moduleResolution: monaco.languages.typescript.ModuleResolutionKind.NodeJs,
module: monaco.languages.typescript.ModuleKind.ESNext,
noEmit: true,
strict: false,
esModuleInterop: true,
allowSyntheticDefaultImports: true,
jsx: monaco.languages.typescript.JsxEmit.React,
});
};
return (
<ViewTransition
update={{[CONFIG_PANEL_TRANSITION]: 'slide-in', 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">
<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,
}}
/>
</div>
</div>
</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,63 +5,289 @@
* 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,
ErrorSeverity,
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 ConfigEditor from './ConfigEditor';
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 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';
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): [CompilerOutput, 'flow' | 'typescript'] {
const results = new Map<string, Array<PrintedCompilerPipelineValue>>();
const error = new CompilerError();
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',
});
const opts: PluginOptions = parsePluginOptions({
...parsedOptions,
environment: {
...parsedOptions.environment,
customHooks: new Map([...COMMON_HOOKS]),
},
logger: {
debugLogIRs: logIR,
logEvent: () => {},
},
});
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.details.push(...err.details);
} else {
/**
* Handle unexpected failures by logging (to get a stack trace)
* and reporting
*/
console.error(err);
error.details.push(
new CompilerErrorDetail({
severity: ErrorSeverity.Invariant,
reason: `Unexpected failure when transforming input! ${err}`,
loc: null,
suggestions: null,
}),
);
}
}
if (error.hasErrors()) {
return [{kind: 'err', results, error: error}, language];
}
return [{kind: 'ok', results, transformOutput}, 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),
[deferredStore.source],
);
const [linterOutput] = useMemo(
() => compile(deferredStore.source, 'linter', deferredStore.config),
[deferredStore.source, deferredStore.config],
);
const [formattedAppliedConfig, setFormattedAppliedConfig] = useState('');
let mergedOutput: CompilerOutput;
let errors: Array<CompilerErrorDetail | CompilerDiagnostic>;
if (compilerOutput.kind === 'ok') {
errors = linterOutput.kind === 'ok' ? [] : linterOutput.error.details;
mergedOutput = {
...compilerOutput,
errors,
};
} else {
mergedOutput = compilerOutput;
errors = compilerOutput.error.details;
}
if (appliedOptions) {
const formatted = prettyFormat(appliedOptions, {
printFunctionName: false,
printBasicPrototype: false,
});
if (formatted !== formattedAppliedConfig) {
setFormattedAppliedConfig(formatted);
useMountEffect(() => {
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},
});
});
return (
<>
<div className="relative flex top-14">
<div className="flex-shrink-0">
<ConfigEditor formattedAppliedConfig={formattedAppliedConfig} />
<div className="relative flex basis top-14">
<div className={clsx('relative sm:basis-1/4')}>
<Input
language={language}
errors={
compilerOutput.kind === 'err' ? compilerOutput.error.details : []
}
/>
</div>
<div className="flex flex-1 min-w-0">
<Input language={language} errors={errors} />
<Output store={deferredStore} compilerOutput={mergedOutput} />
<div className={clsx('flex sm:flex flex-wrap')}>
<Output store={deferredStore} compilerOutput={compilerOutput} />
</div>
</div>
</>

View File

@@ -6,31 +6,22 @@
*/
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';
loader.config({monaco});
type Props = {
errors: Array<CompilerErrorDetail | CompilerDiagnostic>;
errors: Array<CompilerErrorDetail>;
language: 'flow' | 'typescript';
};
@@ -45,13 +36,13 @@ export default function Input({errors, language}: Props): JSX.Element {
const uri = monaco.Uri.parse(`file:///index.js`);
const model = monaco.editor.getModel(uri);
invariant(model, 'Model must exist for the selected input file.');
renderReactCompilerMarkers({
monaco,
model,
details: errors,
source: store.source,
});
}, [monaco, errors, store.source]);
renderReactCompilerMarkers({monaco, model, details: errors});
/**
* 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]);
useEffect(() => {
/**
@@ -83,11 +74,11 @@ export default function Input({errors, language}: Props): JSX.Element {
});
}, [monaco, language]);
const handleChange: (value: string | undefined) => void = async value => {
const handleChange: (value: string | undefined) => void = value => {
if (!value) return;
dispatchStore({
type: 'updateSource',
type: 'updateFile',
payload: {
source: value,
},
@@ -139,42 +130,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

@@ -11,46 +11,19 @@ import {
InformationCircleIcon,
} from '@heroicons/react/outline';
import MonacoEditor, {DiffEditor} from '@monaco-editor/react';
import {
CompilerErrorDetail,
CompilerDiagnostic,
type CompilerError,
} from 'babel-plugin-react-compiler';
import {type CompilerError} from 'babel-plugin-react-compiler';
import parserBabel from 'prettier/plugins/babel';
import * as prettierPluginEstree from 'prettier/plugins/estree';
import * as prettier from 'prettier/standalone';
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';
@@ -71,7 +44,6 @@ export type CompilerOutput =
kind: 'ok';
transformOutput: CompilerTransformOutput;
results: Map<string, Array<PrintedCompilerPipelineValue>>;
errors: Array<CompilerErrorDetail | CompilerDiagnostic>;
}
| {
kind: 'err';
@@ -87,16 +59,12 @@ type Props = {
async function tabify(
source: string,
compilerOutput: CompilerOutput,
showInternals: boolean,
): Promise<Map<string, ReactNode>> {
const tabs = new Map<string, React.ReactNode>();
const reorderedTabs = new Map<string, React.ReactNode>();
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)) {
continue;
}
for (const result of results) {
switch (result.kind) {
case 'hir': {
@@ -155,36 +123,10 @@ async function tabify(
parser: transformOutput.language === 'flow' ? 'babel-flow' : 'babel-ts',
plugins: [parserBabel, prettierPluginEstree],
});
let output: string;
let language: string;
if (compilerOutput.errors.length === 0) {
output = code;
language = 'javascript';
} else {
language = 'markdown';
output = `
# Summary
React Compiler compiled this function successfully, but there are lint errors that indicate potential issues with the original code.
## ${compilerOutput.errors.length} Lint Errors
${compilerOutput.errors.map(e => e.printErrorMessage(source, {eslint: false})).join('\n\n')}
## Output
\`\`\`js
${code}
\`\`\`
`.trim();
}
reorderedTabs.set(
'Output',
'JS',
<TextTabContent
output={output}
language={language}
output={code}
diff={null}
showInfoPanel={false}></TextTabContent>,
);
@@ -200,18 +142,6 @@ ${code}
</>,
);
}
} else if (compilerOutput.kind === 'err') {
const errors = compilerOutput.error.printErrorMessage(source, {
eslint: false,
});
reorderedTabs.set(
'Output',
<TextTabContent
output={errors}
language="markdown"
diff={null}
showInfoPanel={false}></TextTabContent>,
);
}
tabs.forEach((tab, name) => {
reorderedTabs.set(name, tab);
@@ -219,25 +149,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,40 +162,17 @@ 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>
const [tabsOpen, setTabsOpen] = useState<Set<string>>(() => new Set(['JS']));
const [tabs, setTabs] = useState<Map<string, React.ReactNode>>(
() => new Map(),
);
}
useEffect(() => {
tabify(store.source, compilerOutput).then(tabs => {
setTabs(tabs);
});
}, [store.source, compilerOutput]);
function OutputContent({store, compilerOutput}: Props): JSX.Element {
const [tabsOpen, setTabsOpen] = useState<Set<string>>(
() => new Set(['Output']),
);
const [activeTab, setActiveTab] = useState<string>('Output');
/*
* Update the active tab back to the output or errors tab when the compilation state
* changes between success/failure.
*/
const [previousOutputKind, setPreviousOutputKind] = useState(
compilerOutput.kind,
);
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');
});
}
}
const changedPasses: Set<string> = new Set(['Output', 'HIR']); // Initial and final passes should always be bold
const changedPasses: Set<string> = new Set(['JS', 'HIR']); // Initial and final passes should always be bold
let lastResult: string = '';
for (const [passName, results] of compilerOutput.results) {
for (const result of results) {
@@ -298,40 +186,31 @@ 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
defaultTab={store.showInternals ? 'HIR' : 'Output'}
<>
<TabbedWindow
defaultTab="HIR"
setTabsOpen={setTabsOpen}
tabsOpen={tabsOpen}
tabs={tabs}
changedPasses={changedPasses}
/>
</ViewTransition>
{compilerOutput.kind === 'err' ? (
<div
className="flex flex-wrap absolute bottom-0 bg-white grow border-y border-grey-200 transition-all ease-in"
style={{width: 'calc(100vw - 650px)'}}>
<div className="w-full p-4 basis-full border-b">
<h2>COMPILER ERRORS</h2>
</div>
<pre
className="p-4 basis-full text-red-600 overflow-y-scroll whitespace-pre-wrap"
style={{width: 'calc(100vw - 650px)', height: '150px'}}>
<code>{compilerOutput.error.toString()}</code>
</pre>
</div>
) : null}
</>
);
}
@@ -339,12 +218,10 @@ function TextTabContent({
output,
diff,
showInfoPanel,
language,
}: {
output: string;
diff: string | null;
showInfoPanel: boolean;
language: string;
}): JSX.Element {
const [diffMode, setDiffMode] = useState(false);
return (
@@ -383,29 +260,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'}
defaultLanguage="javascript"
value={output}
loading={''}
className="monaco-editor-output"
options={{
...monacoOptions,
readOnly: true,

View File

@@ -28,18 +28,5 @@ 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,
wrappingIndent: 'deepIndent',
};

View File

@@ -10,20 +10,14 @@ 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';
import {useStoreDispatch} from './StoreContext';
export default function Header(): JSX.Element {
const [showCheck, setShowCheck] = useState(false);
const store = useStore();
const dispatchStore = useStoreDispatch();
const {enqueueSnackbar, closeSnackbar} = useSnackbar();
@@ -62,32 +56,6 @@ export default function Header(): JSX.Element {
<p className="hidden select-none sm:block">React Compiler Playground</p>
</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">
<input
type="checkbox"
checked={store.showInternals}
onChange={() =>
startTransition(() => {
addTransitionType(TOGGLE_INTERNALS_TRANSITION);
dispatchStore({type: 'toggleInternals'});
})
}
className="absolute opacity-0 cursor-pointer h-full w-full m-0"
/>
<span
className={clsx(
'absolute inset-0 rounded-full cursor-pointer transition-all duration-250',
"before:content-[''] before:absolute before:w-4 before:h-4 before:left-0.5 before:bottom-0.5",
'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-gray-300',
)}></span>
</label>
<span className="text-secondary">Show Internals</span>
</div>
<button
title="Reset Playground"
aria-label="Reset Playground"

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,19 +53,10 @@ type ReducerAction =
};
}
| {
type: 'updateSource';
type: 'updateFile';
payload: {
source: string;
};
}
| {
type: 'updateConfig';
payload: {
config: string;
};
}
| {
type: 'toggleInternals';
};
function storeReducer(store: Store, action: ReducerAction): Store {
@@ -92,28 +65,13 @@ 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} = action.payload;
const newStore = {
...store,
source,
};
return newStore;
}
case 'updateConfig': {
const config = action.payload.config;
const newStore = {
...store,
config,
};
return newStore;
}
case 'toggleInternals': {
const newStore = {
...store,
showInternals: !store.showInternals,
};
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

@@ -13,21 +13,10 @@ export default function MyApp() {
}
`;
export const defaultConfig = `\
import type { PluginOptions } from 'babel-plugin-react-compiler/dist';
({
//compilationMode: "all"
} satisfies PluginOptions);`;
export const defaultStore: Store = {
source: index,
config: defaultConfig,
showInternals: false,
};
export const emptyStore: Store = {
source: '',
config: '',
showInternals: false,
};

View File

@@ -6,11 +6,7 @@
*/
import {Monaco} from '@monaco-editor/react';
import {
CompilerDiagnostic,
CompilerErrorDetail,
ErrorSeverity,
} from 'babel-plugin-react-compiler';
import {CompilerErrorDetail, ErrorSeverity} from 'babel-plugin-react-compiler';
import {MarkerSeverity, type editor} from 'monaco-editor';
function mapReactCompilerSeverityToMonaco(
@@ -26,46 +22,38 @@ function mapReactCompilerSeverityToMonaco(
}
function mapReactCompilerDiagnosticToMonacoMarker(
detail: CompilerErrorDetail | CompilerDiagnostic,
detail: CompilerErrorDetail,
monaco: Monaco,
source: string,
): editor.IMarkerData | null {
const loc = detail.primaryLocation();
if (loc == null || typeof loc === 'symbol') {
if (detail.loc == null || typeof detail.loc === 'symbol') {
return null;
}
const severity = mapReactCompilerSeverityToMonaco(detail.severity, monaco);
let message = detail.printErrorMessage(source, {eslint: true});
let message = detail.printErrorMessage();
return {
severity,
message,
startLineNumber: loc.start.line,
startColumn: loc.start.column + 1,
endLineNumber: loc.end.line,
endColumn: loc.end.column + 1,
startLineNumber: detail.loc.start.line,
startColumn: detail.loc.start.column + 1,
endLineNumber: detail.loc.end.line,
endColumn: detail.loc.end.column + 1,
};
}
type ReactCompilerMarkerConfig = {
monaco: Monaco;
model: editor.ITextModel;
details: Array<CompilerErrorDetail | CompilerDiagnostic>;
source: string;
details: Array<CompilerErrorDetail>;
};
let decorations: Array<string> = [];
export function renderReactCompilerMarkers({
monaco,
model,
details,
source,
}: ReactCompilerMarkerConfig): void {
const markers: Array<editor.IMarkerData> = [];
for (const detail of details) {
const marker = mapReactCompilerDiagnosticToMonacoMarker(
detail,
monaco,
source,
);
const marker = mapReactCompilerDiagnosticToMonacoMarker(detail, monaco);
if (marker == null) {
continue;
}

View File

@@ -10,20 +10,18 @@ import {
compressToEncodedURIComponent,
decompressFromEncodedURIComponent,
} from 'lz-string';
import {defaultStore, defaultConfig} from '../defaultStore';
import {defaultStore} from '../defaultStore';
/**
* Global Store for Playground
*/
export interface Store {
source: string;
config: string;
showInternals: boolean;
}
export function encodeStore(store: Store): string {
return compressToEncodedURIComponent(JSON.stringify(store));
}
export function decodeStore(hash: string): any {
export function decodeStore(hash: string): Store {
return JSON.parse(decompressFromEncodedURIComponent(hash));
}
@@ -64,14 +62,8 @@ export function initStoreFromUrlOrLocalStorage(): Store {
*/
if (!encodedSource) return defaultStore;
const raw: any = decodeStore(encodedSource);
const raw = decodeStore(encodedSource);
invariant(isValidStore(raw), 'Invalid Store');
// Make sure all properties are populated
return {
source: raw.source,
config: 'config' in raw && raw['config'] ? raw.config : defaultConfig,
showInternals: 'showInternals' in raw ? raw.showInternals : false,
};
return raw;
}

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

@@ -1,6 +1,5 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
import "./.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

@@ -27,38 +27,33 @@
"@babel/types": "7.26.3",
"@heroicons/react": "^1.0.6",
"@monaco-editor/react": "^4.4.6",
"@playwright/test": "^1.51.1",
"@playwright/test": "^1.42.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.6.0-canary.7",
"next": "^15.2.0-canary.64",
"notistack": "^3.0.0-alpha.7",
"prettier": "^3.3.3",
"pretty-format": "^29.3.1",
"re-resizable": "^6.9.16",
"react": "19.1.1",
"react-dom": "19.1.1"
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"devDependencies": {
"@types/node": "18.11.9",
"@types/react": "19.1.13",
"@types/react-dom": "19.1.9",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"autoprefixer": "^10.4.13",
"clsx": "^1.2.1",
"concurrently": "^7.4.0",
"eslint": "^8.28.0",
"eslint-config-next": "15.5.2",
"eslint-config-next": "^15.0.1",
"monaco-editor-webpack-plugin": "^7.1.0",
"postcss": "^8.4.31",
"tailwindcss": "^3.2.4",
"wait-on": "^7.2.0"
},
"resolutions": {
"@types/react": "19.1.12",
"@types/react-dom": "19.1.9"
}
}

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

@@ -8,8 +8,8 @@ set -eo pipefail
HERE=$(pwd)
cd ../../packages/react-compiler-runtime && yarn --silent link && cd "$HERE"
cd ../../packages/babel-plugin-react-compiler && yarn --silent link && cd "$HERE"
cd ../../packages/react-compiler-runtime && yarn --silent link && cd $HERE
cd ../../packages/babel-plugin-react-compiler && yarn --silent link && cd $HERE
yarn --silent link babel-plugin-react-compiler
yarn --silent link react-compiler-runtime

View File

@@ -69,66 +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;
}
@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,
@@ -19,7 +16,7 @@
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react-jsx",
"jsx": "preserve",
"incremental": true,
"plugins": [
{

File diff suppressed because it is too large Load Diff

View File

@@ -19,8 +19,7 @@
"test": "yarn workspaces run test",
"snap": "yarn workspace babel-plugin-react-compiler run snap",
"snap:build": "yarn workspace snap run build",
"npm:publish": "node scripts/release/publish",
"eslint-docs": "yarn workspace babel-plugin-react-compiler build && node scripts/build-eslint-docs.js"
"npm:publish": "node scripts/release/publish"
},
"dependencies": {
"fs-extra": "^4.0.2",
@@ -38,7 +37,7 @@
"prettier": "^3.3.3",
"prettier-plugin-hermes-parser": "^0.26.0",
"prompt-promise": "^1.0.3",
"rimraf": "^6.0.1",
"rimraf": "^5.0.10",
"to-fast-properties": "^2.0.0",
"tsup": "^8.4.0",
"typescript": "^5.4.3",
@@ -46,6 +45,7 @@
"yargs": "^17.7.2"
},
"resolutions": {
"rimraf": "5.0.10",
"@babel/types": "7.26.3"
},
"packageManager": "yarn@1.22.22"

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

@@ -12,7 +12,6 @@ import {
pipelineUsesReanimatedPlugin,
} from '../Entrypoint/Reanimated';
import validateNoUntransformedReferences from '../Entrypoint/ValidateNoUntransformedReferences';
import {CompilerError} from '..';
const ENABLE_REACT_COMPILER_TIMINGS =
process.env['ENABLE_REACT_COMPILER_TIMINGS'] === '1';
@@ -35,58 +34,51 @@ export default function BabelPluginReactCompiler(
*/
Program: {
enter(prog, pass): void {
try {
const filename = pass.filename ?? 'unknown';
if (ENABLE_REACT_COMPILER_TIMINGS === true) {
performance.mark(`${filename}:start`, {
detail: 'BabelPlugin:Program:start',
});
}
let opts = parsePluginOptions(pass.opts);
const isDev =
(typeof __DEV__ !== 'undefined' && __DEV__ === true) ||
process.env['NODE_ENV'] === 'development';
if (
opts.enableReanimatedCheck === true &&
pipelineUsesReanimatedPlugin(pass.file.opts.plugins)
) {
opts = injectReanimatedFlag(opts);
}
if (
opts.environment.enableResetCacheOnSourceFileChanges !== false &&
isDev
) {
opts = {
...opts,
environment: {
...opts.environment,
enableResetCacheOnSourceFileChanges: true,
},
};
}
const result = compileProgram(prog, {
opts,
filename: pass.filename ?? null,
comments: pass.file.ast.comments ?? [],
code: pass.file.code,
const filename = pass.filename ?? 'unknown';
if (ENABLE_REACT_COMPILER_TIMINGS === true) {
performance.mark(`${filename}:start`, {
detail: 'BabelPlugin:Program:start',
});
}
let opts = parsePluginOptions(pass.opts);
const isDev =
(typeof __DEV__ !== 'undefined' && __DEV__ === true) ||
process.env['NODE_ENV'] === 'development';
if (
opts.enableReanimatedCheck === true &&
pipelineUsesReanimatedPlugin(pass.file.opts.plugins)
) {
opts = injectReanimatedFlag(opts);
}
if (
opts.environment.enableResetCacheOnSourceFileChanges !== false &&
isDev
) {
opts = {
...opts,
environment: {
...opts.environment,
enableResetCacheOnSourceFileChanges: true,
},
};
}
const result = compileProgram(prog, {
opts,
filename: pass.filename ?? null,
comments: pass.file.ast.comments ?? [],
code: pass.file.code,
});
validateNoUntransformedReferences(
prog,
pass.filename ?? null,
opts.logger,
opts.environment,
result,
);
if (ENABLE_REACT_COMPILER_TIMINGS === true) {
performance.mark(`${filename}:end`, {
detail: 'BabelPlugin:Program:end',
});
validateNoUntransformedReferences(
prog,
pass.filename ?? null,
opts.logger,
opts.environment,
result,
);
if (ENABLE_REACT_COMPILER_TIMINGS === true) {
performance.mark(`${filename}:end`, {
detail: 'BabelPlugin:Program:end',
});
}
} catch (e) {
if (e instanceof CompilerError) {
throw e.withPrintedMessage(pass.file.code, {eslint: false});
}
throw e;
}
},
exit(_, pass): void {

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

@@ -51,26 +51,12 @@ function insertAdditionalFunctionDeclaration(
CompilerError.invariant(originalFnName != null && compiled.id != null, {
reason:
'Expected function declarations that are referenced elsewhere to have a named identifier',
description: null,
details: [
{
kind: 'error',
loc: fnPath.node.loc ?? null,
message: null,
},
],
loc: fnPath.node.loc ?? null,
});
CompilerError.invariant(originalFnParams.length === compiledParams.length, {
reason:
'Expected React Compiler optimized function declarations to have the same number of parameters as source',
description: null,
details: [
{
kind: 'error',
loc: fnPath.node.loc ?? null,
message: null,
},
],
loc: fnPath.node.loc ?? null,
});
const gatingCondition = t.identifier(
@@ -154,13 +140,7 @@ export function insertGatedFunctionDeclaration(
CompilerError.invariant(compiled.type === 'FunctionDeclaration', {
reason: 'Expected compiled node type to match input type',
description: `Got ${compiled.type} but expected FunctionDeclaration`,
details: [
{
kind: 'error',
loc: fnPath.node.loc ?? null,
message: null,
},
],
loc: fnPath.node.loc ?? null,
});
insertAdditionalFunctionDeclaration(
fnPath,

View File

@@ -9,7 +9,7 @@ import {NodePath} from '@babel/core';
import * as t from '@babel/types';
import {Scope as BabelScope} from '@babel/traverse';
import {CompilerError, ErrorCategory} from '../CompilerError';
import {CompilerError, ErrorSeverity} from '../CompilerError';
import {
EnvironmentConfig,
GeneratedSource,
@@ -18,9 +18,8 @@ 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 {BabelFn, getReactCompilerRuntimeModule} from './Program';
import {SuppressionRange} from './Suppression';
import {CompilerReactTarget} from './Options';
import {getReactCompilerRuntimeModule} from './Program';
export function validateRestrictedImports(
path: NodePath<t.Program>,
@@ -38,7 +37,7 @@ export function validateRestrictedImports(
ImportDeclaration(importDeclPath) {
if (restrictedImports.has(importDeclPath.node.source.value)) {
error.push({
category: ErrorCategory.Todo,
severity: ErrorSeverity.Todo,
reason: 'Bailing out due to blocklisted import',
description: `Import from module ${importDeclPath.node.source.value}`,
loc: importDeclPath.node.loc ?? null,
@@ -46,72 +45,39 @@ export function validateRestrictedImports(
}
},
});
if (error.hasAnyErrors()) {
if (error.hasErrors()) {
return error;
} else {
return null;
}
}
type ProgramContextOptions = {
program: NodePath<t.Program>;
suppressions: Array<SuppressionRange>;
opts: ParsedPluginOptions;
filename: string | null;
code: string | null;
hasModuleScopeOptOut: boolean;
};
export class ProgramContext {
/**
* Program and environment context
*/
/* Program and environment context */
scope: BabelScope;
opts: ParsedPluginOptions;
filename: string | null;
code: string | null;
reactRuntimeModule: string;
suppressions: Array<SuppressionRange>;
hasModuleScopeOptOut: boolean;
hookPattern: string | null;
/*
* This is a hack to work around what seems to be a Babel bug. Babel doesn't
* consistently respect the `skip()` function to avoid revisiting a node within
* a pass, so we use this set to track nodes that we have compiled.
*/
alreadyCompiled: WeakSet<object> | Set<object> = new (WeakSet ?? Set)();
// known generated or referenced identifiers in the program
knownReferencedNames: Set<string> = new Set();
// generated imports
imports: Map<string, Map<string, NonLocalImportSpecifier>> = new Map();
/**
* Metadata from compilation
*/
retryErrors: Array<{fn: BabelFn; error: CompilerError}> = [];
inferredEffectLocations: Set<t.SourceLocation> = new Set();
constructor({
program,
suppressions,
opts,
filename,
code,
hasModuleScopeOptOut,
}: ProgramContextOptions) {
constructor(
program: NodePath<t.Program>,
reactRuntimeModule: CompilerReactTarget,
hookPattern: string | null,
) {
this.hookPattern = hookPattern;
this.scope = program.scope;
this.opts = opts;
this.filename = filename;
this.code = code;
this.reactRuntimeModule = getReactCompilerRuntimeModule(opts.target);
this.suppressions = suppressions;
this.hasModuleScopeOptOut = hasModuleScopeOptOut;
this.reactRuntimeModule = getReactCompilerRuntimeModule(reactRuntimeModule);
}
isHookName(name: string): boolean {
if (this.opts.environment.hookPattern == null) {
if (this.hookPattern == null) {
return isHookName(name);
} else {
const match = new RegExp(this.opts.environment.hookPattern).exec(name);
const match = new RegExp(this.hookPattern).exec(name);
return (
match != null && typeof match[1] === 'string' && isHookName(match[1])
);
@@ -205,7 +171,7 @@ export class ProgramContext {
}
const error = new CompilerError();
error.push({
category: ErrorCategory.Todo,
severity: ErrorSeverity.Todo,
reason: 'Encountered conflicting global in generated program',
description: `Conflict from local binding ${name}`,
loc: scope.getBinding(name)?.path.node.loc ?? null,
@@ -213,12 +179,6 @@ export class ProgramContext {
});
return Err(error);
}
logEvent(event: LoggerEvent): void {
if (this.opts.logger != null) {
this.opts.logger.logEvent(this.filename, event);
}
}
}
function getExistingImports(
@@ -240,7 +200,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),
);
@@ -256,14 +216,8 @@ export function addImportsToProgram(
{
reason:
'Encountered conflicting import specifiers in generated program',
description: `Conflict from import ${loweredImport.module}:(${loweredImport.imported} as ${loweredImport.name})`,
details: [
{
kind: 'error',
loc: GeneratedSource,
message: null,
},
],
description: `Conflict from import ${loweredImport.module}:(${loweredImport.imported} as ${loweredImport.name}).`,
loc: GeneratedSource,
suggestions: null,
},
);
@@ -274,13 +228,7 @@ export function addImportsToProgram(
reason:
'Found inconsistent import specifier. This is an internal bug.',
description: `Expected import ${moduleName}:${specifierName} but found ${loweredImport.module}:${loweredImport.imported}`,
details: [
{
kind: 'error',
loc: GeneratedSource,
message: null,
},
],
loc: GeneratedSource,
},
);
}
@@ -303,29 +251,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,13 +6,8 @@
*/
import * as t from '@babel/types';
import {z} from 'zod/v4';
import {
CompilerDiagnostic,
CompilerError,
CompilerErrorDetail,
CompilerErrorDetailOptions,
} from '../CompilerError';
import {z} from 'zod';
import {CompilerError, CompilerErrorDetailOptions} from '../CompilerError';
import {
EnvironmentConfig,
ExternalFunction,
@@ -20,7 +15,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([
@@ -42,17 +37,9 @@ const PanicThresholdOptionsSchema = z.enum([
]);
export type PanicThresholdOptions = z.infer<typeof PanicThresholdOptionsSchema>;
const DynamicGatingOptionsSchema = z.object({
source: z.string(),
});
export type DynamicGatingOptions = z.infer<typeof DynamicGatingOptionsSchema>;
const CustomOptOutDirectiveSchema = z
.nullable(z.array(z.string()))
.default(null);
type CustomOptOutDirective = z.infer<typeof CustomOptOutDirectiveSchema>;
export type PluginOptions = Partial<{
environment: Partial<EnvironmentConfig>;
export type PluginOptions = {
environment: EnvironmentConfig;
logger: Logger | null;
@@ -78,28 +65,6 @@ export type PluginOptions = Partial<{
*/
gating: ExternalFunction | null;
/**
* If specified, this enables dynamic gating which matches `use memo if(...)`
* directives.
*
* Example usage:
* ```js
* // @dynamicGating:{"source":"myModule"}
* export function MyComponent() {
* 'use memo if(isEnabled)';
* return <div>...</div>;
* }
* ```
* This will emit:
* ```js
* import {isEnabled} from 'myModule';
* export const MyComponent = isEnabled()
* ? <optimized version>
* : <original version>;
* ```
*/
dynamicGating: DynamicGatingOptions | null;
panicThreshold: PanicThresholdOptions;
/*
@@ -133,25 +98,15 @@ export type PluginOptions = Partial<{
* provided rules will skip compilation. To disable this feature (never bailout of compilation
* even if the default ESLint is suppressed), pass an empty array.
*/
eslintSuppressionRules: Array<string> | null | undefined;
eslintSuppressionRules?: Array<string> | null | undefined;
/**
* Whether to report "suppression" errors for Flow suppressions. If false, suppression errors
* are only emitted for ESLint suppressions
*/
flowSuppressions: boolean;
/*
* Ignore 'use no forget' annotations. Helpful during testing but should not be used in production.
*/
ignoreUseNoForget: boolean;
/**
* Unstable / do not use
*/
customOptOutDirectives: CustomOptOutDirective;
sources: Array<string> | ((filename: string) => boolean) | null;
sources?: Array<string> | ((filename: string) => boolean) | null;
/**
* The compiler has customized support for react-native-reanimated, intended as a temporary workaround.
@@ -166,11 +121,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'),
@@ -231,14 +182,12 @@ export type LoggerEvent =
| CompileDiagnosticEvent
| CompileSkipEvent
| PipelineErrorEvent
| TimingEvent
| AutoDepsDecorationsEvent
| AutoDepsEligibleEvent;
| TimingEvent;
export type CompileErrorEvent = {
kind: 'CompileError';
fnLoc: t.SourceLocation | null;
detail: CompilerErrorDetail | CompilerDiagnostic;
detail: CompilerErrorDetailOptions;
};
export type CompileDiagnosticEvent = {
kind: 'CompileDiagnostic';
@@ -270,30 +219,19 @@ export type TimingEvent = {
kind: 'Timing';
measurement: PerformanceMeasure;
};
export type AutoDepsDecorationsEvent = {
kind: 'AutoDepsDecorations';
fnLoc: t.SourceLocation;
decorations: Array<t.SourceLocation>;
};
export type AutoDepsEligibleEvent = {
kind: 'AutoDepsEligible';
fnLoc: t.SourceLocation;
depArrayLoc: t.SourceLocation;
};
export type Logger = {
logEvent: (filename: string | null, event: LoggerEvent) => void;
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,
dynamicGating: null,
eslintSuppressionRules: null,
flowSuppressions: true,
ignoreUseNoForget: false,
@@ -301,11 +239,10 @@ export const defaultOptions: ParsedPluginOptions = {
return filename.indexOf('node_modules') === -1;
},
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;
}
@@ -343,40 +280,6 @@ export function parsePluginOptions(obj: unknown): ParsedPluginOptions {
}
break;
}
case 'dynamicGating': {
if (value == null) {
parsedOptions[key] = null;
} else {
const result = DynamicGatingOptionsSchema.safeParse(value);
if (result.success) {
parsedOptions[key] = result.data;
} else {
CompilerError.throwInvalidConfig({
reason:
'Could not parse dynamic gating. Update React Compiler config to fix the error',
description: `${fromZodError(result.error)}`,
loc: null,
suggestions: null,
});
}
}
break;
}
case 'customOptOutDirectives': {
const result = CustomOptOutDirectiveSchema.safeParse(value);
if (result.success) {
parsedOptions[key] = result.data;
} else {
CompilerError.throwInvalidConfig({
reason:
'Could not parse custom opt out directives. Update React Compiler config to fix the error',
description: `${fromZodError(result.error)}`,
loc: null,
suggestions: null,
});
}
break;
}
default: {
parsedOptions[key] = value;
}

View File

@@ -33,7 +33,9 @@ import {findContextIdentifiers} from '../HIR/FindContextIdentifiers';
import {
analyseFunctions,
dropManualMemoization,
inferMutableRanges,
inferReactivePlaces,
inferReferenceEffects,
inlineImmediatelyInvokedFunctionExpressions,
inferEffectDependencies,
} from '../Inference';
@@ -90,20 +92,17 @@ import {
} from '../Validation';
import {validateLocalsNotReassignedAfterRender} from '../Validation/ValidateLocalsNotReassignedAfterRender';
import {outlineFunctions} from '../Optimization/OutlineFunctions';
import {propagatePhiTypes} from '../TypeInference/PropagatePhiTypes';
import {lowerContextAccess} from '../Optimization/LowerContextAccess';
import {validateNoSetStateInEffects} from '../Validation/ValidateNoSetStateInEffects';
import {validateNoSetStateInPassiveEffects} from '../Validation/ValidateNoSetStateInPassiveEffects';
import {validateNoJSXInTryStatement} from '../Validation/ValidateNoJSXInTryStatement';
import {propagateScopeDependenciesHIR} from '../HIR/PropagateScopeDependenciesHIR';
import {outlineJSX} from '../Optimization/OutlineJsx';
import {optimizePropsMethodCalls} from '../Optimization/OptimizePropsMethodCalls';
import {transformFire} from '../Transform';
import {validateNoImpureFunctionsInRender} from '../Validation/ValidateNoImpureFunctionsInRender';
import {CompilerError} from '..';
import {validateStaticComponents} from '../Validation/ValidateStaticComponents';
import {validateNoFreezingKnownMutableFunctions} from '../Validation/ValidateNoFreezingKnownMutableFunctions';
import {inferMutationAliasingEffects} from '../Inference/InferMutationAliasingEffects';
import {inferMutationAliasingRanges} from '../Inference/InferMutationAliasingRanges';
import {validateNoDerivedComputationsInEffects} from '../Validation/ValidateNoDerivedComputationsInEffects';
import {nameAnonymousFunctions} from '../Transform/NameAnonymousFunctions';
export type CompilerPipelineValue =
| {kind: 'ast'; name: string; value: CodegenFunction}
@@ -130,7 +129,6 @@ function run(
mode,
config,
contextIdentifiers,
func,
logger,
filename,
code,
@@ -172,7 +170,7 @@ function runWithEnvironment(
!env.config.disableMemoizationForDebugging &&
!env.config.enableChangeDetectionForDebugging
) {
dropManualMemoization(hir).unwrap();
dropManualMemoization(hir);
log({kind: 'hir', name: 'DropManualMemoization', value: hir});
}
@@ -227,13 +225,15 @@ function runWithEnvironment(
analyseFunctions(hir);
log({kind: 'hir', name: 'AnalyseFunctions', value: hir});
const mutabilityAliasingErrors = inferMutationAliasingEffects(hir);
log({kind: 'hir', name: 'InferMutationAliasingEffects', value: hir});
const fnEffectErrors = inferReferenceEffects(hir);
if (env.isInferredMemoEnabled) {
if (mutabilityAliasingErrors.isErr()) {
throw mutabilityAliasingErrors.unwrapErr();
if (fnEffectErrors.length > 0) {
CompilerError.throw(fnEffectErrors[0]);
}
}
log({kind: 'hir', name: 'InferReferenceEffects', value: hir});
validateLocalsNotReassignedAfterRender(hir);
// Note: Has to come after infer reference effects because "dead" code may still affect inference
deadCodeElimination(hir);
@@ -247,16 +247,8 @@ function runWithEnvironment(
pruneMaybeThrows(hir);
log({kind: 'hir', name: 'PruneMaybeThrows', value: hir});
const mutabilityAliasingRangeErrors = inferMutationAliasingRanges(hir, {
isFunctionExpression: false,
});
log({kind: 'hir', name: 'InferMutationAliasingRanges', value: hir});
if (env.isInferredMemoEnabled) {
if (mutabilityAliasingRangeErrors.isErr()) {
throw mutabilityAliasingRangeErrors.unwrapErr();
}
validateLocalsNotReassignedAfterRender(hir);
}
inferMutableRanges(hir);
log({kind: 'hir', name: 'InferMutableRanges', value: hir});
if (env.isInferredMemoEnabled) {
if (env.config.assertValidMutableRanges) {
@@ -271,12 +263,8 @@ function runWithEnvironment(
validateNoSetStateInRender(hir).unwrap();
}
if (env.config.validateNoDerivedComputationsInEffects) {
validateNoDerivedComputationsInEffects(hir);
}
if (env.config.validateNoSetStateInEffects) {
env.logErrors(validateNoSetStateInEffects(hir, env));
if (env.config.validateNoSetStateInPassiveEffects) {
env.logErrors(validateNoSetStateInPassiveEffects(hir));
}
if (env.config.validateNoJSXInTryStatements) {
@@ -286,8 +274,6 @@ function runWithEnvironment(
if (env.config.validateNoImpureFunctionsInRender) {
validateNoImpureFunctionsInRender(hir).unwrap();
}
validateNoFreezingKnownMutableFunctions(hir).unwrap();
}
inferReactivePlaces(hir);
@@ -300,6 +286,13 @@ function runWithEnvironment(
value: hir,
});
propagatePhiTypes(hir);
log({
kind: 'hir',
name: 'PropagatePhiTypes',
value: hir,
});
if (env.isInferredMemoEnabled) {
if (env.config.validateStaticComponents) {
env.logErrors(validateStaticComponents(hir));
@@ -325,15 +318,6 @@ function runWithEnvironment(
outlineJSX(hir);
}
if (env.config.enableNameAnonymousFunctions) {
nameAnonymousFunctions(hir);
log({
kind: 'hir',
name: 'NameAnonymousFunctions',
value: hir,
});
}
if (env.config.enableFunctionOutlining) {
outlineFunctions(hir, fbtOperands);
log({kind: 'hir', name: 'OutlineFunctions', value: hir});
@@ -408,11 +392,6 @@ function runWithEnvironment(
if (env.config.inferEffectDependencies) {
inferEffectDependencies(hir);
log({
kind: 'hir',
name: 'InferEffectDependencies',
value: hir,
});
}
if (env.config.inlineJsxTransform) {

View File

@@ -1,13 +1,6 @@
/**
* 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 * 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 +45,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

@@ -8,10 +8,10 @@
import {NodePath} from '@babel/core';
import * as t from '@babel/types';
import {
CompilerDiagnostic,
CompilerError,
CompilerErrorDetail,
CompilerSuggestionOperation,
ErrorCategory,
ErrorSeverity,
} from '../CompilerError';
import {assertExhaustive} from '../Utils/utils';
import {GeneratedSource} from '../HIR';
@@ -86,18 +86,12 @@ export function findProgramSuppressions(
let enableComment: t.Comment | null = null;
let source: SuppressionSource | null = null;
let disableNextLinePattern: RegExp | null = null;
let disablePattern: RegExp | null = null;
let enablePattern: RegExp | null = null;
if (ruleNames.length !== 0) {
const rulePattern = `(${ruleNames.join('|')})`;
disableNextLinePattern = new RegExp(
`eslint-disable-next-line ${rulePattern}`,
);
disablePattern = new RegExp(`eslint-disable ${rulePattern}`);
enablePattern = new RegExp(`eslint-enable ${rulePattern}`);
}
const rulePattern = `(${ruleNames.join('|')})`;
const disableNextLinePattern = new RegExp(
`eslint-disable-next-line ${rulePattern}`,
);
const disablePattern = new RegExp(`eslint-disable ${rulePattern}`);
const enablePattern = new RegExp(`eslint-enable ${rulePattern}`);
const flowSuppressionPattern = new RegExp(
'\\$(FlowFixMe\\w*|FlowExpectedError|FlowIssue)\\[react\\-rule',
);
@@ -113,7 +107,6 @@ export function findProgramSuppressions(
* CommentLine within the block.
*/
disableComment == null &&
disableNextLinePattern != null &&
disableNextLinePattern.test(comment.value)
) {
disableComment = comment;
@@ -131,16 +124,12 @@ export function findProgramSuppressions(
source = 'Flow';
}
if (disablePattern != null && disablePattern.test(comment.value)) {
if (disablePattern.test(comment.value)) {
disableComment = comment;
source = 'Eslint';
}
if (
enablePattern != null &&
enablePattern.test(comment.value) &&
source === 'Eslint'
) {
if (enablePattern.test(comment.value) && source === 'Eslint') {
enableComment = comment;
}
@@ -163,14 +152,7 @@ export function suppressionsToCompilerError(
): CompilerError {
CompilerError.invariant(suppressionRanges.length !== 0, {
reason: `Expected at least suppression comment source range`,
description: null,
details: [
{
kind: 'error',
loc: GeneratedSource,
message: null,
},
],
loc: GeneratedSource,
});
const error = new CompilerError();
for (const suppressionRange of suppressionRanges) {
@@ -199,11 +181,12 @@ export function suppressionsToCompilerError(
'Unhandled suppression source',
);
}
error.pushDiagnostic(
CompilerDiagnostic.create({
reason: reason,
description: `React Compiler only works when your components follow all the rules of React, disabling them may result in unexpected or incorrect behavior. Found suppression \`${suppressionRange.disableComment.value.trim()}\``,
category: ErrorCategory.Suppression,
error.pushErrorDetail(
new CompilerErrorDetail({
reason: `${reason}. React Compiler only works when your components follow all the rules of React, disabling them may result in unexpected or incorrect behavior`,
description: suppressionRange.disableComment.value.trim(),
severity: ErrorSeverity.InvalidReact,
loc: suppressionRange.disableComment.loc ?? null,
suggestions: [
{
description: suggestion,
@@ -214,10 +197,6 @@ export function suppressionsToCompilerError(
op: CompilerSuggestionOperation.Remove,
},
],
}).withDetails({
kind: 'error',
loc: suppressionRange.disableComment.loc ?? null,
message: 'Found React rule suppression',
}),
);
}

View File

@@ -1,70 +1,35 @@
/**
* 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/core';
import * as t from '@babel/types';
import {CompilerError, EnvironmentConfig, Logger} from '..';
import {getOrInsertWith} from '../Utils/utils';
import {Environment, GeneratedSource} from '../HIR';
import {DEFAULT_EXPORT} from '../HIR/Environment';
import {CompileProgramMetadata} from './Program';
import {
CompilerDiagnostic,
CompilerDiagnosticOptions,
ErrorCategory,
} from '../CompilerError';
CompilerError,
CompilerErrorDetailOptions,
EnvironmentConfig,
ErrorSeverity,
Logger,
} from '..';
import {getOrInsertWith} from '../Utils/utils';
import {Environment} from '../HIR';
import {DEFAULT_EXPORT} from '../HIR/Environment';
import {CompileProgramResult} from './Program';
function throwInvalidReact(
options: CompilerDiagnosticOptions,
options: Omit<CompilerErrorDetailOptions, 'severity'>,
{logger, filename}: TraversalState,
): never {
const detail: CompilerErrorDetailOptions = {
...options,
severity: ErrorSeverity.InvalidReact,
};
logger?.logEvent(filename, {
kind: 'CompileError',
fnLoc: null,
detail: new CompilerDiagnostic(options),
detail,
});
CompilerError.throwDiagnostic(options);
}
function isAutodepsSigil(
arg: NodePath<t.ArgumentPlaceholder | t.SpreadElement | t.Expression>,
): boolean {
// Check for AUTODEPS identifier imported from React
if (arg.isIdentifier() && arg.node.name === 'AUTODEPS') {
const binding = arg.scope.getBinding(arg.node.name);
if (binding && binding.path.isImportSpecifier()) {
const importSpecifier = binding.path.node as t.ImportSpecifier;
if (importSpecifier.imported.type === 'Identifier') {
return (importSpecifier.imported as t.Identifier).name === 'AUTODEPS';
}
}
return false;
}
// Check for React.AUTODEPS member expression
if (arg.isMemberExpression() && !arg.node.computed) {
const object = arg.get('object');
const property = arg.get('property');
if (
object.isIdentifier() &&
object.node.name === 'React' &&
property.isIdentifier() &&
property.node.name === 'AUTODEPS'
) {
return true;
}
}
return false;
CompilerError.throw(detail);
}
function assertValidEffectImportReference(
autodepsIndex: number,
numArgs: number,
paths: Array<NodePath<t.Node>>,
context: TraversalState,
): void {
@@ -77,10 +42,11 @@ function assertValidEffectImportReference(
maybeCalleeLoc != null &&
context.inferredEffectLocations.has(maybeCalleeLoc);
/**
* Error on effect calls that still have AUTODEPS in their args
* Only error on untransformed references of the form `useMyEffect(...)`
* or `moduleNamespace.useMyEffect(...)`, with matching argument counts.
* TODO: do we also want a mode to also hard error on non-call references?
*/
const hasAutodepsArg = args.some(isAutodepsSigil);
if (hasAutodepsArg && !hasInferredEffect) {
if (args.length === numArgs && !hasInferredEffect) {
const maybeErrorDiagnostic = matchCompilerDiagnostic(
path,
context.transformErrors,
@@ -92,19 +58,14 @@ function assertValidEffectImportReference(
*/
throwInvalidReact(
{
category: ErrorCategory.AutomaticEffectDependencies,
reason:
'Cannot infer dependencies of this effect. This will break your build!',
description:
'To resolve, either pass a dependency array or fix reported compiler bailout diagnostics' +
(maybeErrorDiagnostic ? ` ${maybeErrorDiagnostic}` : ''),
details: [
{
kind: 'error',
message: 'Cannot infer dependencies',
loc: parent.node.loc ?? GeneratedSource,
},
],
'[InferEffectDependencies] React Compiler is unable to infer dependencies of this effect. ' +
'This will break your build! ' +
'To resolve, either pass your own dependency array or fix reported compiler bailout diagnostics.',
description: maybeErrorDiagnostic
? `(Bailout reason: ${maybeErrorDiagnostic})`
: null,
loc: parent.node.loc ?? null,
},
context,
);
@@ -124,18 +85,13 @@ function assertValidFireImportReference(
);
throwInvalidReact(
{
category: ErrorCategory.Fire,
reason: '[Fire] Untransformed reference to compiler-required feature.',
description:
'Either remove this `fire` call or ensure it is successfully transformed by the compiler' +
(maybeErrorDiagnostic != null ? ` ${maybeErrorDiagnostic}` : ''),
details: [
{
kind: 'error',
message: 'Untransformed `fire` call',
loc: paths[0].node.loc ?? GeneratedSource,
},
],
reason:
'[Fire] Untransformed reference to compiler-required feature. ' +
'Either remove this `fire` call or ensure it is successfully transformed by the compiler',
description: maybeErrorDiagnostic
? `(Bailout reason: ${maybeErrorDiagnostic})`
: null,
loc: paths[0].node.loc ?? null,
},
context,
);
@@ -146,7 +102,7 @@ export default function validateNoUntransformedReferences(
filename: string | null,
logger: Logger | null,
env: EnvironmentConfig,
compileResult: CompileProgramMetadata | null,
compileResult: CompileProgramResult | null,
): void {
const moduleLoadChecks = new Map<
string,
@@ -165,12 +121,12 @@ export default function validateNoUntransformedReferences(
if (env.inferEffectDependencies) {
for (const {
function: {source, importSpecifierName},
autodepsIndex,
numRequiredArgs,
} of env.inferEffectDependencies) {
const module = getOrInsertWith(moduleLoadChecks, source, () => new Map());
module.set(
importSpecifierName,
assertValidEffectImportReference.bind(null, autodepsIndex),
assertValidEffectImportReference.bind(null, numRequiredArgs),
);
}
}
@@ -215,14 +171,7 @@ function validateImportSpecifier(
const binding = local.scope.getBinding(local.node.name);
CompilerError.invariant(binding != null, {
reason: 'Expected binding to be found for import specifier',
description: null,
details: [
{
kind: 'error',
loc: local.node.loc ?? null,
message: null,
},
],
loc: local.node.loc ?? null,
});
checkFn(binding.referencePaths, state);
}
@@ -242,14 +191,7 @@ function validateNamespacedImport(
CompilerError.invariant(binding != null, {
reason: 'Expected binding to be found for import specifier',
description: null,
details: [
{
kind: 'error',
loc: local.node.loc ?? null,
message: null,
},
],
loc: local.node.loc ?? null,
});
const filteredReferences = new Map<
CheckInvalidReferenceFn,
@@ -287,7 +229,7 @@ function transformProgram(
moduleLoadChecks: Map<string, Map<string, CheckInvalidReferenceFn>>,
filename: string | null,
logger: Logger | null,
compileResult: CompileProgramMetadata | null,
compileResult: CompileProgramResult | null,
): void {
const traversalState: TraversalState = {
shouldInvalidateScopes: true,

View File

@@ -1,752 +0,0 @@
/**
* TypeScript definitions for Flow type JSON representations
* Based on the output of /data/sandcastle/boxes/fbsource/fbcode/flow/src/typing/convertTypes.ml
*/
// Base type for all Flow types with a kind field
export interface BaseFlowType {
kind: string;
}
// Type for representing polarity
export type Polarity = 'positive' | 'negative' | 'neutral';
// Type for representing a name that might be null
export type OptionalName = string | null;
// Open type
export interface OpenType extends BaseFlowType {
kind: 'Open';
}
// Def type
export interface DefType extends BaseFlowType {
kind: 'Def';
def: DefT;
}
// Eval type
export interface EvalType extends BaseFlowType {
kind: 'Eval';
type: FlowType;
destructor: Destructor;
}
// Generic type
export interface GenericType extends BaseFlowType {
kind: 'Generic';
name: string;
bound: FlowType;
no_infer: boolean;
}
// ThisInstance type
export interface ThisInstanceType extends BaseFlowType {
kind: 'ThisInstance';
instance: InstanceT;
is_this: boolean;
name: string;
}
// ThisTypeApp type
export interface ThisTypeAppType extends BaseFlowType {
kind: 'ThisTypeApp';
t1: FlowType;
t2: FlowType;
t_list?: Array<FlowType>;
}
// TypeApp type
export interface TypeAppType extends BaseFlowType {
kind: 'TypeApp';
type: FlowType;
targs: Array<FlowType>;
from_value: boolean;
use_desc: boolean;
}
// FunProto type
export interface FunProtoType extends BaseFlowType {
kind: 'FunProto';
}
// ObjProto type
export interface ObjProtoType extends BaseFlowType {
kind: 'ObjProto';
}
// NullProto type
export interface NullProtoType extends BaseFlowType {
kind: 'NullProto';
}
// FunProtoBind type
export interface FunProtoBindType extends BaseFlowType {
kind: 'FunProtoBind';
}
// Intersection type
export interface IntersectionType extends BaseFlowType {
kind: 'Intersection';
members: Array<FlowType>;
}
// Union type
export interface UnionType extends BaseFlowType {
kind: 'Union';
members: Array<FlowType>;
}
// Maybe type
export interface MaybeType extends BaseFlowType {
kind: 'Maybe';
type: FlowType;
}
// Optional type
export interface OptionalType extends BaseFlowType {
kind: 'Optional';
type: FlowType;
use_desc: boolean;
}
// Keys type
export interface KeysType extends BaseFlowType {
kind: 'Keys';
type: FlowType;
}
// Annot type
export interface AnnotType extends BaseFlowType {
kind: 'Annot';
type: FlowType;
use_desc: boolean;
}
// Opaque type
export interface OpaqueType extends BaseFlowType {
kind: 'Opaque';
opaquetype: {
opaque_id: string;
underlying_t: FlowType | null;
super_t: FlowType | null;
opaque_type_args: Array<{
name: string;
type: FlowType;
polarity: Polarity;
}>;
opaque_name: string;
};
}
// Namespace type
export interface NamespaceType extends BaseFlowType {
kind: 'Namespace';
namespace_symbol: {
symbol: string;
};
values_type: FlowType;
types_tmap: PropertyMap;
}
// Any type
export interface AnyType extends BaseFlowType {
kind: 'Any';
}
// StrUtil type
export interface StrUtilType extends BaseFlowType {
kind: 'StrUtil';
op: 'StrPrefix' | 'StrSuffix';
prefix?: string;
suffix?: string;
remainder?: FlowType;
}
// TypeParam definition
export interface TypeParam {
name: string;
bound: FlowType;
polarity: Polarity;
default: FlowType | null;
}
// EnumInfo types
export type EnumInfo = ConcreteEnum | AbstractEnum;
export interface ConcreteEnum {
kind: 'ConcreteEnum';
enum_name: string;
enum_id: string;
members: Array<string>;
representation_t: FlowType;
has_unknown_members: boolean;
}
export interface AbstractEnum {
kind: 'AbstractEnum';
representation_t: FlowType;
}
// CanonicalRendersForm types
export type CanonicalRendersForm =
| InstrinsicRenders
| NominalRenders
| StructuralRenders
| DefaultRenders;
export interface InstrinsicRenders {
kind: 'InstrinsicRenders';
name: string;
}
export interface NominalRenders {
kind: 'NominalRenders';
renders_id: string;
renders_name: string;
renders_super: FlowType;
}
export interface StructuralRenders {
kind: 'StructuralRenders';
renders_variant: 'RendersNormal' | 'RendersMaybe' | 'RendersStar';
renders_structural_type: FlowType;
}
export interface DefaultRenders {
kind: 'DefaultRenders';
}
// InstanceT definition
export interface InstanceT {
inst: InstType;
static: FlowType;
super: FlowType;
implements: Array<FlowType>;
}
// InstType definition
export interface InstType {
class_name: string | null;
class_id: string;
type_args: Array<{
name: string;
type: FlowType;
polarity: Polarity;
}>;
own_props: PropertyMap;
proto_props: PropertyMap;
call_t: null | {
id: number;
call: FlowType;
};
}
// DefT types
export type DefT =
| NumGeneralType
| StrGeneralType
| BoolGeneralType
| BigIntGeneralType
| EmptyType
| MixedType
| NullType
| VoidType
| SymbolType
| FunType
| ObjType
| ArrType
| ClassType
| InstanceType
| SingletonStrType
| NumericStrKeyType
| SingletonNumType
| SingletonBoolType
| SingletonBigIntType
| TypeType
| PolyType
| ReactAbstractComponentType
| RendersType
| EnumValueType
| EnumObjectType;
export interface NumGeneralType extends BaseFlowType {
kind: 'NumGeneral';
}
export interface StrGeneralType extends BaseFlowType {
kind: 'StrGeneral';
}
export interface BoolGeneralType extends BaseFlowType {
kind: 'BoolGeneral';
}
export interface BigIntGeneralType extends BaseFlowType {
kind: 'BigIntGeneral';
}
export interface EmptyType extends BaseFlowType {
kind: 'Empty';
}
export interface MixedType extends BaseFlowType {
kind: 'Mixed';
}
export interface NullType extends BaseFlowType {
kind: 'Null';
}
export interface VoidType extends BaseFlowType {
kind: 'Void';
}
export interface SymbolType extends BaseFlowType {
kind: 'Symbol';
}
export interface FunType extends BaseFlowType {
kind: 'Fun';
static: FlowType;
funtype: FunTypeObj;
}
export interface ObjType extends BaseFlowType {
kind: 'Obj';
objtype: ObjTypeObj;
}
export interface ArrType extends BaseFlowType {
kind: 'Arr';
arrtype: ArrTypeObj;
}
export interface ClassType extends BaseFlowType {
kind: 'Class';
type: FlowType;
}
export interface InstanceType extends BaseFlowType {
kind: 'Instance';
instance: InstanceT;
}
export interface SingletonStrType extends BaseFlowType {
kind: 'SingletonStr';
from_annot: boolean;
value: string;
}
export interface NumericStrKeyType extends BaseFlowType {
kind: 'NumericStrKey';
number: string;
string: string;
}
export interface SingletonNumType extends BaseFlowType {
kind: 'SingletonNum';
from_annot: boolean;
number: string;
string: string;
}
export interface SingletonBoolType extends BaseFlowType {
kind: 'SingletonBool';
from_annot: boolean;
value: boolean;
}
export interface SingletonBigIntType extends BaseFlowType {
kind: 'SingletonBigInt';
from_annot: boolean;
value: string;
}
export interface TypeType extends BaseFlowType {
kind: 'Type';
type_kind: TypeTKind;
type: FlowType;
}
export type TypeTKind =
| 'TypeAliasKind'
| 'TypeParamKind'
| 'OpaqueKind'
| 'ImportTypeofKind'
| 'ImportClassKind'
| 'ImportEnumKind'
| 'InstanceKind'
| 'RenderTypeKind';
export interface PolyType extends BaseFlowType {
kind: 'Poly';
tparams: Array<TypeParam>;
t_out: FlowType;
id: string;
}
export interface ReactAbstractComponentType extends BaseFlowType {
kind: 'ReactAbstractComponent';
config: FlowType;
renders: FlowType;
instance: ComponentInstance;
component_kind: ComponentKind;
}
export type ComponentInstance =
| {kind: 'RefSetterProp'; type: FlowType}
| {kind: 'Omitted'};
export type ComponentKind =
| {kind: 'Structural'}
| {kind: 'Nominal'; id: string; name: string; types: Array<FlowType> | null};
export interface RendersType extends BaseFlowType {
kind: 'Renders';
form: CanonicalRendersForm;
}
export interface EnumValueType extends BaseFlowType {
kind: 'EnumValue';
enum_info: EnumInfo;
}
export interface EnumObjectType extends BaseFlowType {
kind: 'EnumObject';
enum_value_t: FlowType;
enum_info: EnumInfo;
}
// ObjKind types
export type ObjKind =
| {kind: 'Exact'}
| {kind: 'Inexact'}
| {kind: 'Indexed'; dicttype: DictType};
// DictType definition
export interface DictType {
dict_name: string | null;
key: FlowType;
value: FlowType;
dict_polarity: Polarity;
}
// ArrType types
export type ArrTypeObj = ArrayAT | TupleAT | ROArrayAT;
export interface ArrayAT {
kind: 'ArrayAT';
elem_t: FlowType;
}
export interface TupleAT {
kind: 'TupleAT';
elem_t: FlowType;
elements: Array<TupleElement>;
min_arity: number;
max_arity: number;
inexact: boolean;
}
export interface ROArrayAT {
kind: 'ROArrayAT';
elem_t: FlowType;
}
// TupleElement definition
export interface TupleElement {
name: string | null;
t: FlowType;
polarity: Polarity;
optional: boolean;
}
// Flags definition
export interface Flags {
obj_kind: ObjKind;
}
// Property types
export type Property =
| FieldProperty
| GetProperty
| SetProperty
| GetSetProperty
| MethodProperty;
export interface FieldProperty {
kind: 'Field';
type: FlowType;
polarity: Polarity;
}
export interface GetProperty {
kind: 'Get';
type: FlowType;
}
export interface SetProperty {
kind: 'Set';
type: FlowType;
}
export interface GetSetProperty {
kind: 'GetSet';
get_type: FlowType;
set_type: FlowType;
}
export interface MethodProperty {
kind: 'Method';
type: FlowType;
}
// PropertyMap definition
export interface PropertyMap {
[key: string]: Property; // For other properties in the map
}
// ObjType definition
export interface ObjTypeObj {
flags: Flags;
props: PropertyMap;
proto_t: FlowType;
call_t: number | null;
}
// FunType definition
export interface FunTypeObj {
this_t: {
type: FlowType;
status: ThisStatus;
};
params: Array<{
name: string | null;
type: FlowType;
}>;
rest_param: null | {
name: string | null;
type: FlowType;
};
return_t: FlowType;
type_guard: null | {
inferred: boolean;
param_name: string;
type_guard: FlowType;
one_sided: boolean;
};
effect: Effect;
}
// ThisStatus types
export type ThisStatus =
| {kind: 'This_Method'; unbound: boolean}
| {kind: 'This_Function'};
// Effect types
export type Effect =
| {kind: 'HookDecl'; id: string}
| {kind: 'HookAnnot'}
| {kind: 'ArbitraryEffect'}
| {kind: 'AnyEffect'};
// Destructor types
export type Destructor =
| NonMaybeTypeDestructor
| PropertyTypeDestructor
| ElementTypeDestructor
| OptionalIndexedAccessNonMaybeTypeDestructor
| OptionalIndexedAccessResultTypeDestructor
| ExactTypeDestructor
| ReadOnlyTypeDestructor
| PartialTypeDestructor
| RequiredTypeDestructor
| SpreadTypeDestructor
| SpreadTupleTypeDestructor
| RestTypeDestructor
| ValuesTypeDestructor
| ConditionalTypeDestructor
| TypeMapDestructor
| ReactElementPropsTypeDestructor
| ReactElementConfigTypeDestructor
| ReactCheckComponentConfigDestructor
| ReactDRODestructor
| MakeHooklikeDestructor
| MappedTypeDestructor
| EnumTypeDestructor;
export interface NonMaybeTypeDestructor {
kind: 'NonMaybeType';
}
export interface PropertyTypeDestructor {
kind: 'PropertyType';
name: string;
}
export interface ElementTypeDestructor {
kind: 'ElementType';
index_type: FlowType;
}
export interface OptionalIndexedAccessNonMaybeTypeDestructor {
kind: 'OptionalIndexedAccessNonMaybeType';
index: OptionalIndexedAccessIndex;
}
export type OptionalIndexedAccessIndex =
| {kind: 'StrLitIndex'; name: string}
| {kind: 'TypeIndex'; type: FlowType};
export interface OptionalIndexedAccessResultTypeDestructor {
kind: 'OptionalIndexedAccessResultType';
}
export interface ExactTypeDestructor {
kind: 'ExactType';
}
export interface ReadOnlyTypeDestructor {
kind: 'ReadOnlyType';
}
export interface PartialTypeDestructor {
kind: 'PartialType';
}
export interface RequiredTypeDestructor {
kind: 'RequiredType';
}
export interface SpreadTypeDestructor {
kind: 'SpreadType';
target: SpreadTarget;
operands: Array<SpreadOperand>;
operand_slice: Slice | null;
}
export type SpreadTarget =
| {kind: 'Value'; make_seal: 'Sealed' | 'Frozen' | 'As_Const'}
| {kind: 'Annot'; make_exact: boolean};
export type SpreadOperand = {kind: 'Type'; type: FlowType} | Slice;
export interface Slice {
kind: 'Slice';
prop_map: PropertyMap;
generics: Array<string>;
dict: DictType | null;
reachable_targs: Array<{
type: FlowType;
polarity: Polarity;
}>;
}
export interface SpreadTupleTypeDestructor {
kind: 'SpreadTupleType';
inexact: boolean;
resolved_rev: string;
unresolved: string;
}
export interface RestTypeDestructor {
kind: 'RestType';
merge_mode: RestMergeMode;
type: FlowType;
}
export type RestMergeMode =
| {kind: 'SpreadReversal'}
| {kind: 'ReactConfigMerge'; polarity: Polarity}
| {kind: 'Omit'};
export interface ValuesTypeDestructor {
kind: 'ValuesType';
}
export interface ConditionalTypeDestructor {
kind: 'ConditionalType';
distributive_tparam_name: string | null;
infer_tparams: string;
extends_t: FlowType;
true_t: FlowType;
false_t: FlowType;
}
export interface TypeMapDestructor {
kind: 'ObjectKeyMirror';
}
export interface ReactElementPropsTypeDestructor {
kind: 'ReactElementPropsType';
}
export interface ReactElementConfigTypeDestructor {
kind: 'ReactElementConfigType';
}
export interface ReactCheckComponentConfigDestructor {
kind: 'ReactCheckComponentConfig';
props: {
[key: string]: Property;
};
}
export interface ReactDRODestructor {
kind: 'ReactDRO';
dro_type:
| 'HookReturn'
| 'HookArg'
| 'Props'
| 'ImmutableAnnot'
| 'DebugAnnot';
}
export interface MakeHooklikeDestructor {
kind: 'MakeHooklike';
}
export interface MappedTypeDestructor {
kind: 'MappedType';
homomorphic: Homomorphic;
distributive_tparam_name: string | null;
property_type: FlowType;
mapped_type_flags: {
variance: Polarity;
optional: 'MakeOptional' | 'RemoveOptional' | 'KeepOptionality';
};
}
export type Homomorphic =
| {kind: 'Homomorphic'}
| {kind: 'Unspecialized'}
| {kind: 'SemiHomomorphic'; type: FlowType};
export interface EnumTypeDestructor {
kind: 'EnumType';
}
// Union of all possible Flow types
export type FlowType =
| OpenType
| DefType
| EvalType
| GenericType
| ThisInstanceType
| ThisTypeAppType
| TypeAppType
| FunProtoType
| ObjProtoType
| NullProtoType
| FunProtoBindType
| IntersectionType
| UnionType
| MaybeType
| OptionalType
| KeysType
| AnnotType
| OpaqueType
| NamespaceType
| AnyType
| StrUtilType;

View File

@@ -1,138 +0,0 @@
import {CompilerError, SourceLocation} from '..';
import {
ConcreteType,
printConcrete,
printType,
StructuralValue,
Type,
VariableId,
} from './Types';
export function unsupportedLanguageFeature(
desc: string,
loc: SourceLocation,
): never {
CompilerError.throwInvalidJS({
reason: `Typedchecker does not currently support language feature: ${desc}`,
loc,
});
}
export type UnificationError =
| {
kind: 'TypeUnification';
left: ConcreteType<Type>;
right: ConcreteType<Type>;
}
| {
kind: 'StructuralUnification';
left: StructuralValue;
right: ConcreteType<Type>;
};
function printUnificationError(err: UnificationError): string {
if (err.kind === 'TypeUnification') {
return `${printConcrete(err.left, printType)} is incompatible with ${printConcrete(err.right, printType)}`;
} else {
return `structural ${err.left.kind} is incompatible with ${printConcrete(err.right, printType)}`;
}
}
export function raiseUnificationErrors(
errs: null | Array<UnificationError>,
loc: SourceLocation,
): void {
if (errs != null) {
if (errs.length === 0) {
CompilerError.invariant(false, {
reason: 'Should not have array of zero errors',
description: null,
details: [
{
kind: 'error',
loc,
message: null,
},
],
});
} else if (errs.length === 1) {
CompilerError.throwInvalidJS({
reason: `Unable to unify types because ${printUnificationError(errs[0])}`,
loc,
});
} else {
const messages = errs
.map(err => `\t* ${printUnificationError(err)}`)
.join('\n');
CompilerError.throwInvalidJS({
reason: `Unable to unify types because:\n${messages}`,
loc,
});
}
}
}
export function unresolvableTypeVariable(
id: VariableId,
loc: SourceLocation,
): never {
CompilerError.throwInvalidJS({
reason: `Unable to resolve free variable ${id} to a concrete type`,
loc,
});
}
export function cannotAddVoid(explicit: boolean, loc: SourceLocation): never {
if (explicit) {
CompilerError.throwInvalidJS({
reason: `Undefined is not a valid operand of \`+\``,
loc,
});
} else {
CompilerError.throwInvalidJS({
reason: `Value may be undefined, which is not a valid operand of \`+\``,
loc,
});
}
}
export function unsupportedTypeAnnotation(
desc: string,
loc: SourceLocation,
): never {
CompilerError.throwInvalidJS({
reason: `Typedchecker does not currently support type annotation: ${desc}`,
loc,
});
}
export function checkTypeArgumentArity(
desc: string,
expected: number,
actual: number,
loc: SourceLocation,
): void {
if (expected !== actual) {
CompilerError.throwInvalidJS({
reason: `Expected ${desc} to have ${expected} type parameters, got ${actual}`,
loc,
});
}
}
export function notAFunction(desc: string, loc: SourceLocation): void {
CompilerError.throwInvalidJS({
reason: `Cannot call ${desc} because it is not a function`,
loc,
});
}
export function notAPolymorphicFunction(
desc: string,
loc: SourceLocation,
): void {
CompilerError.throwInvalidJS({
reason: `Cannot call ${desc} with type arguments because it is not a polymorphic function`,
loc,
});
}

View File

@@ -1,312 +0,0 @@
import {GeneratedSource} from '../HIR';
import {assertExhaustive} from '../Utils/utils';
import {unsupportedLanguageFeature} from './TypeErrors';
import {
ConcreteType,
ResolvedType,
TypeParameter,
TypeParameterId,
DEBUG,
printConcrete,
printType,
} from './Types';
export function substitute(
type: ConcreteType<ResolvedType>,
typeParameters: Array<TypeParameter<ResolvedType>>,
typeArguments: Array<ResolvedType>,
): ResolvedType {
const substMap = new Map<TypeParameterId, ResolvedType>();
for (let i = 0; i < typeParameters.length; i++) {
// TODO: Length checks to make sure type params match up with args
const typeParameter = typeParameters[i];
const typeArgument = typeArguments[i];
substMap.set(typeParameter.id, typeArgument);
}
const substitutionFunction = (t: ResolvedType): ResolvedType => {
// TODO: We really want a stateful mapper or visitor here so that we can model nested polymorphic types
if (t.type.kind === 'Generic' && substMap.has(t.type.id)) {
const substitutedType = substMap.get(t.type.id)!;
return substitutedType;
}
return {
kind: 'Concrete',
type: mapType(substitutionFunction, t.type),
platform: t.platform,
};
};
const substituted = mapType(substitutionFunction, type);
if (DEBUG) {
let substs = '';
for (let i = 0; i < typeParameters.length; i++) {
const typeParameter = typeParameters[i];
const typeArgument = typeArguments[i];
substs += `[${typeParameter.name}${typeParameter.id} := ${printType(typeArgument)}]`;
}
console.log(
`${printConcrete(type, printType)}${substs} = ${printConcrete(substituted, printType)}`,
);
}
return {kind: 'Concrete', type: substituted, platform: /* TODO */ 'shared'};
}
export function mapType<T, U>(
f: (t: T) => U,
type: ConcreteType<T>,
): ConcreteType<U> {
switch (type.kind) {
case 'Mixed':
case 'Number':
case 'String':
case 'Boolean':
case 'Void':
return type;
case 'Nullable':
return {
kind: 'Nullable',
type: f(type.type),
};
case 'Array':
return {
kind: 'Array',
element: f(type.element),
};
case 'Set':
return {
kind: 'Set',
element: f(type.element),
};
case 'Map':
return {
kind: 'Map',
key: f(type.key),
value: f(type.value),
};
case 'Function':
return {
kind: 'Function',
typeParameters:
type.typeParameters?.map(param => ({
id: param.id,
name: param.name,
bound: f(param.bound),
})) ?? null,
params: type.params.map(f),
returnType: f(type.returnType),
};
case 'Component': {
return {
kind: 'Component',
children: type.children != null ? f(type.children) : null,
props: new Map([...type.props.entries()].map(([k, v]) => [k, f(v)])),
};
}
case 'Generic':
return {
kind: 'Generic',
id: type.id,
bound: f(type.bound),
};
case 'Object':
return type;
case 'Tuple':
return {
kind: 'Tuple',
id: type.id,
members: type.members.map(f),
};
case 'Structural':
return type;
case 'Enum':
case 'Union':
case 'Instance':
unsupportedLanguageFeature(type.kind, GeneratedSource);
default:
assertExhaustive(type, 'Unknown type kind');
}
}
export function diff<R, T>(
a: ConcreteType<T>,
b: ConcreteType<T>,
onChild: (a: T, b: T) => R,
onChildMismatch: (child: R, cur: R) => R,
onMismatch: (a: ConcreteType<T>, b: ConcreteType<T>, cur: R) => R,
init: R,
): R {
let errors = init;
// Check if kinds match
if (a.kind !== b.kind) {
errors = onMismatch(a, b, errors);
return errors;
}
// Based on kind, check other properties
switch (a.kind) {
case 'Mixed':
case 'Number':
case 'String':
case 'Boolean':
case 'Void':
// Simple types, no further checks needed
break;
case 'Nullable':
// Check the nested type
errors = onChildMismatch(onChild(a.type, (b as typeof a).type), errors);
break;
case 'Array':
case 'Set':
// Check the element type
errors = onChildMismatch(
onChild(a.element, (b as typeof a).element),
errors,
);
break;
case 'Map':
// Check both key and value types
errors = onChildMismatch(onChild(a.key, (b as typeof a).key), errors);
errors = onChildMismatch(onChild(a.value, (b as typeof a).value), errors);
break;
case 'Function': {
const bFunc = b as typeof a;
// Check type parameters
if ((a.typeParameters == null) !== (bFunc.typeParameters == null)) {
errors = onMismatch(a, b, errors);
}
if (a.typeParameters != null && bFunc.typeParameters != null) {
if (a.typeParameters.length !== bFunc.typeParameters.length) {
errors = onMismatch(a, b, errors);
}
// Type parameters are just numbers, so we can compare them directly
for (let i = 0; i < a.typeParameters.length; i++) {
if (a.typeParameters[i] !== bFunc.typeParameters[i]) {
errors = onMismatch(a, b, errors);
}
}
}
// Check parameters
if (a.params.length !== bFunc.params.length) {
errors = onMismatch(a, b, errors);
}
for (let i = 0; i < a.params.length; i++) {
errors = onChildMismatch(onChild(a.params[i], bFunc.params[i]), errors);
}
// Check return type
errors = onChildMismatch(onChild(a.returnType, bFunc.returnType), errors);
break;
}
case 'Component': {
const bComp = b as typeof a;
// Check children
if (a.children !== bComp.children) {
errors = onMismatch(a, b, errors);
}
// Check props
if (a.props.size !== bComp.props.size) {
errors = onMismatch(a, b, errors);
}
for (const [k, v] of a.props) {
const bProp = bComp.props.get(k);
if (bProp == null) {
errors = onMismatch(a, b, errors);
} else {
errors = onChildMismatch(onChild(v, bProp), errors);
}
}
break;
}
case 'Generic': {
// Check that the type parameter IDs match
if (a.id !== (b as typeof a).id) {
errors = onMismatch(a, b, errors);
}
break;
}
case 'Structural': {
const bStruct = b as typeof a;
// Check that the structural IDs match
if (a.id !== bStruct.id) {
errors = onMismatch(a, b, errors);
}
break;
}
case 'Object': {
const bNom = b as typeof a;
// Check that the nominal IDs match
if (a.id !== bNom.id) {
errors = onMismatch(a, b, errors);
}
break;
}
case 'Tuple': {
const bTuple = b as typeof a;
// Check that the tuple IDs match
if (a.id !== bTuple.id) {
errors = onMismatch(a, b, errors);
}
for (let i = 0; i < a.members.length; i++) {
errors = onChildMismatch(
onChild(a.members[i], bTuple.members[i]),
errors,
);
}
break;
}
case 'Enum':
case 'Instance':
case 'Union': {
unsupportedLanguageFeature(a.kind, GeneratedSource);
}
default:
assertExhaustive(a, 'Unknown type kind');
}
return errors;
}
export function filterOptional(t: ResolvedType): ResolvedType {
if (t.kind === 'Concrete' && t.type.kind === 'Nullable') {
return t.type.type;
}
return t;
}

View File

@@ -38,13 +38,7 @@ export function assertConsistentIdentifiers(fn: HIRFunction): void {
CompilerError.invariant(instr.lvalue.identifier.name === null, {
reason: `Expected all lvalues to be temporaries`,
description: `Found named lvalue \`${instr.lvalue.identifier.name}\``,
details: [
{
kind: 'error',
loc: instr.lvalue.loc,
message: null,
},
],
loc: instr.lvalue.loc,
suggestions: null,
});
CompilerError.invariant(!assignments.has(instr.lvalue.identifier.id), {
@@ -52,13 +46,7 @@ export function assertConsistentIdentifiers(fn: HIRFunction): void {
description: `Found duplicate assignment of '${printPlace(
instr.lvalue,
)}'`,
details: [
{
kind: 'error',
loc: instr.lvalue.loc,
message: null,
},
],
loc: instr.lvalue.loc,
suggestions: null,
});
assignments.add(instr.lvalue.identifier.id);
@@ -89,13 +77,7 @@ function validate(
CompilerError.invariant(identifier === previous, {
reason: `Duplicate identifier object`,
description: `Found duplicate identifier object for id ${identifier.id}`,
details: [
{
kind: 'error',
loc: loc ?? GeneratedSource,
message: null,
},
],
loc: loc ?? GeneratedSource,
suggestions: null,
});
}

View File

@@ -18,13 +18,7 @@ export function assertTerminalSuccessorsExist(fn: HIRFunction): void {
description: `Block bb${successor} does not exist for terminal '${printTerminal(
block.terminal,
)}'`,
details: [
{
kind: 'error',
loc: (block.terminal as any).loc ?? GeneratedSource,
message: null,
},
],
loc: (block.terminal as any).loc ?? GeneratedSource,
suggestions: null,
});
return successor;
@@ -39,26 +33,14 @@ export function assertTerminalPredsExist(fn: HIRFunction): void {
CompilerError.invariant(predBlock != null, {
reason: 'Expected predecessor block to exist',
description: `Block ${block.id} references non-existent ${pred}`,
details: [
{
kind: 'error',
loc: GeneratedSource,
message: null,
},
],
loc: GeneratedSource,
});
CompilerError.invariant(
[...eachTerminalSuccessor(predBlock.terminal)].includes(block.id),
{
reason: 'Terminal successor does not reference correct predecessor',
description: `Block bb${block.id} has bb${predBlock.id} as a predecessor, but bb${predBlock.id}'s successors do not include bb${block.id}`,
details: [
{
kind: 'error',
loc: GeneratedSource,
message: null,
},
],
loc: GeneratedSource,
},
);
}

View File

@@ -1,10 +1,3 @@
/**
* 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 {
BlockId,
@@ -131,13 +124,7 @@ export function recursivelyTraverseItems<T, TContext>(
CompilerError.invariant(disjoint || nested, {
reason: 'Invalid nesting in program blocks or scopes',
description: `Items overlap but are not nested: ${maybeParentRange.start}:${maybeParentRange.end}(${currRange.start}:${currRange.end})`,
details: [
{
kind: 'error',
loc: GeneratedSource,
message: null,
},
],
loc: GeneratedSource,
});
if (disjoint) {
exit(maybeParent, context);

View File

@@ -5,14 +5,13 @@
* LICENSE file in the root directory of this source tree.
*/
import {HIRFunction, MutableRange, Place} from './HIR';
import invariant from 'invariant';
import {HIRFunction, Identifier, MutableRange} from './HIR';
import {
eachInstructionLValue,
eachInstructionOperand,
eachTerminalOperand,
} from './visitors';
import {CompilerError} from '..';
import {printPlace} from './PrintHIR';
/*
* Checks that all mutable ranges in the function are well-formed, with
@@ -21,49 +20,38 @@ import {printPlace} from './PrintHIR';
export function assertValidMutableRanges(fn: HIRFunction): void {
for (const [, block] of fn.body.blocks) {
for (const phi of block.phis) {
visit(phi.place, `phi for block bb${block.id}`);
for (const [pred, operand] of phi.operands) {
visit(operand, `phi predecessor bb${pred} for block bb${block.id}`);
visitIdentifier(phi.place.identifier);
for (const [, operand] of phi.operands) {
visitIdentifier(operand.identifier);
}
}
for (const instr of block.instructions) {
for (const operand of eachInstructionLValue(instr)) {
visit(operand, `instruction [${instr.id}]`);
visitIdentifier(operand.identifier);
}
for (const operand of eachInstructionOperand(instr)) {
visit(operand, `instruction [${instr.id}]`);
visitIdentifier(operand.identifier);
}
}
for (const operand of eachTerminalOperand(block.terminal)) {
visit(operand, `terminal [${block.terminal.id}]`);
visitIdentifier(operand.identifier);
}
}
}
function visit(place: Place, description: string): void {
validateMutableRange(place, place.identifier.mutableRange, description);
if (place.identifier.scope !== null) {
validateMutableRange(place, place.identifier.scope.range, description);
function visitIdentifier(identifier: Identifier): void {
validateMutableRange(identifier.mutableRange);
if (identifier.scope !== null) {
validateMutableRange(identifier.scope.range);
}
}
function validateMutableRange(
place: Place,
range: MutableRange,
description: string,
): void {
CompilerError.invariant(
(range.start === 0 && range.end === 0) || range.end > range.start,
{
reason: `Invalid mutable range: [${range.start}:${range.end}]`,
description: `${printPlace(place)} in ${description}`,
details: [
{
kind: 'error',
loc: place.loc,
message: null,
},
],
},
function validateMutableRange(mutableRange: MutableRange): void {
invariant(
(mutableRange.start === 0 && mutableRange.end === 0) ||
mutableRange.end > mutableRange.start,
'Identifier scope mutableRange was invalid: [%s:%s]',
mutableRange.start,
mutableRange.end,
);
}

View File

@@ -1,10 +1,3 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import {CompilerError} from '../CompilerError';
import {getScopes, recursivelyTraverseItems} from './AssertValidBlockNesting';
import {Environment} from './Environment';
@@ -234,14 +227,7 @@ function pushEndScopeTerminal(
const fallthroughId = context.fallthroughs.get(scope.id);
CompilerError.invariant(fallthroughId != null, {
reason: 'Expected scope to exist',
description: null,
details: [
{
kind: 'error',
loc: GeneratedSource,
message: null,
},
],
loc: GeneratedSource,
});
context.rewrites.push({
kind: 'EndScope',

View File

@@ -1,10 +1,3 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import {CompilerError} from '../CompilerError';
import {inRange} from '../ReactiveScopes/InferReactiveScopeVariables';
import {printDependency} from '../ReactiveScopes/PrintReactiveFunction';
@@ -19,7 +12,6 @@ import {
BasicBlock,
BlockId,
DependencyPathEntry,
FunctionExpression,
GeneratedSource,
getHookKind,
HIRFunction,
@@ -31,7 +23,6 @@ import {
PropertyLiteral,
ReactiveScopeDependency,
ScopeId,
TInstruction,
} from './HIR';
const DEBUG_PRINT = false;
@@ -129,33 +120,6 @@ export function collectHoistablePropertyLoads(
});
}
export function collectHoistablePropertyLoadsInInnerFn(
fnInstr: TInstruction<FunctionExpression>,
temporaries: ReadonlyMap<IdentifierId, ReactiveScopeDependency>,
hoistableFromOptionals: ReadonlyMap<BlockId, ReactiveScopeDependency>,
): ReadonlyMap<BlockId, BlockInfo> {
const fn = fnInstr.value.loweredFunc.func;
const initialContext: CollectHoistablePropertyLoadsContext = {
temporaries,
knownImmutableIdentifiers: new Set(),
hoistableFromOptionals,
registry: new PropertyPathRegistry(),
nestedFnImmutableContext: null,
assumedInvokedFns: fn.env.config.enableTreatFunctionDepsAsConditional
? new Set()
: getAssumedInvokedFunctions(fn),
};
const nestedFnImmutableContext = new Set(
fn.context
.filter(place =>
isImmutableAtInstr(place.identifier, fnInstr.id, initialContext),
)
.map(place => place.identifier.id),
);
initialContext.nestedFnImmutableContext = nestedFnImmutableContext;
return collectHoistablePropertyLoadsImpl(fn, initialContext);
}
type CollectHoistablePropertyLoadsContext = {
temporaries: ReadonlyMap<IdentifierId, ReactiveScopeDependency>;
knownImmutableIdentifiers: ReadonlySet<IdentifierId>;
@@ -241,10 +205,7 @@ type PropertyPathNode =
class PropertyPathRegistry {
roots: Map<IdentifierId, RootNode> = new Map();
getOrCreateIdentifier(
identifier: Identifier,
reactive: boolean,
): PropertyPathNode {
getOrCreateIdentifier(identifier: Identifier): PropertyPathNode {
/**
* Reads from a statically scoped variable are always safe in JS,
* with the exception of TDZ (not addressed by this pass).
@@ -258,26 +219,12 @@ class PropertyPathRegistry {
optionalProperties: new Map(),
fullPath: {
identifier,
reactive,
path: [],
},
hasOptional: false,
parent: null,
};
this.roots.set(identifier.id, rootNode);
} else {
CompilerError.invariant(reactive === rootNode.fullPath.reactive, {
reason:
'[HoistablePropertyLoads] Found inconsistencies in `reactive` flag when deduping identifier reads within the same scope',
description: null,
details: [
{
kind: 'error',
loc: identifier.loc,
message: null,
},
],
});
}
return rootNode;
}
@@ -295,7 +242,6 @@ class PropertyPathRegistry {
parent: parent,
fullPath: {
identifier: parent.fullPath.identifier,
reactive: parent.fullPath.reactive,
path: parent.fullPath.path.concat(entry),
},
hasOptional: parent.hasOptional || entry.optional,
@@ -311,7 +257,7 @@ class PropertyPathRegistry {
* so all subpaths of a PropertyLoad should already exist
* (e.g. a.b is added before a.b.c),
*/
let currNode = this.getOrCreateIdentifier(n.identifier, n.reactive);
let currNode = this.getOrCreateIdentifier(n.identifier);
if (n.path.length === 0) {
return currNode;
}
@@ -333,11 +279,10 @@ function getMaybeNonNullInInstruction(
instr: InstructionValue,
context: CollectHoistablePropertyLoadsContext,
): PropertyPathNode | null {
let path: ReactiveScopeDependency | null = null;
let path = null;
if (instr.kind === 'PropertyLoad') {
path = context.temporaries.get(instr.object.identifier.id) ?? {
identifier: instr.object.identifier,
reactive: instr.object.reactive,
path: [],
};
} else if (instr.kind === 'Destructure') {
@@ -400,7 +345,7 @@ function collectNonNullsInBlocks(
) {
const identifier = fn.params[0].identifier;
knownNonNullIdentifiers.add(
context.registry.getOrCreateIdentifier(identifier, true),
context.registry.getOrCreateIdentifier(identifier),
);
}
const nodes = new Map<
@@ -454,32 +399,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);
}
}
}
}
}
@@ -531,14 +450,7 @@ function propagateNonNull(
if (node == null) {
CompilerError.invariant(false, {
reason: `Bad node ${nodeId}, kind: ${direction}`,
description: null,
details: [
{
kind: 'error',
loc: GeneratedSource,
message: null,
},
],
loc: GeneratedSource,
});
}
const neighbors = Array.from(
@@ -610,14 +522,7 @@ function propagateNonNull(
CompilerError.invariant(i++ < 100, {
reason:
'[CollectHoistablePropertyLoads] fixed point iteration did not terminate after 100 loops',
description: null,
details: [
{
kind: 'error',
loc: GeneratedSource,
message: null,
},
],
loc: GeneratedSource,
});
changed = false;
@@ -649,13 +554,7 @@ export function assertNonNull<T extends NonNullable<U>, U>(
CompilerError.invariant(value != null, {
reason: 'Unexpected null',
description: source != null ? `(from ${source})` : null,
details: [
{
kind: 'error',
loc: GeneratedSource,
message: null,
},
],
loc: GeneratedSource,
});
return value;
}
@@ -681,11 +580,9 @@ function reduceMaybeOptionalChains(
changed = false;
for (const original of optionalChainNodes) {
let {identifier, path: origPath, reactive} = original.fullPath;
let currNode: PropertyPathNode = registry.getOrCreateIdentifier(
identifier,
reactive,
);
let {identifier, path: origPath} = original.fullPath;
let currNode: PropertyPathNode =
registry.getOrCreateIdentifier(identifier);
for (let i = 0; i < origPath.length; i++) {
const entry = origPath[i];
// If the base is known to be non-null, replace with a non-optional load

View File

@@ -1,10 +1,3 @@
/**
* 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 {assertNonNull} from './CollectHoistablePropertyLoads';
import {
@@ -186,13 +179,7 @@ function matchOptionalTestBlock(
reason:
'[OptionalChainDeps] Inconsistent optional chaining property load',
description: `Test=${printIdentifier(terminal.test.identifier)} PropertyLoad base=${printIdentifier(propertyLoad.value.object.identifier)}`,
details: [
{
kind: 'error',
loc: propertyLoad.loc,
message: null,
},
],
loc: propertyLoad.loc,
},
);
@@ -200,14 +187,7 @@ function matchOptionalTestBlock(
storeLocal.value.identifier.id === propertyLoad.lvalue.identifier.id,
{
reason: '[OptionalChainDeps] Unexpected storeLocal',
description: null,
details: [
{
kind: 'error',
loc: propertyLoad.loc,
message: null,
},
],
loc: propertyLoad.loc,
},
);
if (
@@ -224,14 +204,7 @@ function matchOptionalTestBlock(
alternate.instructions[1].value.kind === 'StoreLocal',
{
reason: 'Unexpected alternate structure',
description: null,
details: [
{
kind: 'error',
loc: terminal.loc,
message: null,
},
],
loc: terminal.loc,
},
);
@@ -267,14 +240,7 @@ function traverseOptionalBlock(
if (maybeTest.terminal.kind === 'branch') {
CompilerError.invariant(optional.terminal.optional, {
reason: '[OptionalChainDeps] Expect base case to be always optional',
description: null,
details: [
{
kind: 'error',
loc: optional.terminal.loc,
message: null,
},
],
loc: optional.terminal.loc,
});
/**
* Optional base expressions are currently within value blocks which cannot
@@ -312,19 +278,11 @@ function traverseOptionalBlock(
maybeTest.instructions.at(-1)!.lvalue.identifier.id,
{
reason: '[OptionalChainDeps] Unexpected test expression',
description: null,
details: [
{
kind: 'error',
loc: maybeTest.terminal.loc,
message: null,
},
],
loc: maybeTest.terminal.loc,
},
);
baseObject = {
identifier: maybeTest.instructions[0].value.place.identifier,
reactive: maybeTest.instructions[0].value.place.reactive,
path,
};
test = maybeTest.terminal;
@@ -408,14 +366,7 @@ function traverseOptionalBlock(
reason:
'[OptionalChainDeps] Unexpected instructions an inner optional block. ' +
'This indicates that the compiler may be incorrectly concatenating two unrelated optional chains',
description: null,
details: [
{
kind: 'error',
loc: optional.terminal.loc,
message: null,
},
],
loc: optional.terminal.loc,
});
}
const matchConsequentResult = matchOptionalTestBlock(test, context.blocks);
@@ -428,18 +379,11 @@ function traverseOptionalBlock(
{
reason: '[OptionalChainDeps] Unexpected optional goto-fallthrough',
description: `${matchConsequentResult.consequentGoto} != ${optional.terminal.fallthrough}`,
details: [
{
kind: 'error',
loc: optional.terminal.loc,
message: null,
},
],
loc: optional.terminal.loc,
},
);
const load = {
identifier: baseObject.identifier,
reactive: baseObject.reactive,
path: [
...baseObject.path,
{

View File

@@ -24,14 +24,7 @@ export function computeUnconditionalBlocks(fn: HIRFunction): Set<BlockId> {
CompilerError.invariant(!unconditionalBlocks.has(current), {
reason:
'Internal error: non-terminating loop in ComputeUnconditionalBlocks',
description: null,
details: [
{
kind: 'error',
loc: null,
message: null,
},
],
loc: null,
suggestions: null,
});
unconditionalBlocks.add(current);

View File

@@ -1,109 +0,0 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import {Effect, ValueKind} from '..';
import {TypeConfig} from './TypeSchema';
/**
* Libraries developed before we officially documented the [Rules of React](https://react.dev/reference/rules)
* implement APIs which cannot be memoized safely, either via manual or automatic memoization.
*
* Any non-hook API that is designed to be called during render (not events/effects) should be safe to memoize:
*
* ```js
* function Component() {
* const {someFunction} = useLibrary();
* // it should always be safe to memoize functions like this
* const result = useMemo(() => someFunction(), [someFunction]);
* }
* ```
*
* However, some APIs implement "interior mutability" — mutating values rather than copying into a new value
* and setting state with the new value. Such functions (`someFunction()` in the example) could return different
* values even though the function itself is the same object. This breaks memoization, since React relies on
* the outer object (or function) changing if part of its value has changed.
*
* Given that we didn't have the Rules of React precisely documented prior to the introduction of React compiler,
* it's understandable that some libraries accidentally shipped APIs that break this rule. However, developers
* can easily run into pitfalls with these APIs. They may manually memoize them, which can break their app. Or
* they may try using React Compiler, and think that the compiler has broken their code.
*
* To help ensure that developers can successfully use the compiler with existing code, this file teaches the
* compiler about specific APIs that are known to be incompatible with memoization. We've tried to be as precise
* as possible.
*
* The React team is open to collaborating with library authors to help develop compatible versions of these APIs,
* and we have already reached out to the teams who own any API listed here to ensure they are aware of the issue.
*/
export function defaultModuleTypeProvider(
moduleName: string,
): TypeConfig | null {
switch (moduleName) {
case 'react-hook-form': {
return {
kind: 'object',
properties: {
useForm: {
kind: 'hook',
returnType: {
kind: 'object',
properties: {
// Only the `watch()` function returned by react-hook-form's `useForm()` API is incompatible
watch: {
kind: 'function',
positionalParams: [],
restParam: Effect.Read,
calleeEffect: Effect.Read,
returnType: {kind: 'type', name: 'Any'},
returnValueKind: ValueKind.Mutable,
knownIncompatible: `React Hook Form's \`useForm()\` API returns a \`watch()\` function which cannot be memoized safely.`,
},
},
},
},
},
};
}
case '@tanstack/react-table': {
return {
kind: 'object',
properties: {
/*
* Many of the properties of `useReactTable()`'s return value are incompatible, so we mark the entire hook
* as incompatible
*/
useReactTable: {
kind: 'hook',
positionalParams: [],
restParam: Effect.Read,
returnType: {kind: 'type', name: 'Any'},
knownIncompatible: `TanStack Table's \`useReactTable()\` API returns functions that cannot be memoized safely`,
},
},
};
}
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

@@ -25,9 +25,8 @@ export class ReactiveScopeDependencyTreeHIR {
* `identifier.path`, or `identifier?.path` is in this map, it is safe to
* evaluate (non-optional) PropertyLoads from.
*/
#hoistableObjects: Map<Identifier, HoistableNode & {reactive: boolean}> =
new Map();
#deps: Map<Identifier, DependencyNode & {reactive: boolean}> = new Map();
#hoistableObjects: Map<Identifier, HoistableNode> = new Map();
#deps: Map<Identifier, DependencyNode> = new Map();
/**
* @param hoistableObjects a set of paths from which we can safely evaluate
@@ -36,10 +35,9 @@ export class ReactiveScopeDependencyTreeHIR {
* duplicates when traversing the CFG.
*/
constructor(hoistableObjects: Iterable<ReactiveScopeDependency>) {
for (const {path, identifier, reactive} of hoistableObjects) {
for (const {path, identifier} of hoistableObjects) {
let currNode = ReactiveScopeDependencyTreeHIR.#getOrCreateRoot(
identifier,
reactive,
this.#hoistableObjects,
path.length > 0 && path[0].optional ? 'Optional' : 'NonNull',
);
@@ -54,14 +52,7 @@ export class ReactiveScopeDependencyTreeHIR {
prevAccessType == null || prevAccessType === accessType,
{
reason: 'Conflicting access types',
description: null,
details: [
{
kind: 'error',
loc: GeneratedSource,
message: null,
},
],
loc: GeneratedSource,
},
);
let nextNode = currNode.properties.get(path[i].property);
@@ -79,8 +70,7 @@ export class ReactiveScopeDependencyTreeHIR {
static #getOrCreateRoot<T extends string>(
identifier: Identifier,
reactive: boolean,
roots: Map<Identifier, TreeNode<T> & {reactive: boolean}>,
roots: Map<Identifier, TreeNode<T>>,
defaultAccessType: T,
): TreeNode<T> {
// roots can always be accessed unconditionally in JS
@@ -89,22 +79,9 @@ export class ReactiveScopeDependencyTreeHIR {
if (rootNode === undefined) {
rootNode = {
properties: new Map(),
reactive,
accessType: defaultAccessType,
};
roots.set(identifier, rootNode);
} else {
CompilerError.invariant(reactive === rootNode.reactive, {
reason: '[DeriveMinimalDependenciesHIR] Conflicting reactive root flag',
description: `Identifier ${printIdentifier(identifier)}`,
details: [
{
kind: 'error',
loc: GeneratedSource,
message: null,
},
],
});
}
return rootNode;
}
@@ -115,10 +92,9 @@ export class ReactiveScopeDependencyTreeHIR {
* safe-to-evaluate subpath
*/
addDependency(dep: ReactiveScopeDependency): void {
const {identifier, reactive, path} = dep;
const {identifier, path} = dep;
let depCursor = ReactiveScopeDependencyTreeHIR.#getOrCreateRoot(
identifier,
reactive,
this.#deps,
PropertyAccessType.UnconditionalAccess,
);
@@ -196,13 +172,7 @@ export class ReactiveScopeDependencyTreeHIR {
deriveMinimalDependencies(): Set<ReactiveScopeDependency> {
const results = new Set<ReactiveScopeDependency>();
for (const [rootId, rootNode] of this.#deps.entries()) {
collectMinimalDependenciesInSubtree(
rootNode,
rootNode.reactive,
rootId,
[],
results,
);
collectMinimalDependenciesInSubtree(rootNode, rootId, [], results);
}
return results;
@@ -324,24 +294,25 @@ type HoistableNode = TreeNode<'Optional' | 'NonNull'>;
type DependencyNode = TreeNode<PropertyAccessType>;
/**
* TODO: this is directly pasted from DeriveMinimalDependencies. Since we no
* longer have conditionally accessed nodes, we can simplify
*
* Recursively calculates minimal dependencies in a subtree.
* @param node DependencyNode representing a dependency subtree.
* @returns a minimal list of dependencies in this subtree.
*/
function collectMinimalDependenciesInSubtree(
node: DependencyNode,
reactive: boolean,
rootIdentifier: Identifier,
path: Array<DependencyPathEntry>,
results: Set<ReactiveScopeDependency>,
): void {
if (isDependency(node.accessType)) {
results.add({identifier: rootIdentifier, reactive, path});
results.add({identifier: rootIdentifier, path});
} else {
for (const [childName, childNode] of node.properties) {
collectMinimalDependenciesInSubtree(
childNode,
reactive,
rootIdentifier,
[
...path,

View File

@@ -89,13 +89,7 @@ export class Dominator<T> {
CompilerError.invariant(dominator !== undefined, {
reason: 'Unknown node',
description: null,
details: [
{
kind: 'error',
loc: null,
message: null,
},
],
loc: null,
suggestions: null,
});
return dominator === id ? null : dominator;
@@ -136,13 +130,7 @@ export class PostDominator<T> {
CompilerError.invariant(dominator !== undefined, {
reason: 'Unknown node',
description: null,
details: [
{
kind: 'error',
loc: null,
message: null,
},
],
loc: null,
suggestions: null,
});
return dominator === id ? null : dominator;
@@ -187,13 +175,7 @@ function computeImmediateDominators<T>(graph: Graph<T>): Map<T, T> {
CompilerError.invariant(newIdom !== null, {
reason: `At least one predecessor must have been visited for block ${id}`,
description: null,
details: [
{
kind: 'error',
loc: null,
message: null,
},
],
loc: null,
suggestions: null,
});

View File

@@ -6,10 +6,18 @@
*/
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 {Logger, ProgramContext} from '../Entrypoint';
import {
CompilationMode,
defaultOptions,
Logger,
PanicThresholdOptions,
parsePluginOptions,
PluginOptions,
ProgramContext,
} from '../Entrypoint';
import {Err, Ok, Result} from '../Utils/Result';
import {
DEFAULT_GLOBALS,
@@ -47,10 +55,8 @@ import {
ShapeRegistry,
addHook,
} from './ObjectShape';
import {Scope as BabelScope, NodePath} from '@babel/traverse';
import {Scope as BabelScope} from '@babel/traverse';
import {TypeSchema} from './TypeSchema';
import {FlowTypeEnv} from '../Flood/Types';
import {defaultModuleTypeProvider} from './DefaultModuleTypeProvider';
export const ReactElementSymbolSchema = z.object({
elementSymbol: z.union([
@@ -152,14 +158,14 @@ export type Hook = z.infer<typeof HookSchema>;
* missing some recursive Object / Function shapeIds
*/
export const EnvironmentConfigSchema = z.object({
const EnvironmentConfigSchema = z.object({
customHooks: z.map(z.string(), HookSchema).default(new Map()),
/**
* 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
@@ -210,7 +216,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
@@ -245,12 +251,6 @@ export const EnvironmentConfigSchema = z.object({
*/
enableUseTypeAnnotations: z.boolean().default(false),
/**
* Allows specifying a function that can populate HIR with type information from
* Flow
*/
flowTypeProvider: z.nullable(z.any()).default(null),
/**
* Enables inference of optional dependency chains. Without this flag
* a property chain such as `props?.items?.foo` will infer as a dep on
@@ -261,8 +261,6 @@ export const EnvironmentConfigSchema = z.object({
enableFire: z.boolean().default(false),
enableNameAnonymousFunctions: z.boolean().default(false),
/**
* Enables inference and auto-insertion of effect dependencies. Takes in an array of
* configurable module and import pairs to allow for user-land experimentation. For example,
@@ -270,19 +268,21 @@ export const EnvironmentConfigSchema = z.object({
* {
* module: 'react',
* imported: 'useEffect',
* autodepsIndex: 1,
* numRequiredArgs: 1,
* },{
* module: 'MyExperimentalEffectHooks',
* imported: 'useExperimentalEffect',
* autodepsIndex: 2,
* numRequiredArgs: 2,
* },
* ]
* would insert dependencies for calls of `useEffect` imported from `react` and calls of
* useExperimentalEffect` from `MyExperimentalEffectHooks`.
*
* `autodepsIndex` tells the compiler which index we expect the AUTODEPS to appear in.
* With the configuration above, we'd insert dependencies for `useEffect` if it has two
* arguments, and the second is AUTODEPS.
* `numRequiredArgs` tells the compiler the amount of arguments required to append a dependency
* array to the end of the call. With the configuration above, we'd insert dependencies for
* `useEffect` if it is only given a single argument and it would be appended to the argument list.
*
* numRequiredArgs must always be greater than 0, otherwise there is no function to analyze for dependencies
*
* Still experimental.
*/
@@ -291,7 +291,7 @@ export const EnvironmentConfigSchema = z.object({
z.array(
z.object({
function: ExternalFunctionSchema,
autodepsIndex: z.number().min(1, 'autodepsIndex must be > 0'),
numRequiredArgs: z.number().min(1, 'numRequiredArgs must be > 0'),
}),
),
)
@@ -323,16 +323,10 @@ export const EnvironmentConfigSchema = z.object({
validateNoSetStateInRender: z.boolean().default(true),
/**
* Validates that setState is not called synchronously within an effect (useEffect and friends).
* Validates that setState is not called directly within a passive effect (useEffect).
* Scheduling a setState (with an event listener, subscription, etc) is valid.
*/
validateNoSetStateInEffects: z.boolean().default(false),
/**
* Validates that effects are not used to calculate derived data which could instead be computed
* during render.
*/
validateNoDerivedComputationsInEffects: z.boolean().default(false),
validateNoSetStateInPassiveEffects: z.boolean().default(false),
/**
* Validates against creating JSX within a try block and recommends using an error boundary
@@ -373,11 +367,6 @@ export const EnvironmentConfigSchema = z.object({
*/
validateNoImpureFunctionsInRender: z.boolean().default(false),
/**
* Validate against passing mutable functions to hooks
*/
validateNoFreezingKnownMutableFunctions: z.boolean().default(false),
/*
* When enabled, the compiler assumes that hooks follow the Rules of React:
* - Hooks may memoize computation based on any of their parameters, thus
@@ -619,14 +608,7 @@ export const EnvironmentConfigSchema = z.object({
*
* Here the variables `ref` and `myRef` will be typed as Refs.
*/
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),
enableTreatRefLikeIdentifiersAsRefs: z.boolean().default(false),
/*
* If specified a value, the compiler lowers any calls to `useContext` to use
@@ -649,35 +631,195 @@ export const EnvironmentConfigSchema = z.object({
* ```
*/
lowerContextAccess: ExternalFunctionSchema.nullable().default(null),
/**
* If enabled, will validate useMemos that don't return any values:
*
* Valid:
* useMemo(() => foo, [foo]);
* useMemo(() => { return foo }, [foo]);
* Invalid:
* useMemo(() => { ... }, [...]);
*/
validateNoVoidUseMemo: z.boolean().default(false),
/**
* Validates that Components/Hooks are always defined at module level. This prevents scope
* reference errors that occur when the compiler attempts to optimize the nested component/hook
* while its parent function remains uncompiled.
*/
validateNoDynamicallyCreatedComponentsOrHooks: z.boolean().default(false),
/**
* When enabled, allows setState calls in effects when the value being set is
* derived from a ref. This is useful for patterns where initial layout measurements
* from refs need to be stored in state during mount.
*/
enableAllowSetStateFromRefsInEffects: z.boolean().default(true),
});
export type EnvironmentConfig = z.infer<typeof EnvironmentConfigSchema>;
/**
* For test fixtures and playground only.
*
* Pragmas are straightforward to parse for boolean options (`:true` and
* `:false`). These are 'enabled' config values for non-boolean configs (i.e.
* what is used when parsing `:true`).
*/
const testComplexConfigDefaults: PartialEnvironmentConfig = {
validateNoCapitalizedCalls: [],
enableChangeDetectionForDebugging: {
source: 'react-compiler-runtime',
importSpecifierName: '$structuralCheck',
},
enableEmitFreeze: {
source: 'react-compiler-runtime',
importSpecifierName: 'makeReadOnly',
},
enableEmitInstrumentForget: {
fn: {
source: 'react-compiler-runtime',
importSpecifierName: 'useRenderCounter',
},
gating: {
source: 'react-compiler-runtime',
importSpecifierName: 'shouldInstrument',
},
globalGating: 'DEV',
},
enableEmitHookGuards: {
source: 'react-compiler-runtime',
importSpecifierName: '$dispatcherGuard',
},
inlineJsxTransform: {
elementSymbol: 'react.transitional.element',
globalDevVar: 'DEV',
},
lowerContextAccess: {
source: 'react-compiler-runtime',
importSpecifierName: 'useContext_withSelector',
},
inferEffectDependencies: [
{
function: {
source: 'react',
importSpecifierName: 'useEffect',
},
numRequiredArgs: 1,
},
{
function: {
source: 'shared-runtime',
importSpecifierName: 'useSpecialEffect',
},
numRequiredArgs: 2,
},
{
function: {
source: 'useEffectWrapper',
importSpecifierName: 'default',
},
numRequiredArgs: 1,
},
],
};
/**
* For snap test fixtures and playground only.
*/
function parseConfigPragmaEnvironmentForTest(
pragma: string,
): EnvironmentConfig {
const maybeConfig: any = {};
// Get the defaults to programmatically check for boolean properties
const defaultConfig = EnvironmentConfigSchema.parse({});
for (const token of pragma.split(' ')) {
if (!token.startsWith('@')) {
continue;
}
const keyVal = token.slice(1);
let [key, val = undefined] = keyVal.split(':');
const isSet = val === undefined || val === 'true';
if (isSet && key in testComplexConfigDefaults) {
maybeConfig[key] =
testComplexConfigDefaults[key as keyof PartialEnvironmentConfig];
continue;
}
if (key === 'customMacros' && val) {
const valSplit = val.split('.');
if (valSplit.length > 0) {
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;
}
if (
key !== 'enableResetCacheOnSourceFileChanges' &&
typeof defaultConfig[key as keyof EnvironmentConfig] !== 'boolean'
) {
// skip parsing non-boolean properties
continue;
}
if (val === undefined || val === 'true') {
maybeConfig[key] = true;
} else {
maybeConfig[key] = false;
}
}
const config = EnvironmentConfigSchema.safeParse(maybeConfig);
if (config.success) {
/**
* Unless explicitly enabled, do not insert HMR handling code
* in test fixtures or playground to reduce visual noise.
*/
if (config.data.enableResetCacheOnSourceFileChanges == null) {
config.data.enableResetCacheOnSourceFileChanges = false;
}
return config.data;
}
CompilerError.invariant(false, {
reason: 'Internal error, could not parse config from pragma string',
description: `${fromZodError(config.error)}`,
loc: null,
suggestions: null,
});
}
export function parseConfigPragmaForTests(
pragma: string,
defaults: {
compilationMode: CompilationMode;
},
): PluginOptions {
const environment = parseConfigPragmaEnvironmentForTest(pragma);
let compilationMode: CompilationMode = defaults.compilationMode;
let panicThreshold: PanicThresholdOptions = 'all_errors';
let noEmit: boolean = defaultOptions.noEmit;
for (const token of pragma.split(' ')) {
if (!token.startsWith('@')) {
continue;
}
switch (token) {
case '@compilationMode(annotation)': {
compilationMode = 'annotation';
break;
}
case '@compilationMode(infer)': {
compilationMode = 'infer';
break;
}
case '@compilationMode(all)': {
compilationMode = 'all';
break;
}
case '@compilationMode(syntax)': {
compilationMode = 'syntax';
break;
}
case '@panicThreshold(none)': {
panicThreshold = 'none';
break;
}
case '@noEmit': {
noEmit = true;
break;
}
}
}
return parsePluginOptions({
environment,
compilationMode,
panicThreshold,
noEmit,
});
}
export type PartialEnvironmentConfig = Partial<EnvironmentConfig>;
export type ReactFunctionType = 'Component' | 'Hook' | 'Other';
@@ -721,9 +863,6 @@ export class Environment {
#contextIdentifiers: Set<t.Identifier>;
#hoistedIdentifiers: Set<t.Identifier>;
parentFunction: NodePath<t.Function>;
#flowTypeEnvironment: FlowTypeEnv | null;
constructor(
scope: BabelScope,
@@ -731,7 +870,6 @@ export class Environment {
compilerMode: CompilerMode,
config: EnvironmentConfig,
contextIdentifiers: Set<t.Identifier>,
parentFunction: NodePath<t.Function>, // the outermost function being compiled
logger: Logger | null,
filename: string | null,
code: string | null,
@@ -766,13 +904,7 @@ export class Environment {
CompilerError.invariant(!this.#globals.has(hookName), {
reason: `[Globals] Found existing definition in global registry for custom hook ${hookName}`,
description: null,
details: [
{
kind: 'error',
loc: null,
message: null,
},
],
loc: null,
suggestions: null,
});
this.#globals.set(
@@ -796,43 +928,8 @@ export class Environment {
this.#moduleTypes.set(REANIMATED_MODULE_NAME, reanimatedModuleType);
}
this.parentFunction = parentFunction;
this.#contextIdentifiers = contextIdentifiers;
this.#hoistedIdentifiers = new Set();
if (config.flowTypeProvider != null) {
this.#flowTypeEnvironment = new FlowTypeEnv();
CompilerError.invariant(code != null, {
reason:
'Expected Environment to be initialized with source code when a Flow type provider is specified',
description: null,
details: [
{
kind: 'error',
loc: null,
message: null,
},
],
});
this.#flowTypeEnvironment.init(this, code);
} else {
this.#flowTypeEnvironment = null;
}
}
get typeContext(): FlowTypeEnv {
CompilerError.invariant(this.#flowTypeEnvironment != null, {
reason: 'Flow type environment not initialized',
description: null,
details: [
{
kind: 'error',
loc: null,
message: null,
},
],
});
return this.#flowTypeEnvironment;
}
get isInferredMemoEnabled(): boolean {
@@ -897,22 +994,10 @@ export class Environment {
#resolveModuleType(moduleName: string, loc: SourceLocation): Global | null {
let moduleType = this.#moduleTypes.get(moduleName);
if (moduleType === undefined) {
/*
* NOTE: Zod doesn't work when specifying a function as a default, so we have to
* fallback to the default value here
*/
const moduleTypeProvider =
this.config.moduleTypeProvider ?? defaultModuleTypeProvider;
if (moduleTypeProvider == null) {
if (this.config.moduleTypeProvider == null) {
return null;
}
if (typeof moduleTypeProvider !== 'function') {
CompilerError.throwInvalidConfig({
reason: `Expected a function for \`moduleTypeProvider\``,
loc,
});
}
const unparsedModuleConfig = moduleTypeProvider(moduleName);
const unparsedModuleConfig = this.config.moduleTypeProvider(moduleName);
if (unparsedModuleConfig != null) {
const parsedModuleConfig = TypeSchema.safeParse(unparsedModuleConfig);
if (!parsedModuleConfig.success) {
@@ -1086,13 +1171,7 @@ export class Environment {
CompilerError.invariant(shape !== undefined, {
reason: `[HIR] Forget internal error: cannot resolve shape ${shapeId}`,
description: null,
details: [
{
kind: 'error',
loc: null,
message: null,
},
],
loc: null,
suggestions: null,
});
return shape.properties.get('*') ?? null;
@@ -1117,13 +1196,7 @@ export class Environment {
CompilerError.invariant(shape !== undefined, {
reason: `[HIR] Forget internal error: cannot resolve shape ${shapeId}`,
description: null,
details: [
{
kind: 'error',
loc: null,
message: null,
},
],
loc: null,
suggestions: null,
});
if (typeof property === 'string') {
@@ -1148,13 +1221,7 @@ export class Environment {
CompilerError.invariant(shape !== undefined, {
reason: `[HIR] Forget internal error: cannot resolve shape ${shapeId}`,
description: null,
details: [
{
kind: 'error',
loc: null,
message: null,
},
],
loc: null,
suggestions: null,
});
return shape.functionType;

View File

@@ -184,13 +184,7 @@ function handleAssignment(
CompilerError.invariant(valuePath.isLVal(), {
reason: `[FindContextIdentifiers] Expected object property value to be an LVal, got: ${valuePath.type}`,
description: null,
details: [
{
kind: 'error',
loc: valuePath.node.loc ?? GeneratedSource,
message: null,
},
],
loc: valuePath.node.loc ?? GeneratedSource,
suggestions: null,
});
handleAssignment(currentFn, identifiers, valuePath);
@@ -198,13 +192,7 @@ function handleAssignment(
CompilerError.invariant(property.isRestElement(), {
reason: `[FindContextIdentifiers] Invalid assumptions for babel types.`,
description: null,
details: [
{
kind: 'error',
loc: property.node.loc ?? GeneratedSource,
message: null,
},
],
loc: property.node.loc ?? GeneratedSource,
suggestions: null,
});
handleAssignment(currentFn, identifiers, property);

View File

@@ -9,8 +9,6 @@ import {Effect, ValueKind, ValueReason} from './HIR';
import {
BUILTIN_SHAPES,
BuiltInArrayId,
BuiltInAutodepsId,
BuiltInFireFunctionId,
BuiltInFireId,
BuiltInMapId,
BuiltInMixedReadonlyId,
@@ -18,7 +16,6 @@ import {
BuiltInSetId,
BuiltInUseActionStateId,
BuiltInUseContextHookId,
BuiltInUseEffectEventId,
BuiltInUseEffectHookId,
BuiltInUseInsertionEffectHookId,
BuiltInUseLayoutEffectHookId,
@@ -27,10 +24,6 @@ import {
BuiltInUseRefId,
BuiltInUseStateId,
BuiltInUseTransitionId,
BuiltInWeakMapId,
BuiltInWeakSetId,
BuiltinEffectEventId,
ReanimatedSharedValueId,
ShapeRegistry,
addFunction,
addHook,
@@ -114,99 +107,6 @@ const TYPED_GLOBALS: Array<[string, BuiltInType]> = [
returnValueKind: ValueKind.Mutable,
}),
],
[
'entries',
addFunction(DEFAULT_SHAPES, [], {
positionalParams: [Effect.Capture],
restParam: null,
returnType: {kind: 'Object', shapeId: BuiltInArrayId},
calleeEffect: Effect.Read,
returnValueKind: ValueKind.Mutable,
aliasing: {
receiver: '@receiver',
params: ['@object'],
rest: null,
returns: '@returns',
temporaries: [],
effects: [
{
kind: 'Create',
into: '@returns',
reason: ValueReason.KnownReturnSignature,
value: ValueKind.Mutable,
},
// Object values are captured into the return
{
kind: 'Capture',
from: '@object',
into: '@returns',
},
],
},
}),
],
[
'keys',
addFunction(DEFAULT_SHAPES, [], {
positionalParams: [Effect.Read],
restParam: null,
returnType: {kind: 'Object', shapeId: BuiltInArrayId},
calleeEffect: Effect.Read,
returnValueKind: ValueKind.Mutable,
aliasing: {
receiver: '@receiver',
params: ['@object'],
rest: null,
returns: '@returns',
temporaries: [],
effects: [
{
kind: 'Create',
into: '@returns',
reason: ValueReason.KnownReturnSignature,
value: ValueKind.Mutable,
},
// Only keys are captured, and keys are immutable
{
kind: 'ImmutableCapture',
from: '@object',
into: '@returns',
},
],
},
}),
],
[
'values',
addFunction(DEFAULT_SHAPES, [], {
positionalParams: [Effect.Capture],
restParam: null,
returnType: {kind: 'Object', shapeId: BuiltInArrayId},
calleeEffect: Effect.Read,
returnValueKind: ValueKind.Mutable,
aliasing: {
receiver: '@receiver',
params: ['@object'],
rest: null,
returns: '@returns',
temporaries: [],
effects: [
{
kind: 'Create',
into: '@returns',
reason: ValueReason.KnownReturnSignature,
value: ValueKind.Mutable,
},
// Object values are captured into the return
{
kind: 'Capture',
from: '@object',
into: '@returns',
},
],
},
}),
],
]),
],
[
@@ -590,38 +490,6 @@ const TYPED_GLOBALS: Array<[string, BuiltInType]> = [
true,
),
],
[
'WeakMap',
addFunction(
DEFAULT_SHAPES,
[],
{
positionalParams: [Effect.ConditionallyMutateIterator],
restParam: null,
returnType: {kind: 'Object', shapeId: BuiltInWeakMapId},
calleeEffect: Effect.Read,
returnValueKind: ValueKind.Mutable,
},
null,
true,
),
],
[
'WeakSet',
addFunction(
DEFAULT_SHAPES,
[],
{
positionalParams: [Effect.ConditionallyMutateIterator],
restParam: null,
returnType: {kind: 'Object', shapeId: BuiltInWeakSetId},
calleeEffect: Effect.Read,
returnValueKind: ValueKind.Mutable,
},
null,
true,
),
],
// TODO: rest of Global objects
];
@@ -738,41 +606,6 @@ const REACT_APIS: Array<[string, BuiltInType]> = [
calleeEffect: Effect.Read,
hookKind: 'useEffect',
returnValueKind: ValueKind.Frozen,
aliasing: {
receiver: '@receiver',
params: [],
rest: '@rest',
returns: '@returns',
temporaries: ['@effect'],
effects: [
// Freezes the function and deps
{
kind: 'Freeze',
value: '@rest',
reason: ValueReason.Effect,
},
// Internally creates an effect object that captures the function and deps
{
kind: 'Create',
into: '@effect',
value: ValueKind.Frozen,
reason: ValueReason.KnownReturnSignature,
},
// The effect stores the function and dependencies
{
kind: 'Capture',
from: '@rest',
into: '@effect',
},
// Returns undefined
{
kind: 'Create',
into: '@returns',
value: ValueKind.Primitive,
reason: ValueReason.KnownReturnSignature,
},
],
},
},
BuiltInUseEffectHookId,
),
@@ -841,40 +674,13 @@ const REACT_APIS: Array<[string, BuiltInType]> = [
{
positionalParams: [],
restParam: null,
returnType: {
kind: 'Function',
return: {kind: 'Poly'},
shapeId: BuiltInFireFunctionId,
isConstructor: false,
},
returnType: {kind: 'Primitive'},
calleeEffect: Effect.Read,
returnValueKind: ValueKind.Frozen,
},
BuiltInFireId,
),
],
[
'useEffectEvent',
addHook(
DEFAULT_SHAPES,
{
positionalParams: [],
restParam: Effect.Freeze,
returnType: {
kind: 'Function',
return: {kind: 'Poly'},
shapeId: BuiltinEffectEventId,
isConstructor: false,
},
calleeEffect: Effect.Read,
hookKind: 'useEffectEvent',
// Frozen because it should not mutate any locally-bound values
returnValueKind: ValueKind.Frozen,
},
BuiltInUseEffectEventId,
),
],
['AUTODEPS', addObject(DEFAULT_SHAPES, BuiltInAutodepsId, [])],
];
TYPED_GLOBALS.push(
@@ -1000,8 +806,6 @@ export function installTypeConfig(
noAlias: typeConfig.noAlias === true,
mutableOnlyIfOperandsAreMutable:
typeConfig.mutableOnlyIfOperandsAreMutable === true,
aliasing: typeConfig.aliasing,
knownIncompatible: typeConfig.knownIncompatible ?? null,
});
}
case 'hook': {
@@ -1019,8 +823,6 @@ export function installTypeConfig(
),
returnValueKind: typeConfig.returnValueKind ?? ValueKind.Frozen,
noAlias: typeConfig.noAlias === true,
aliasing: typeConfig.aliasing,
knownIncompatible: typeConfig.knownIncompatible ?? null,
});
}
case 'object': {
@@ -1100,7 +902,7 @@ export function getReanimatedModuleType(registry: ShapeRegistry): ObjectType {
addHook(registry, {
positionalParams: [],
restParam: Effect.Freeze,
returnType: {kind: 'Object', shapeId: ReanimatedSharedValueId},
returnType: {kind: 'Poly'},
returnValueKind: ValueKind.Mutable,
noAlias: true,
calleeEffect: Effect.Read,

View File

@@ -7,19 +7,12 @@
import {BindingKind} from '@babel/traverse';
import * as t from '@babel/types';
import {
CompilerDiagnostic,
CompilerError,
ErrorCategory,
} from '../CompilerError';
import {CompilerError, CompilerErrorDetailOptions} from '../CompilerError';
import {assertExhaustive} from '../Utils/utils';
import {Environment, ReactFunctionType} from './Environment';
import type {HookKind} from './ObjectShape';
import {Type, makeType} from './Types';
import {z} from 'zod/v4';
import type {AliasingEffect} from '../Inference/AliasingEffects';
import {isReservedWord} from '../Utils/Keyword';
import {Err, Ok, Result} from '../Utils/Result';
import {z} from 'zod';
/*
* *******************************************************************************************
@@ -58,8 +51,7 @@ export type SourceLocation = t.SourceLocation | typeof GeneratedSource;
*/
export type ReactiveFunction = {
loc: SourceLocation;
id: ValidIdentifierName | null;
nameHint: string | null;
id: string | null;
params: Array<Place | SpreadPattern>;
generator: boolean;
async: boolean;
@@ -108,7 +100,6 @@ export type ReactiveInstruction = {
id: InstructionId;
lvalue: Place | null;
value: ReactiveValue;
effects?: Array<AliasingEffect> | null; // TODO make non-optional
loc: SourceLocation;
};
@@ -281,21 +272,36 @@ export type ReactiveTryTerminal = {
// A function lowered to HIR form, ie where its body is lowered to an HIR control-flow graph
export type HIRFunction = {
loc: SourceLocation;
id: ValidIdentifierName | null;
nameHint: string | null;
id: string | null;
fnType: ReactFunctionType;
env: Environment;
params: Array<Place | SpreadPattern>;
returnTypeAnnotation: t.FlowType | t.TSType | null;
returns: Place;
returnType: Type;
context: Array<Place>;
effects: Array<FunctionEffect> | null;
body: HIR;
generator: boolean;
async: boolean;
directives: Array<string>;
aliasingEffects: Array<AliasingEffect> | null;
};
export type FunctionEffect =
| {
kind: 'GlobalMutation';
error: CompilerErrorDetailOptions;
}
| {
kind: 'ReactMutation';
error: CompilerErrorDetailOptions;
}
| {
kind: 'ContextMutation';
places: ReadonlySet<Place>;
effect: Effect;
loc: SourceLocation;
};
/*
* Each reactive scope may have its own control-flow, so the instructions form
* a control-flow graph. The graph comprises a set of basic blocks which reference
@@ -437,25 +443,12 @@ export type ThrowTerminal = {
};
export type Case = {test: Place | null; block: BlockId};
export type ReturnVariant = 'Void' | 'Implicit' | 'Explicit';
export type ReturnTerminal = {
kind: 'return';
/**
* Void:
* () => { ... }
* function() { ... }
* Implicit (ArrowFunctionExpression only):
* () => foo
* Explicit:
* () => { return ... }
* function () { return ... }
*/
returnVariant: ReturnVariant;
loc: SourceLocation;
value: Place;
id: InstructionId;
fallthrough?: never;
effects: Array<AliasingEffect> | null;
};
export type GotoTerminal = {
@@ -616,7 +609,6 @@ export type MaybeThrowTerminal = {
id: InstructionId;
loc: SourceLocation;
fallthrough?: never;
effects: Array<AliasingEffect> | null;
};
export type ReactiveScopeTerminal = {
@@ -653,14 +645,12 @@ export type Instruction = {
lvalue: Place;
value: InstructionValue;
loc: SourceLocation;
effects: Array<AliasingEffect> | null;
};
export type TInstruction<T extends InstructionValue> = {
id: InstructionId;
lvalue: Place;
value: T;
effects: Array<AliasingEffect> | null;
loc: SourceLocation;
};
@@ -756,27 +746,6 @@ export enum InstructionKind {
Function = 'Function',
}
export function convertHoistedLValueKind(
kind: InstructionKind,
): InstructionKind | null {
switch (kind) {
case InstructionKind.HoistedLet:
return InstructionKind.Let;
case InstructionKind.HoistedConst:
return InstructionKind.Const;
case InstructionKind.HoistedFunction:
return InstructionKind.Function;
case InstructionKind.Let:
case InstructionKind.Const:
case InstructionKind.Function:
case InstructionKind.Reassign:
case InstructionKind.Catch:
return null;
default:
assertExhaustive(kind, 'Unexpected lvalue kind');
}
}
function _staticInvariantInstructionValueHasLocation(
value: InstructionValue,
): SourceLocation {
@@ -911,20 +880,8 @@ export type InstructionValue =
| StoreLocal
| {
kind: 'StoreContext';
/**
* StoreContext kinds:
* Reassign: context variable reassignment in source
* Const: const declaration + assignment in source
* ('const' context vars are ones whose declarations are hoisted)
* Let: let declaration + assignment in source
* Function: function declaration in source (similar to `const`)
*/
lvalue: {
kind:
| InstructionKind.Reassign
| InstructionKind.Const
| InstructionKind.Let
| InstructionKind.Function;
kind: InstructionKind.Reassign;
place: Place;
};
value: Place;
@@ -953,21 +910,13 @@ export type InstructionValue =
value: Place;
loc: SourceLocation;
}
| ({
| {
kind: 'TypeCastExpression';
value: Place;
typeAnnotation: t.FlowType | t.TSType;
type: Type;
loc: SourceLocation;
} & (
| {
typeAnnotation: t.FlowType;
typeAnnotationKind: 'cast';
}
| {
typeAnnotation: t.TSType;
typeAnnotationKind: 'as' | 'satisfies';
}
))
}
| JsxExpression
| {
kind: 'ObjectExpression';
@@ -1130,8 +1079,7 @@ export type JsxAttribute =
export type FunctionExpression = {
kind: 'FunctionExpression';
name: ValidIdentifierName | null;
nameHint: string | null;
name: string | null;
loweredFunc: LoweredFunction;
type:
| 'ArrowFunctionExpression'
@@ -1306,52 +1254,22 @@ export function forkTemporaryIdentifier(
};
}
export function validateIdentifierName(
name: string,
): Result<ValidatedIdentifier, CompilerError> {
if (isReservedWord(name)) {
const error = new CompilerError();
error.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.Syntax,
reason: 'Expected a non-reserved identifier name',
description: `\`${name}\` is a reserved word in JavaScript and cannot be used as an identifier name`,
suggestions: null,
}).withDetails({
kind: 'error',
loc: GeneratedSource,
message: 'reserved word',
}),
);
return Err(error);
} else if (!t.isValidIdentifier(name)) {
const error = new CompilerError();
error.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.Syntax,
reason: `Expected a valid identifier name`,
description: `\`${name}\` is not a valid JavaScript identifier`,
suggestions: null,
}).withDetails({
kind: 'error',
loc: GeneratedSource,
message: 'reserved word',
}),
);
}
return Ok({
kind: 'named',
value: name as ValidIdentifierName,
});
}
/**
* Creates a valid identifier name. This should *not* be used for synthesizing
* identifier names: only call this method for identifier names that appear in the
* original source code.
*/
export function makeIdentifierName(name: string): ValidatedIdentifier {
return validateIdentifierName(name).unwrap();
CompilerError.invariant(t.isValidIdentifier(name), {
reason: `Expected a valid identifier name`,
loc: GeneratedSource,
description: `\`${name}\` is not a valid JavaScript identifier`,
suggestions: null,
});
return {
kind: 'named',
value: name as ValidIdentifierName,
};
}
/**
@@ -1363,14 +1281,8 @@ export function makeIdentifierName(name: string): ValidatedIdentifier {
export function promoteTemporary(identifier: Identifier): void {
CompilerError.invariant(identifier.name === null, {
reason: `Expected a temporary (unnamed) identifier`,
loc: GeneratedSource,
description: `Identifier already has a name, \`${identifier.name}\``,
details: [
{
kind: 'error',
loc: GeneratedSource,
message: null,
},
],
suggestions: null,
});
identifier.name = {
@@ -1393,14 +1305,8 @@ export function isPromotedTemporary(name: string): boolean {
export function promoteTemporaryJsxTag(identifier: Identifier): void {
CompilerError.invariant(identifier.name === null, {
reason: `Expected a temporary (unnamed) identifier`,
loc: GeneratedSource,
description: `Identifier already has a name, \`${identifier.name}\``,
details: [
{
kind: 'error',
loc: GeneratedSource,
message: null,
},
],
suggestions: null,
});
identifier.name = {
@@ -1433,21 +1339,6 @@ export enum ValueReason {
*/
JsxCaptured = 'jsx-captured',
/**
* Argument to a hook
*/
HookCaptured = 'hook-captured',
/**
* Return value of a hook
*/
HookReturn = 'hook-return',
/**
* Passed to an effect
*/
Effect = 'effect',
/**
* Return value of a function with known frozen return value, e.g. `useState`.
*/
@@ -1498,20 +1389,6 @@ export const ValueKindSchema = z.enum([
ValueKind.Context,
]);
export const ValueReasonSchema = z.enum([
ValueReason.Context,
ValueReason.Effect,
ValueReason.Global,
ValueReason.HookCaptured,
ValueReason.HookReturn,
ValueReason.JsxCaptured,
ValueReason.KnownReturnSignature,
ValueReason.Other,
ValueReason.ReactiveFunctionArgument,
ValueReason.ReducerState,
ValueReason.State,
]);
// The effect with which a value is modified.
export enum Effect {
// Default value: not allowed after lifetime inference
@@ -1568,13 +1445,7 @@ export function isMutableEffect(
CompilerError.invariant(false, {
reason: 'Unexpected unknown effect',
description: null,
details: [
{
kind: 'error',
loc: location,
message: null,
},
],
loc: location,
suggestions: null,
});
}
@@ -1656,18 +1527,6 @@ export type DependencyPathEntry = {
export type DependencyPath = Array<DependencyPathEntry>;
export type ReactiveScopeDependency = {
identifier: Identifier;
/**
* Reflects whether the base identifier is reactive. Note that some reactive
* objects may have non-reactive properties, but we do not currently track
* this.
*
* ```js
* // Technically, result[0] is reactive and result[1] is not.
* // Currently, both dependencies would be marked as reactive.
* const result = useState();
* ```
*/
reactive: boolean;
path: DependencyPath;
};
@@ -1707,13 +1566,7 @@ export function makeBlockId(id: number): BlockId {
CompilerError.invariant(id >= 0 && Number.isInteger(id), {
reason: 'Expected block id to be a non-negative integer',
description: null,
details: [
{
kind: 'error',
loc: null,
message: null,
},
],
loc: null,
suggestions: null,
});
return id as BlockId;
@@ -1730,13 +1583,7 @@ export function makeScopeId(id: number): ScopeId {
CompilerError.invariant(id >= 0 && Number.isInteger(id), {
reason: 'Expected block id to be a non-negative integer',
description: null,
details: [
{
kind: 'error',
loc: null,
message: null,
},
],
loc: null,
suggestions: null,
});
return id as ScopeId;
@@ -1753,13 +1600,7 @@ export function makeIdentifierId(id: number): IdentifierId {
CompilerError.invariant(id >= 0 && Number.isInteger(id), {
reason: 'Expected identifier id to be a non-negative integer',
description: null,
details: [
{
kind: 'error',
loc: null,
message: null,
},
],
loc: null,
suggestions: null,
});
return id as IdentifierId;
@@ -1776,13 +1617,7 @@ export function makeDeclarationId(id: number): DeclarationId {
CompilerError.invariant(id >= 0 && Number.isInteger(id), {
reason: 'Expected declaration id to be a non-negative integer',
description: null,
details: [
{
kind: 'error',
loc: null,
message: null,
},
],
loc: null,
suggestions: null,
});
return id as DeclarationId;
@@ -1799,13 +1634,7 @@ export function makeInstructionId(id: number): InstructionId {
CompilerError.invariant(id >= 0 && Number.isInteger(id), {
reason: 'Expected instruction id to be a non-negative integer',
description: null,
details: [
{
kind: 'error',
loc: null,
message: null,
},
],
loc: null,
suggestions: null,
});
return id as InstructionId;
@@ -1851,26 +1680,10 @@ export function isUseStateType(id: Identifier): boolean {
return id.type.kind === 'Object' && id.type.shapeId === 'BuiltInUseState';
}
export function isJsxType(type: Type): boolean {
return type.kind === 'Object' && type.shapeId === 'BuiltInJsx';
}
export function isRefOrRefValue(id: Identifier): boolean {
return isUseRefType(id) || isRefValueType(id);
}
/*
* Returns true if the type is a Ref or a custom user type that acts like a ref when it
* shouldn't. For now the only other case of this is Reanimated's shared values.
*/
export function isRefOrRefLikeMutableType(type: Type): boolean {
return (
type.kind === 'Object' &&
(type.shapeId === 'BuiltInUseRefId' ||
type.shapeId == 'ReanimatedSharedValueId')
);
}
export function isSetStateType(id: Identifier): boolean {
return id.type.kind === 'Function' && id.type.shapeId === 'BuiltInSetState';
}
@@ -1901,19 +1714,6 @@ export function isDispatcherType(id: Identifier): boolean {
return id.type.kind === 'Function' && id.type.shapeId === 'BuiltInDispatch';
}
export function isFireFunctionType(id: Identifier): boolean {
return (
id.type.kind === 'Function' && id.type.shapeId === 'BuiltInFireFunction'
);
}
export function isEffectEventFunctionType(id: Identifier): boolean {
return (
id.type.kind === 'Function' &&
id.type.shapeId === 'BuiltInEffectEventFunction'
);
}
export function isStableType(id: Identifier): boolean {
return (
isSetStateType(id) ||
@@ -1924,40 +1724,6 @@ export function isStableType(id: Identifier): boolean {
);
}
export function isStableTypeContainer(id: Identifier): boolean {
const type_ = id.type;
if (type_.kind !== 'Object') {
return false;
}
return (
isUseStateType(id) || // setState
type_.shapeId === 'BuiltInUseActionState' || // setActionState
isUseReducerType(id) || // dispatcher
type_.shapeId === 'BuiltInUseTransition' // startTransition
);
}
export function evaluatesToStableTypeOrContainer(
env: Environment,
{value}: Instruction,
): boolean {
if (value.kind === 'CallExpression' || value.kind === 'MethodCall') {
const callee =
value.kind === 'CallExpression' ? value.callee : value.property;
const calleeHookKind = getHookKind(env, callee.identifier);
switch (calleeHookKind) {
case 'useState':
case 'useReducer':
case 'useActionState':
case 'useRef':
case 'useTransition':
return true;
}
}
return false;
}
export function isUseEffectHookType(id: Identifier): boolean {
return (
id.type.kind === 'Function' && id.type.shapeId === 'BuiltInUseEffectHook'

View File

@@ -7,7 +7,7 @@
import {Binding, NodePath} from '@babel/traverse';
import * as t from '@babel/types';
import {CompilerError, ErrorCategory} from '../CompilerError';
import {CompilerError} from '../CompilerError';
import {Environment} from './Environment';
import {
BasicBlock,
@@ -106,10 +106,11 @@ export default class HIRBuilder {
#current: WipBlock;
#entry: BlockId;
#scopes: Array<Scope> = [];
#context: Map<t.Identifier, SourceLocation>;
#context: Array<t.Identifier>;
#bindings: Bindings;
#env: Environment;
#exceptionHandlerStack: Array<BlockId> = [];
parentFunction: NodePath<t.Function>;
errors: CompilerError = new CompilerError();
/**
* Traversal context: counts the number of `fbt` tag parents
@@ -121,7 +122,7 @@ export default class HIRBuilder {
return this.#env.nextIdentifierId;
}
get context(): Map<t.Identifier, SourceLocation> {
get context(): Array<t.Identifier> {
return this.#context;
}
@@ -135,17 +136,16 @@ export default class HIRBuilder {
constructor(
env: Environment,
options?: {
bindings?: Bindings | null;
context?: Map<t.Identifier, SourceLocation>;
entryBlockKind?: BlockKind;
},
parentFunction: NodePath<t.Function>, // the outermost function being compiled
bindings: Bindings | null = null,
context: Array<t.Identifier> | null = null,
) {
this.#env = env;
this.#bindings = options?.bindings ?? new Map();
this.#context = options?.context ?? new Map();
this.#bindings = bindings ?? new Map();
this.parentFunction = parentFunction;
this.#context = context ?? [];
this.#entry = makeBlockId(env.nextBlockId);
this.#current = newBlock(this.#entry, options?.entryBlockKind ?? 'block');
this.#current = newBlock(this.#entry, 'block');
}
currentBlockKind(): BlockKind {
@@ -165,7 +165,6 @@ export default class HIRBuilder {
handler: exceptionHandler,
id: makeInstructionId(0),
loc: instruction.loc,
effects: null,
},
continuationBlock,
);
@@ -240,7 +239,7 @@ export default class HIRBuilder {
// Check if the binding is from module scope
const outerBinding =
this.#env.parentFunction.scope.parent.getBinding(originalName);
this.parentFunction.scope.parent.getBinding(originalName);
if (babelBinding === outerBinding) {
const path = babelBinding.path;
if (path.isImportDefaultSpecifier()) {
@@ -294,7 +293,7 @@ export default class HIRBuilder {
const binding = this.#resolveBabelBinding(path);
if (binding) {
// Check if the binding is from module scope, if so return null
const outerBinding = this.#env.parentFunction.scope.parent.getBinding(
const outerBinding = this.parentFunction.scope.parent.getBinding(
path.node.name,
);
if (binding === outerBinding) {
@@ -308,33 +307,9 @@ export default class HIRBuilder {
resolveBinding(node: t.Identifier): Identifier {
if (node.name === 'fbt') {
CompilerError.throwDiagnostic({
category: ErrorCategory.Todo,
reason: 'Support local variables named `fbt`',
description:
'Local variables named `fbt` may conflict with the fbt plugin and are not yet supported',
details: [
{
kind: 'error',
message: 'Rename to avoid conflict with fbt plugin',
loc: node.loc ?? GeneratedSource,
},
],
});
}
if (node.name === 'this') {
CompilerError.throwDiagnostic({
category: ErrorCategory.UnsupportedSyntax,
reason: '`this` is not supported syntax',
description:
'React Compiler does not support compiling functions that use `this`',
details: [
{
kind: 'error',
message: '`this` was used here',
loc: node.loc ?? GeneratedSource,
},
],
CompilerError.throwTodo({
reason: 'Support local variables named "fbt"',
loc: node.loc ?? null,
});
}
const originalName = node.name;
@@ -401,7 +376,7 @@ export default class HIRBuilder {
}
// Terminate the current block w the given terminal, and start a new block
terminate(terminal: Terminal, nextBlockKind: BlockKind | null): BlockId {
terminate(terminal: Terminal, nextBlockKind: BlockKind | null): void {
const {id: blockId, kind, instructions} = this.#current;
this.#completed.set(blockId, {
kind,
@@ -415,7 +390,6 @@ export default class HIRBuilder {
const nextId = this.#env.nextBlockId;
this.#current = newBlock(nextId, nextBlockKind);
}
return blockId;
}
/*
@@ -507,13 +481,7 @@ export default class HIRBuilder {
{
reason: 'Mismatched label',
description: null,
details: [
{
kind: 'error',
loc: null,
message: null,
},
],
loc: null,
suggestions: null,
},
);
@@ -536,13 +504,7 @@ export default class HIRBuilder {
{
reason: 'Mismatched label',
description: null,
details: [
{
kind: 'error',
loc: null,
message: null,
},
],
loc: null,
suggestions: null,
},
);
@@ -578,13 +540,7 @@ export default class HIRBuilder {
{
reason: 'Mismatched loops',
description: null,
details: [
{
kind: 'error',
loc: null,
message: null,
},
],
loc: null,
suggestions: null,
},
);
@@ -609,13 +565,7 @@ export default class HIRBuilder {
CompilerError.invariant(false, {
reason: 'Expected a loop or switch to be in scope',
description: null,
details: [
{
kind: 'error',
loc: null,
message: null,
},
],
loc: null,
suggestions: null,
});
}
@@ -636,13 +586,7 @@ export default class HIRBuilder {
CompilerError.invariant(false, {
reason: 'Continue may only refer to a labeled loop',
description: null,
details: [
{
kind: 'error',
loc: null,
message: null,
},
],
loc: null,
suggestions: null,
});
}
@@ -650,13 +594,7 @@ export default class HIRBuilder {
CompilerError.invariant(false, {
reason: 'Expected a loop to be in scope',
description: null,
details: [
{
kind: 'error',
loc: null,
message: null,
},
],
loc: null,
suggestions: null,
});
}
@@ -679,13 +617,7 @@ function _shrink(func: HIR): void {
CompilerError.invariant(block != null, {
reason: `expected block ${blockId} to exist`,
description: null,
details: [
{
kind: 'error',
loc: null,
message: null,
},
],
loc: null,
suggestions: null,
});
target = getTargetIfIndirection(block);
@@ -814,17 +746,6 @@ function getReversePostorderedBlocks(func: HIR): HIR['blocks'] {
* (eg bb2 then bb1), we ensure that they get reversed back to the correct order.
*/
const block = func.blocks.get(blockId)!;
CompilerError.invariant(block != null, {
reason: '[HIRBuilder] Unexpected null block',
description: `expected block ${blockId} to exist`,
details: [
{
kind: 'error',
loc: GeneratedSource,
message: null,
},
],
});
const successors = [...eachTerminalSuccessor(block.terminal)].reverse();
const fallthrough = terminalFallthrough(block.terminal);
@@ -879,13 +800,7 @@ export function markInstructionIds(func: HIR): void {
CompilerError.invariant(!visited.has(instr), {
reason: `${printInstruction(instr)} already visited!`,
description: null,
details: [
{
kind: 'error',
loc: instr.loc,
message: null,
},
],
loc: instr.loc,
suggestions: null,
});
visited.add(instr);
@@ -908,13 +823,7 @@ export function markPredecessors(func: HIR): void {
CompilerError.invariant(block != null, {
reason: 'unexpected missing block',
description: `block ${blockId}`,
details: [
{
kind: 'error',
loc: GeneratedSource,
message: null,
},
],
loc: GeneratedSource,
});
if (prevBlock) {
block.preds.add(prevBlock.id);

View File

@@ -12,7 +12,6 @@ import {
GeneratedSource,
HIRFunction,
Instruction,
Place,
} from './HIR';
import {markPredecessors} from './HIRBuilder';
import {terminalFallthrough, terminalHasFallthrough} from './visitors';
@@ -61,13 +60,7 @@ export function mergeConsecutiveBlocks(fn: HIRFunction): void {
CompilerError.invariant(predecessor !== undefined, {
reason: `Expected predecessor ${predecessorId} to exist`,
description: null,
details: [
{
kind: 'error',
loc: null,
message: null,
},
],
loc: null,
suggestions: null,
});
if (predecessor.terminal.kind !== 'goto' || predecessor.kind !== 'block') {
@@ -83,32 +76,24 @@ export function mergeConsecutiveBlocks(fn: HIRFunction): void {
CompilerError.invariant(phi.operands.size === 1, {
reason: `Found a block with a single predecessor but where a phi has multiple (${phi.operands.size}) operands`,
description: null,
details: [
{
kind: 'error',
loc: null,
message: null,
},
],
loc: null,
suggestions: null,
});
const operand = Array.from(phi.operands.values())[0]!;
const lvalue: Place = {
kind: 'Identifier',
identifier: phi.place.identifier,
effect: Effect.ConditionallyMutate,
reactive: false,
loc: GeneratedSource,
};
const instr: Instruction = {
id: predecessor.terminal.id,
lvalue: {...lvalue},
lvalue: {
kind: 'Identifier',
identifier: phi.place.identifier,
effect: Effect.ConditionallyMutate,
reactive: false,
loc: GeneratedSource,
},
value: {
kind: 'LoadLocal',
place: {...operand},
loc: GeneratedSource,
},
effects: [{kind: 'Alias', from: {...operand}, into: {...lvalue}}],
loc: GeneratedSource,
};
predecessor.instructions.push(instr);
@@ -119,17 +104,6 @@ export function mergeConsecutiveBlocks(fn: HIRFunction): void {
merged.merge(block.id, predecessorId);
fn.body.blocks.delete(block.id);
}
for (const [, block] of fn.body.blocks) {
for (const phi of block.phis) {
for (const [predecessorId, operand] of phi.operands) {
const mapped = merged.get(predecessorId);
if (mapped !== predecessorId) {
phi.operands.delete(predecessorId);
phi.operands.set(mapped, operand);
}
}
}
}
markPredecessors(fn.body);
for (const [, {terminal}] of fn.body.blocks) {
if (terminalHasFallthrough(terminal)) {

View File

@@ -1,10 +1,3 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import {
HIRFunction,
InstructionId,

View File

@@ -6,30 +6,14 @@
*/
import {CompilerError} from '../CompilerError';
import {AliasingEffect, AliasingSignature} from '../Inference/AliasingEffects';
import {assertExhaustive} from '../Utils/utils';
import {
Effect,
GeneratedSource,
Hole,
makeDeclarationId,
makeIdentifierId,
makeInstructionId,
Place,
SourceLocation,
SpreadPattern,
ValueKind,
ValueReason,
} from './HIR';
import {Effect, ValueKind, ValueReason} from './HIR';
import {
BuiltInType,
FunctionType,
makeType,
ObjectType,
PolyType,
PrimitiveType,
} from './Types';
import {AliasingEffectConfig, AliasingSignatureConfig} from './TypeSchema';
/*
* This file exports types and defaults for JavaScript object shapes. These are
@@ -58,20 +42,13 @@ function createAnonId(): string {
export function addFunction(
registry: ShapeRegistry,
properties: Iterable<[string, BuiltInType | PolyType]>,
fn: Omit<FunctionSignature, 'hookKind' | 'aliasing'> & {
aliasing?: AliasingSignatureConfig | null | undefined;
},
fn: Omit<FunctionSignature, 'hookKind'>,
id: string | null = null,
isConstructor: boolean = false,
): FunctionType {
const shapeId = id ?? createAnonId();
const aliasing =
fn.aliasing != null
? parseAliasingSignatureConfig(fn.aliasing, '<builtin>', GeneratedSource)
: null;
addShape(registry, shapeId, properties, {
...fn,
aliasing,
hookKind: null,
});
return {
@@ -89,18 +66,11 @@ export function addFunction(
*/
export function addHook(
registry: ShapeRegistry,
fn: Omit<FunctionSignature, 'aliasing'> & {
hookKind: HookKind;
aliasing?: AliasingSignatureConfig | null | undefined;
},
fn: FunctionSignature & {hookKind: HookKind},
id: string | null = null,
): FunctionType {
const shapeId = id ?? createAnonId();
const aliasing =
fn.aliasing != null
? parseAliasingSignatureConfig(fn.aliasing, '<builtin>', GeneratedSource)
: null;
addShape(registry, shapeId, [], {...fn, aliasing});
addShape(registry, shapeId, [], fn);
return {
kind: 'Function',
return: fn.returnType,
@@ -109,142 +79,6 @@ export function addHook(
};
}
function parseAliasingSignatureConfig(
typeConfig: AliasingSignatureConfig,
moduleName: string,
loc: SourceLocation,
): AliasingSignature {
const lifetimes = new Map<string, Place>();
function define(temp: string): Place {
CompilerError.invariant(!lifetimes.has(temp), {
reason: `Invalid type configuration for module`,
description: `Expected aliasing signature to have unique names for receiver, params, rest, returns, and temporaries in module '${moduleName}'`,
details: [
{
kind: 'error',
loc,
message: null,
},
],
});
const place = signatureArgument(lifetimes.size);
lifetimes.set(temp, place);
return place;
}
function lookup(temp: string): Place {
const place = lifetimes.get(temp);
CompilerError.invariant(place != null, {
reason: `Invalid type configuration for module`,
description: `Expected aliasing signature effects to reference known names from receiver/params/rest/returns/temporaries, but '${temp}' is not a known name in '${moduleName}'`,
details: [
{
kind: 'error',
loc,
message: null,
},
],
});
return place;
}
const receiver = define(typeConfig.receiver);
const params = typeConfig.params.map(define);
const rest = typeConfig.rest != null ? define(typeConfig.rest) : null;
const returns = define(typeConfig.returns);
const temporaries = typeConfig.temporaries.map(define);
const effects = typeConfig.effects.map(
(effect: AliasingEffectConfig): AliasingEffect => {
switch (effect.kind) {
case 'ImmutableCapture':
case 'CreateFrom':
case 'Capture':
case 'Alias':
case 'Assign': {
const from = lookup(effect.from);
const into = lookup(effect.into);
return {
kind: effect.kind,
from,
into,
};
}
case 'Mutate':
case 'MutateTransitiveConditionally': {
const value = lookup(effect.value);
return {kind: effect.kind, value};
}
case 'Create': {
const into = lookup(effect.into);
return {
kind: 'Create',
into,
reason: effect.reason,
value: effect.value,
};
}
case 'Freeze': {
const value = lookup(effect.value);
return {
kind: 'Freeze',
value,
reason: effect.reason,
};
}
case 'Impure': {
const place = lookup(effect.place);
return {
kind: 'Impure',
place,
error: CompilerError.throwTodo({
reason: 'Support impure effect declarations',
loc: GeneratedSource,
}),
};
}
case 'Apply': {
const receiver = lookup(effect.receiver);
const fn = lookup(effect.function);
const args: Array<Place | SpreadPattern | Hole> = effect.args.map(
arg => {
if (typeof arg === 'string') {
return lookup(arg);
} else if (arg.kind === 'Spread') {
return {kind: 'Spread', place: lookup(arg.place)};
} else {
return arg;
}
},
);
const into = lookup(effect.into);
return {
kind: 'Apply',
receiver,
function: fn,
mutatesFunction: effect.mutatesFunction,
args,
into,
loc,
signature: null,
};
}
default: {
assertExhaustive(
effect,
`Unexpected effect kind '${(effect as any).kind}'`,
);
}
}
},
);
return {
receiver: receiver.identifier.id,
params: params.map(p => p.identifier.id),
rest: rest != null ? rest.identifier.id : null,
returns: returns.identifier.id,
temporaries,
effects,
};
}
/*
* Add an object to an existing ShapeRegistry.
*
@@ -277,13 +111,7 @@ function addShape(
CompilerError.invariant(!registry.has(id), {
reason: `[ObjectShape] Could not add shape to registry: name ${id} already exists.`,
description: null,
details: [
{
kind: 'error',
loc: null,
message: null,
},
],
loc: null,
suggestions: null,
});
registry.set(id, shape);
@@ -303,7 +131,6 @@ export type HookKind =
| 'useCallback'
| 'useTransition'
| 'useImperativeHandle'
| 'useEffectEvent'
| 'Custom';
/*
@@ -350,11 +177,8 @@ export type FunctionSignature = {
mutableOnlyIfOperandsAreMutable?: boolean;
impure?: boolean;
knownIncompatible?: string | null | undefined;
canonicalName?: string;
aliasing?: AliasingSignature | null | undefined;
};
/*
@@ -379,8 +203,6 @@ export const BuiltInPropsId = 'BuiltInProps';
export const BuiltInArrayId = 'BuiltInArray';
export const BuiltInSetId = 'BuiltInSet';
export const BuiltInMapId = 'BuiltInMap';
export const BuiltInWeakSetId = 'BuiltInWeakSet';
export const BuiltInWeakMapId = 'BuiltInWeakMap';
export const BuiltInFunctionId = 'BuiltInFunction';
export const BuiltInJsxId = 'BuiltInJsx';
export const BuiltInObjectId = 'BuiltInObject';
@@ -401,13 +223,6 @@ export const BuiltInUseContextHookId = 'BuiltInUseContextHook';
export const BuiltInUseTransitionId = 'BuiltInUseTransition';
export const BuiltInStartTransitionId = 'BuiltInStartTransition';
export const BuiltInFireId = 'BuiltInFire';
export const BuiltInFireFunctionId = 'BuiltInFireFunction';
export const BuiltInUseEffectEventId = 'BuiltInUseEffectEvent';
export const BuiltinEffectEventId = 'BuiltInEffectEventFunction';
export const BuiltInAutodepsId = 'BuiltInAutoDepsId';
// See getReanimatedModuleType() in Globals.ts — this is part of supporting Reanimated's ref-like types
export const ReanimatedSharedValueId = 'ReanimatedSharedValueId';
// ShapeRegistry with default definitions for built-ins.
export const BUILTIN_SHAPES: ShapeRegistry = new Map();
@@ -481,30 +296,6 @@ addObject(BUILTIN_SHAPES, BuiltInArrayId, [
returnType: PRIMITIVE_TYPE,
calleeEffect: Effect.Store,
returnValueKind: ValueKind.Primitive,
aliasing: {
receiver: '@receiver',
params: [],
rest: '@rest',
returns: '@returns',
temporaries: [],
effects: [
// Push directly mutates the array itself
{kind: 'Mutate', value: '@receiver'},
// The arguments are captured into the array
{
kind: 'Capture',
from: '@rest',
into: '@receiver',
},
// Returns the new length, a primitive
{
kind: 'Create',
into: '@returns',
value: ValueKind.Primitive,
reason: ValueReason.KnownReturnSignature,
},
],
},
}),
],
[
@@ -535,60 +326,6 @@ addObject(BUILTIN_SHAPES, BuiltInArrayId, [
returnValueKind: ValueKind.Mutable,
noAlias: true,
mutableOnlyIfOperandsAreMutable: true,
aliasing: {
receiver: '@receiver',
params: ['@callback'],
rest: null,
returns: '@returns',
temporaries: [
// Temporary representing captured items of the receiver
'@item',
// Temporary representing the result of the callback
'@callbackReturn',
/*
* Undefined `this` arg to the callback. Note the signature does not
* support passing an explicit thisArg second param
*/
'@thisArg',
],
effects: [
// Map creates a new mutable array
{
kind: 'Create',
into: '@returns',
value: ValueKind.Mutable,
reason: ValueReason.KnownReturnSignature,
},
// The first arg to the callback is an item extracted from the receiver array
{
kind: 'CreateFrom',
from: '@receiver',
into: '@item',
},
// The undefined this for the callback
{
kind: 'Create',
into: '@thisArg',
value: ValueKind.Primitive,
reason: ValueReason.KnownReturnSignature,
},
// calls the callback, returning the result into a temporary
{
kind: 'Apply',
receiver: '@thisArg',
args: ['@item', {kind: 'Hole'}, '@receiver'],
function: '@callback',
into: '@callbackReturn',
mutatesFunction: false,
},
// captures the result of the callback into the return array
{
kind: 'Capture',
from: '@callbackReturn',
into: '@returns',
},
],
},
}),
],
[
@@ -736,32 +473,6 @@ addObject(BUILTIN_SHAPES, BuiltInSetId, [
calleeEffect: Effect.Store,
// returnValueKind is technically dependent on the ValueKind of the set itself
returnValueKind: ValueKind.Mutable,
aliasing: {
receiver: '@receiver',
params: [],
rest: '@rest',
returns: '@returns',
temporaries: [],
effects: [
// Set.add returns the receiver Set
{
kind: 'Assign',
from: '@receiver',
into: '@returns',
},
// Set.add mutates the set itself
{
kind: 'Mutate',
value: '@receiver',
},
// Captures the rest params into the set
{
kind: 'Capture',
from: '@rest',
into: '@receiver',
},
],
},
}),
],
[
@@ -1052,101 +763,6 @@ addObject(BUILTIN_SHAPES, BuiltInMapId, [
],
]);
addObject(BUILTIN_SHAPES, BuiltInWeakSetId, [
[
/**
* add(value)
* Parameters
* value: the value of the element to add to the Set object.
* Returns the Set object with added value.
*/
'add',
addFunction(BUILTIN_SHAPES, [], {
positionalParams: [Effect.Capture],
restParam: null,
returnType: {kind: 'Object', shapeId: BuiltInWeakSetId},
calleeEffect: Effect.Store,
// returnValueKind is technically dependent on the ValueKind of the set itself
returnValueKind: ValueKind.Mutable,
}),
],
[
/**
* setInstance.delete(value)
* Returns true if value was already in Set; otherwise false.
*/
'delete',
addFunction(BUILTIN_SHAPES, [], {
positionalParams: [Effect.Read],
restParam: null,
returnType: PRIMITIVE_TYPE,
calleeEffect: Effect.Store,
returnValueKind: ValueKind.Primitive,
}),
],
[
'has',
addFunction(BUILTIN_SHAPES, [], {
positionalParams: [Effect.Read],
restParam: null,
returnType: PRIMITIVE_TYPE,
calleeEffect: Effect.Read,
returnValueKind: ValueKind.Primitive,
}),
],
]);
addObject(BUILTIN_SHAPES, BuiltInWeakMapId, [
[
'delete',
addFunction(BUILTIN_SHAPES, [], {
positionalParams: [Effect.Read],
restParam: null,
returnType: PRIMITIVE_TYPE,
calleeEffect: Effect.Store,
returnValueKind: ValueKind.Primitive,
}),
],
[
'get',
addFunction(BUILTIN_SHAPES, [], {
positionalParams: [Effect.Read],
restParam: null,
returnType: {kind: 'Poly'},
calleeEffect: Effect.Capture,
returnValueKind: ValueKind.Mutable,
}),
],
[
'has',
addFunction(BUILTIN_SHAPES, [], {
positionalParams: [Effect.Read],
restParam: null,
returnType: PRIMITIVE_TYPE,
calleeEffect: Effect.Read,
returnValueKind: ValueKind.Primitive,
}),
],
[
/**
* Params
* key: the key of the element to add to the Map object. The key may be
* any JavaScript type (any primitive value or any type of JavaScript
* object).
* value: the value of the element to add to the Map object.
* Returns the Map object.
*/
'set',
addFunction(BUILTIN_SHAPES, [], {
positionalParams: [Effect.Capture, Effect.Capture],
restParam: null,
returnType: {kind: 'Object', shapeId: BuiltInWeakMapId},
calleeEffect: Effect.Store,
returnValueKind: ValueKind.Mutable,
}),
],
]);
addObject(BUILTIN_SHAPES, BuiltInUseStateId, [
['0', {kind: 'Poly'}],
[
@@ -1231,21 +847,6 @@ addObject(BUILTIN_SHAPES, BuiltInRefValueId, [
['*', {kind: 'Object', shapeId: BuiltInRefValueId}],
]);
addObject(BUILTIN_SHAPES, ReanimatedSharedValueId, []);
addFunction(
BUILTIN_SHAPES,
[],
{
positionalParams: [],
restParam: Effect.ConditionallyMutate,
returnType: {kind: 'Poly'},
calleeEffect: Effect.ConditionallyMutate,
returnValueKind: ValueKind.Mutable,
},
BuiltinEffectEventId,
);
/**
* MixedReadOnly =
* | primitive
@@ -1464,53 +1065,6 @@ export const DefaultNonmutatingHook = addHook(
calleeEffect: Effect.Read,
hookKind: 'Custom',
returnValueKind: ValueKind.Frozen,
aliasing: {
receiver: '@receiver',
params: [],
rest: '@rest',
returns: '@returns',
temporaries: [],
effects: [
// Freeze the arguments
{
kind: 'Freeze',
value: '@rest',
reason: ValueReason.HookCaptured,
},
// Returns a frozen value
{
kind: 'Create',
into: '@returns',
value: ValueKind.Frozen,
reason: ValueReason.HookReturn,
},
// May alias any arguments into the return
{
kind: 'Alias',
from: '@rest',
into: '@returns',
},
],
},
},
'DefaultNonmutatingHook',
);
export function signatureArgument(id: number): Place {
const place: Place = {
kind: 'Identifier',
effect: Effect.Unknown,
loc: GeneratedSource,
reactive: false,
identifier: {
declarationId: makeDeclarationId(id),
id: makeIdentifierId(id),
loc: GeneratedSource,
mutableRange: {start: makeInstructionId(0), end: makeInstructionId(0)},
name: null,
scope: null,
type: makeType(),
},
};
return place;
}

View File

@@ -5,6 +5,7 @@
* LICENSE file in the root directory of this source tree.
*/
import generate from '@babel/generator';
import {CompilerError} from '../CompilerError';
import {printReactiveScopeSummary} from '../ReactiveScopes/PrintReactiveFunction';
import DisjointSet from '../Utils/DisjointSet';
@@ -34,7 +35,6 @@ import type {
Type,
} from './HIR';
import {GotoVariant, InstructionKind} from './HIR';
import {AliasingEffect, AliasingSignature} from '../Inference/AliasingEffects';
export type Options = {
indent: number;
@@ -53,11 +53,6 @@ export function printFunction(fn: HIRFunction): string {
let definition = '';
if (fn.id !== null) {
definition += fn.id;
} else {
definition += '<<anonymous>>';
}
if (fn.nameHint != null) {
definition += ` ${fn.nameHint}`;
}
if (fn.params.length !== 0) {
definition +=
@@ -72,13 +67,13 @@ export function printFunction(fn: HIRFunction): string {
})
.join(', ') +
')';
} else {
definition += '()';
}
definition += `: ${printPlace(fn.returns)}`;
output.push(definition);
output.push(...fn.directives);
if (definition.length !== 0) {
output.push(definition);
}
output.push(printType(fn.returnType));
output.push(printHIR(fn.body));
output.push(...fn.directives);
return output.join('\n');
}
@@ -156,10 +151,7 @@ export function printMixedHIR(
export function printInstruction(instr: ReactiveInstruction): string {
const id = `[${instr.id}]`;
let value = printInstructionValue(instr.value);
if (instr.effects != null) {
value += `\n ${instr.effects.map(printAliasingEffect).join('\n ')}`;
}
const value = printInstructionValue(instr.value);
if (instr.lvalue !== null) {
return `${id} ${printPlace(instr.lvalue)} = ${value}`;
@@ -218,12 +210,9 @@ export function printTerminal(terminal: Terminal): Array<string> | string {
break;
}
case 'return': {
value = `[${terminal.id}] Return ${terminal.returnVariant}${
value = `[${terminal.id}] Return${
terminal.value != null ? ' ' + printPlace(terminal.value) : ''
}`;
if (terminal.effects != null) {
value += `\n ${terminal.effects.map(printAliasingEffect).join('\n ')}`;
}
break;
}
case 'goto': {
@@ -292,9 +281,6 @@ export function printTerminal(terminal: Terminal): Array<string> | string {
}
case 'maybe-throw': {
value = `[${terminal.id}] MaybeThrow continuation=bb${terminal.continuation} handler=bb${terminal.handler}`;
if (terminal.effects != null) {
value += `\n ${terminal.effects.map(printAliasingEffect).join('\n ')}`;
}
break;
}
case 'scope': {
@@ -468,7 +454,7 @@ export function printInstructionValue(instrValue: ReactiveValue): string {
break;
}
case 'UnsupportedNode': {
value = `UnsupportedNode ${instrValue.node.type}`;
value = `UnsupportedNode(${generate(instrValue.node).code})`;
break;
}
case 'LoadLocal': {
@@ -557,11 +543,20 @@ export function printInstructionValue(instrValue: ReactiveValue): string {
const context = instrValue.loweredFunc.func.context
.map(dep => printPlace(dep))
.join(',');
const aliasingEffects =
instrValue.loweredFunc.func.aliasingEffects
?.map(printAliasingEffect)
?.join(', ') ?? '';
value = `${kind} ${name} @context[${context}] @aliasingEffects=[${aliasingEffects}]\n${fn}`;
const effects =
instrValue.loweredFunc.func.effects
?.map(effect => {
if (effect.kind === 'ContextMutation') {
return `ContextMutation places=[${[...effect.places]
.map(place => printPlace(place))
.join(', ')}] effect=${effect.effect}`;
} else {
return `GlobalMutation`;
}
})
.join(', ') ?? '';
const type = printType(instrValue.loweredFunc.func.returnType).trim();
value = `${kind} ${name} @context[${context}] @effects[${effects}]${type !== '' ? ` return${type}` : ''}:\n${fn}`;
break;
}
case 'TaggedTemplateExpression': {
@@ -599,13 +594,7 @@ export function printInstructionValue(instrValue: ReactiveValue): string {
{
reason: 'Bad assumption about quasi length.',
description: null,
details: [
{
kind: 'error',
loc: instrValue.loc,
message: null,
},
],
loc: instrValue.loc,
suggestions: null,
},
);
@@ -711,7 +700,7 @@ export function printInstructionValue(instrValue: ReactiveValue): string {
break;
}
case 'FinishMemoize': {
value = `FinishMemoize decl=${printPlace(instrValue.decl)}${instrValue.pruned ? ' pruned' : ''}`;
value = `FinishMemoize decl=${printPlace(instrValue.decl)}`;
break;
}
default: {
@@ -874,15 +863,8 @@ export function printManualMemoDependency(
} else {
CompilerError.invariant(val.root.value.identifier.name?.kind === 'named', {
reason: 'DepsValidation: expected named local variable in depslist',
description: null,
suggestions: null,
details: [
{
kind: 'error',
loc: val.root.value.loc,
message: null,
},
],
loc: val.root.value.loc,
});
rootStr = nameOnly
? val.root.value.identifier.name.value
@@ -896,8 +878,7 @@ export function printType(type: Type): string {
if (type.kind === 'Object' && type.shapeId != null) {
return `:T${type.kind}<${type.shapeId}>`;
} else if (type.kind === 'Function' && type.shapeId != null) {
const returnType = printType(type.return);
return `:T${type.kind}<${type.shapeId}>()${returnType !== '' ? `: ${returnType}` : ''}`;
return `:T${type.kind}<${type.shapeId}>`;
} else {
return `:T${type.kind}`;
}
@@ -941,110 +922,3 @@ function getFunctionName(
return defaultValue;
}
}
export function printAliasingEffect(effect: AliasingEffect): string {
switch (effect.kind) {
case 'Assign': {
return `Assign ${printPlaceForAliasEffect(effect.into)} = ${printPlaceForAliasEffect(effect.from)}`;
}
case 'Alias': {
return `Alias ${printPlaceForAliasEffect(effect.into)} <- ${printPlaceForAliasEffect(effect.from)}`;
}
case 'MaybeAlias': {
return `MaybeAlias ${printPlaceForAliasEffect(effect.into)} <- ${printPlaceForAliasEffect(effect.from)}`;
}
case 'Capture': {
return `Capture ${printPlaceForAliasEffect(effect.into)} <- ${printPlaceForAliasEffect(effect.from)}`;
}
case 'ImmutableCapture': {
return `ImmutableCapture ${printPlaceForAliasEffect(effect.into)} <- ${printPlaceForAliasEffect(effect.from)}`;
}
case 'Create': {
return `Create ${printPlaceForAliasEffect(effect.into)} = ${effect.value}`;
}
case 'CreateFrom': {
return `Create ${printPlaceForAliasEffect(effect.into)} = kindOf(${printPlaceForAliasEffect(effect.from)})`;
}
case 'CreateFunction': {
return `Function ${printPlaceForAliasEffect(effect.into)} = Function captures=[${effect.captures.map(printPlaceForAliasEffect).join(', ')}]`;
}
case 'Apply': {
const receiverCallee =
effect.receiver.identifier.id === effect.function.identifier.id
? printPlaceForAliasEffect(effect.receiver)
: `${printPlaceForAliasEffect(effect.receiver)}.${printPlaceForAliasEffect(effect.function)}`;
const args = effect.args
.map(arg => {
if (arg.kind === 'Identifier') {
return printPlaceForAliasEffect(arg);
} else if (arg.kind === 'Hole') {
return ' ';
}
return `...${printPlaceForAliasEffect(arg.place)}`;
})
.join(', ');
let signature = '';
if (effect.signature != null) {
if (effect.signature.aliasing != null) {
signature = printAliasingSignature(effect.signature.aliasing);
} else {
signature = JSON.stringify(effect.signature, null, 2);
}
}
return `Apply ${printPlaceForAliasEffect(effect.into)} = ${receiverCallee}(${args})${signature != '' ? '\n ' : ''}${signature}`;
}
case 'Freeze': {
return `Freeze ${printPlaceForAliasEffect(effect.value)} ${effect.reason}`;
}
case 'Mutate':
case 'MutateConditionally':
case 'MutateTransitive':
case 'MutateTransitiveConditionally': {
return `${effect.kind} ${printPlaceForAliasEffect(effect.value)}${effect.kind === 'Mutate' && effect.reason?.kind === 'AssignCurrentProperty' ? ' (assign `.current`)' : ''}`;
}
case 'MutateFrozen': {
return `MutateFrozen ${printPlaceForAliasEffect(effect.place)} reason=${JSON.stringify(effect.error.reason)}`;
}
case 'MutateGlobal': {
return `MutateGlobal ${printPlaceForAliasEffect(effect.place)} reason=${JSON.stringify(effect.error.reason)}`;
}
case 'Impure': {
return `Impure ${printPlaceForAliasEffect(effect.place)} reason=${JSON.stringify(effect.error.reason)}`;
}
case 'Render': {
return `Render ${printPlaceForAliasEffect(effect.place)}`;
}
default: {
assertExhaustive(effect, `Unexpected kind '${(effect as any).kind}'`);
}
}
}
function printPlaceForAliasEffect(place: Place): string {
return printIdentifier(place.identifier);
}
export function printAliasingSignature(signature: AliasingSignature): string {
const tokens: Array<string> = ['function '];
if (signature.temporaries.length !== 0) {
tokens.push('<');
tokens.push(
signature.temporaries.map(temp => `$${temp.identifier.id}`).join(', '),
);
tokens.push('>');
}
tokens.push('(');
tokens.push('this=$' + String(signature.receiver));
for (const param of signature.params) {
tokens.push(', $' + String(param));
}
if (signature.rest != null) {
tokens.push(`, ...$${String(signature.rest)}`);
}
tokens.push('): ');
tokens.push('$' + String(signature.returns) + ':');
for (const effect of signature.effects) {
tokens.push('\n ' + printAliasingEffect(effect));
}
return tokens.join('');
}

View File

@@ -1,10 +1,3 @@
/**
* 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 {
ScopeId,
HIRFunction,
@@ -30,7 +23,6 @@ import {
FunctionExpression,
ObjectMethod,
PropertyLiteral,
convertHoistedLValueKind,
} from './HIR';
import {
collectHoistablePropertyLoads,
@@ -86,14 +78,7 @@ export function propagateScopeDependenciesHIR(fn: HIRFunction): void {
const hoistables = hoistablePropertyLoads.get(scope.id);
CompilerError.invariant(hoistables != null, {
reason: '[PropagateScopeDependencies] Scope not found in tracked blocks',
description: null,
details: [
{
kind: 'error',
loc: GeneratedSource,
message: null,
},
],
loc: GeneratedSource,
});
/**
* Step 2: Calculate hoistable dependencies.
@@ -124,7 +109,7 @@ export function propagateScopeDependenciesHIR(fn: HIRFunction): void {
}
}
export function findTemporariesUsedOutsideDeclaringScope(
function findTemporariesUsedOutsideDeclaringScope(
fn: HIRFunction,
): ReadonlySet<DeclarationId> {
/*
@@ -254,18 +239,12 @@ function isLoadContextMutable(
id: InstructionId,
): instrValue is LoadContext {
if (instrValue.kind === 'LoadContext') {
/**
* Not all context variables currently have scopes due to limitations of
* mutability analysis for function expressions.
*
* Currently, many function expressions references are inferred to be
* 'Read' | 'Freeze' effects which don't replay mutable effects of captured
* context.
*/
return (
instrValue.place.identifier.scope != null &&
id >= instrValue.place.identifier.scope.range.end
);
CompilerError.invariant(instrValue.place.identifier.scope != null, {
reason:
'[PropagateScopeDependencies] Expected all context variables to be assigned a scope',
loc: instrValue.loc,
});
return id >= instrValue.place.identifier.scope.range.end;
}
return false;
}
@@ -323,7 +302,6 @@ function collectTemporariesSidemapImpl(
) {
temporaries.set(lvalue.identifier.id, {
identifier: value.place.identifier,
reactive: value.place.reactive,
path: [],
});
}
@@ -377,13 +355,11 @@ function getProperty(
if (resolvedDependency == null) {
property = {
identifier: object.identifier,
reactive: object.reactive,
path: [{property: propertyName, optional}],
};
} else {
property = {
identifier: resolvedDependency.identifier,
reactive: resolvedDependency.reactive,
path: [...resolvedDependency.path, {property: propertyName, optional}],
};
}
@@ -395,7 +371,7 @@ type Decl = {
scope: Stack<ReactiveScope>;
};
export class DependencyCollectionContext {
class Context {
#declarations: Map<DeclarationId, Decl> = new Map();
#reassignments: Map<Identifier, Decl> = new Map();
@@ -435,14 +411,7 @@ export class DependencyCollectionContext {
const scopedDependencies = this.#dependencies.value;
CompilerError.invariant(scopedDependencies != null, {
reason: '[PropagateScopeDeps]: Unexpected scope mismatch',
description: null,
details: [
{
kind: 'error',
loc: scope.loc,
message: null,
},
],
loc: scope.loc,
});
// Restore context of previous scope
@@ -495,9 +464,6 @@ export class DependencyCollectionContext {
}
this.#reassignments.set(identifier, decl);
}
hasDeclared(identifier: Identifier): boolean {
return this.#declarations.has(identifier.declarationId);
}
// Checks if identifier is a valid dependency in the current scope
#checkValidDependency(maybeDependency: ReactiveScopeDependency): boolean {
@@ -549,7 +515,6 @@ export class DependencyCollectionContext {
this.visitDependency(
this.#temporaries.get(place.identifier.id) ?? {
identifier: place.identifier,
reactive: place.reactive,
path: [],
},
);
@@ -614,7 +579,6 @@ export class DependencyCollectionContext {
) {
maybeDependency = {
identifier: maybeDependency.identifier,
reactive: maybeDependency.reactive,
path: [],
};
}
@@ -636,11 +600,7 @@ export class DependencyCollectionContext {
identifier =>
identifier.declarationId === place.identifier.declarationId,
) &&
this.#checkValidDependency({
identifier: place.identifier,
reactive: place.reactive,
path: [],
})
this.#checkValidDependency({identifier: place.identifier, path: []})
) {
currentScope.reassignments.add(place.identifier);
}
@@ -678,10 +638,7 @@ enum HIRValue {
Terminal,
}
export function handleInstruction(
instr: Instruction,
context: DependencyCollectionContext,
): void {
function handleInstruction(instr: Instruction, context: Context): void {
const {id, value, lvalue} = instr;
context.declare(lvalue.identifier, {
id,
@@ -705,21 +662,21 @@ export function handleInstruction(
});
} else if (value.kind === 'DeclareLocal' || value.kind === 'DeclareContext') {
/*
* Some variables may be declared and never initialized. We need to retain
* (and hoist) these declarations if they are included in a reactive scope.
* One approach is to simply add all `DeclareLocal`s as scope declarations.
*
* Context variables with hoisted declarations only become live after their
* first assignment. We only declare real DeclareLocal / DeclareContext
* instructions (not hoisted ones) to avoid generating dependencies on
* hoisted declarations.
* Some variables may be declared and never initialized. We need
* to retain (and hoist) these declarations if they are included
* in a reactive scope. One approach is to simply add all `DeclareLocal`s
* as scope declarations.
*/
if (convertHoistedLValueKind(value.lvalue.kind) === null) {
context.declare(value.lvalue.place.identifier, {
id,
scope: context.currentScope,
});
}
/*
* We add context variable declarations here, not at `StoreContext`, since
* context Store / Loads are modeled as reads and mutates to the underlying
* variable reference (instead of through intermediate / inlined temporaries)
*/
context.declare(value.lvalue.place.identifier, {
id,
scope: context.currentScope,
});
} else if (value.kind === 'Destructure') {
context.visitOperand(value.value);
for (const place of eachPatternOperand(value.lvalue.pattern)) {
@@ -731,26 +688,6 @@ export function handleInstruction(
scope: context.currentScope,
});
}
} else if (value.kind === 'StoreContext') {
/**
* Some StoreContext variables have hoisted declarations. If we're storing
* to a context variable that hasn't yet been declared, the StoreContext is
* the declaration.
* (see corresponding logic in PruneHoistedContext)
*/
if (
!context.hasDeclared(value.lvalue.place.identifier) ||
value.lvalue.kind !== InstructionKind.Reassign
) {
context.declare(value.lvalue.place.identifier, {
id,
scope: context.currentScope,
});
}
for (const operand of eachInstructionValueOperand(value)) {
context.visitOperand(operand);
}
} else {
for (const operand of eachInstructionValueOperand(value)) {
context.visitOperand(operand);
@@ -764,7 +701,7 @@ function collectDependencies(
temporaries: ReadonlyMap<IdentifierId, ReactiveScopeDependency>,
processedInstrsInOptional: ReadonlySet<Instruction | Terminal>,
): Map<ReactiveScope, Array<ReactiveScopeDependency>> {
const context = new DependencyCollectionContext(
const context = new Context(
usedOutsideDeclaringScope,
temporaries,
processedInstrsInOptional,

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