Compare commits

..

3 Commits

Author SHA1 Message Date
eps1lon
027534e673 Add experimental_ prefix to renderToMarkup 2024-08-12 18:38:44 +02:00
eps1lon
f23fc95209 Allow nightly releases of react-markup 2024-08-12 18:35:33 +02:00
eps1lon
07b977b734 react-html -> react-markup 2024-08-05 15:41:54 +02:00
1120 changed files with 20384 additions and 57288 deletions

View File

@@ -23,7 +23,6 @@ module.exports = {
'babel',
'ft-flow',
'jest',
'es',
'no-for-of-loops',
'no-function-declare-after-return',
'react',
@@ -48,7 +47,7 @@ module.exports = {
'ft-flow/no-unused-expressions': ERROR,
// 'ft-flow/no-weak-types': WARNING,
// 'ft-flow/require-valid-file-annotation': ERROR,
'es/no-optional-chaining': ERROR,
'no-cond-assign': OFF,
'no-constant-condition': OFF,
'no-control-regex': OFF,
@@ -436,7 +435,6 @@ module.exports = {
'packages/react-dom/src/test-utils/*.js',
],
rules: {
'es/no-optional-chaining': OFF,
'react-internal/no-production-logging': OFF,
'react-internal/warning-args': OFF,
'react-internal/safe-string-coercion': [
@@ -498,7 +496,6 @@ module.exports = {
__IS_CHROME__: 'readonly',
__IS_FIREFOX__: 'readonly',
__IS_EDGE__: 'readonly',
__IS_NATIVE__: 'readonly',
__IS_INTERNAL_VERSION__: 'readonly',
},
},

107
.github/dependabot.yml vendored
View File

@@ -1,10 +1,107 @@
version: 2
updates:
- package-ecosystem: "npm"
directories:
- "/fixtures/*"
directory: "/fixtures/art"
schedule:
interval: "monthly"
interval: "weekly"
open-pull-requests-limit: 0
- package-ecosystem: "npm"
directory: "/fixtures/attribute-behavior"
schedule:
interval: "weekly"
open-pull-requests-limit: 0
- package-ecosystem: "npm"
directory: "/fixtures/concurrent"
schedule:
interval: "weekly"
open-pull-requests-limit: 0
- package-ecosystem: "npm"
directory: "/fixtures/devtools"
schedule:
interval: "weekly"
open-pull-requests-limit: 0
- package-ecosystem: "npm"
directory: "/fixtures/dom"
schedule:
interval: "weekly"
open-pull-requests-limit: 0
- package-ecosystem: "npm"
directory: "/fixtures/eslint"
schedule:
interval: "weekly"
open-pull-requests-limit: 0
- package-ecosystem: "npm"
directory: "/fixtures/expiration"
schedule:
interval: "weekly"
open-pull-requests-limit: 0
- package-ecosystem: "npm"
directory: "/fixtures/fiber-debugger"
schedule:
interval: "weekly"
open-pull-requests-limit: 0
- package-ecosystem: "npm"
directory: "/fixtures/fiber-triangle"
schedule:
interval: "weekly"
open-pull-requests-limit: 0
- package-ecosystem: "npm"
directory: "/fixtures/fizz"
schedule:
interval: "weekly"
open-pull-requests-limit: 0
- package-ecosystem: "npm"
directory: "/fixtures/fizz-ssr-browser"
schedule:
interval: "weekly"
open-pull-requests-limit: 0
- package-ecosystem: "npm"
directory: "/fixtures/flight"
schedule:
interval: "weekly"
open-pull-requests-limit: 0
- package-ecosystem: "npm"
directory: "/fixtures/flight-browser"
schedule:
interval: "weekly"
open-pull-requests-limit: 0
- package-ecosystem: "npm"
directory: "/fixtures/flight-esm"
schedule:
interval: "weekly"
open-pull-requests-limit: 0
- package-ecosystem: "npm"
directory: "/fixtures/legacy-jsx-runtimes"
schedule:
interval: "weekly"
open-pull-requests-limit: 0
- package-ecosystem: "npm"
directory: "/fixtures/nesting"
schedule:
interval: "weekly"
open-pull-requests-limit: 0
- package-ecosystem: "npm"
directory: "/fixtures/packaging"
schedule:
interval: "weekly"
open-pull-requests-limit: 0
- package-ecosystem: "npm"
directory: "/fixtures/scheduler"
schedule:
interval: "weekly"
open-pull-requests-limit: 0
- package-ecosystem: "npm"
directory: "/fixtures/ssr"
schedule:
interval: "weekly"
open-pull-requests-limit: 0
- package-ecosystem: "npm"
directory: "/fixtures/ssr-2"
schedule:
interval: "weekly"
open-pull-requests-limit: 0
- package-ecosystem: "npm"
directory: "/fixtures/stacks"
schedule:
interval: "weekly"
open-pull-requests-limit: 0
ignore:
- dependency-name: "*"

View File

@@ -15,7 +15,7 @@ env:
defaults:
run:
working-directory: compiler/apps/playground
working-directory: compiler
jobs:
playground:
@@ -27,17 +27,13 @@ jobs:
with:
node-version-file: '.nvmrc'
cache: yarn
cache-dependency-path: compiler/**/yarn.lock
cache-dependency-path: compiler/yarn.lock
- name: Restore cached node_modules
uses: actions/cache@v4
id: node_modules
with:
path: "**/node_modules"
key: ${{ runner.arch }}-${{ runner.os }}-modules-${{ hashFiles('compiler/**/yarn.lock') }}
- name: yarn install compiler
run: yarn install --frozen-lockfile
working-directory: compiler
- name: yarn install playground
run: yarn install --frozen-lockfile
- run: yarn install --frozen-lockfile
- run: npx playwright install --with-deps chromium
- run: yarn test
- run: yarn workspace playground test

View File

@@ -1,52 +0,0 @@
name: (Compiler) Publish Prereleases
on:
workflow_call:
inputs:
commit_sha:
required: true
default: ''
type: string
release_channel:
required: true
type: string
dist_tag:
required: true
type: string
secrets:
NPM_TOKEN:
required: true
env:
TZ: /usr/share/zoneinfo/America/Los_Angeles
# https://github.com/actions/cache/blob/main/tips-and-workarounds.md#cache-segment-restore-timeout
SEGMENT_DOWNLOAD_TIMEOUT_MINS: 1
GH_TOKEN: ${{ github.token }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
defaults:
run:
working-directory: compiler
jobs:
publish_prerelease:
name: Publish prelease (${{ inputs.release_channel }}) ${{ inputs.commit_sha }} @${{ inputs.dist_tag }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: yarn
cache-dependency-path: compiler/yarn.lock
- name: Restore cached node_modules
uses: actions/cache@v4
id: node_modules
with:
path: "**/node_modules"
key: ${{ runner.arch }}-${{ runner.os }}-modules-${{ hashFiles('compiler/**/yarn.lock') }}
- run: yarn install --frozen-lockfile
- name: Publish packages to npm
run: |
cp ./scripts/release/ci-npmrc ~/.npmrc
scripts/release/publish.js --frfr --ci --tags ${{ inputs.dist_tag }}

View File

@@ -1,21 +0,0 @@
name: (Compiler) Publish Prereleases Manual
on:
workflow_dispatch:
inputs:
prerelease_commit_sha:
required: false
env:
TZ: /usr/share/zoneinfo/America/Los_Angeles
jobs:
publish_prerelease_experimental:
name: Publish to Experimental channel
uses: facebook/react/.github/workflows/compiler_prereleases.yml@main
with:
commit_sha: ${{ inputs.prerelease_commit_sha || github.sha }}
release_channel: experimental
dist_tag: experimental
secrets:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}

View File

@@ -1,20 +0,0 @@
name: (Compiler) Publish Prereleases Nightly
on:
schedule:
# At 10 minutes past 16:00 on Mon, Tue, Wed, Thu, and Fri
- cron: 10 16 * * 1,2,3,4,5
env:
TZ: /usr/share/zoneinfo/America/Los_Angeles
jobs:
publish_prerelease_experimental:
name: Publish to Experimental channel
uses: facebook/react/.github/workflows/compiler_prereleases.yml@main
with:
commit_sha: ${{ github.sha }}
release_channel: experimental
dist_tag: experimental
secrets:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}

View File

@@ -6,7 +6,7 @@ on:
pull_request:
paths:
- compiler/**
- .github/workflows/compiler_typescript.yml
- .github/workflows/compiler-typescript.yml
env:
TZ: /usr/share/zoneinfo/America/Los_Angeles

View File

@@ -11,11 +11,6 @@ on:
commit_sha:
required: false
type: string
force:
description: 'Force a commit to the builds/... branches'
required: true
default: false
type: boolean
env:
TZ: /usr/share/zoneinfo/America/Los_Angeles
@@ -82,7 +77,7 @@ jobs:
working-directory: scripts/release
- name: Download artifacts for base revision
run: |
GH_TOKEN=${{ github.token }} scripts/release/download-experimental-build.js --commit=${{ inputs.commit_sha || github.event.workflow_run.head_sha || github.sha }}
GH_TOKEN=${{ github.token }} scripts/release/download-experimental-build.js --commit=${{ inputs.commit_sha || github.event.workflow_run.head_sha }}
- name: Display structure of build
run: ls -R build
- name: Strip @license from eslint plugin and react-refresh
@@ -90,11 +85,6 @@ jobs:
sed -i -e 's/ @license React*//' \
build/oss-experimental/eslint-plugin-react-hooks/cjs/eslint-plugin-react-hooks.development.js \
build/oss-experimental/react-refresh/cjs/react-refresh-babel.development.js
- name: Insert @headers into eslint plugin and react-refresh
run: |
sed -i -e 's/ LICENSE file in the root directory of this source tree./ LICENSE file in the root directory of this source tree.\n *\n * @noformat\n * @nolint\n * @lightSyntaxTransform\n * @preventMunge\n * @oncall react_core/' \
build/oss-experimental/eslint-plugin-react-hooks/cjs/eslint-plugin-react-hooks.development.js \
build/oss-experimental/react-refresh/cjs/react-refresh-babel.development.js
- name: Move relevant files for React in www into compiled
run: |
# Move the facebook-www folder into compiled
@@ -123,14 +113,13 @@ jobs:
run: |
BASE_FOLDER='compiled-rn/facebook-fbsource/xplat/js'
mkdir -p ${BASE_FOLDER}/react-native-github/Libraries/Renderer/
mkdir -p ${BASE_FOLDER}/RKJSModules/vendor/react/{scheduler,react,react-dom,react-is,react-test-renderer}/
mkdir -p ${BASE_FOLDER}/RKJSModules/vendor/react/{scheduler,react,react-is,react-test-renderer}/
# Move React Native renderer
mv build/react-native/implementations/ $BASE_FOLDER/react-native-github/Libraries/Renderer/
mv build/react-native/shims/ $BASE_FOLDER/react-native-github/Libraries/Renderer/
mv build/facebook-react-native/scheduler/cjs/ $BASE_FOLDER/RKJSModules/vendor/react/scheduler/
mv build/facebook-react-native/react/cjs/ $BASE_FOLDER/RKJSModules/vendor/react/react/
mv build/facebook-react-native/react-dom/cjs/ $BASE_FOLDER/RKJSModules/vendor/react/react-dom/
mv build/facebook-react-native/react-is/cjs/ $BASE_FOLDER/RKJSModules/vendor/react/react-is/
mv build/facebook-react-native/react-test-renderer/cjs/ $BASE_FOLDER/RKJSModules/vendor/react/react-test-renderer/
@@ -145,9 +134,9 @@ jobs:
ls -R ./compiled-rn
- name: Add REVISION files
run: |
echo ${{ github.sha }} >> ./compiled/facebook-www/REVISION
echo ${{ github.event.workflow_run.head_sha }} >> ./compiled/facebook-www/REVISION
cp ./compiled/facebook-www/REVISION ./compiled/facebook-www/REVISION_TRANSFORMS
echo ${{ github.sha}} >> ./compiled-rn/facebook-fbsource/xplat/js/react-native-github/Libraries/Renderer/REVISION
echo ${{ github.event.workflow_run.head_sha}} >> ./compiled-rn/facebook-fbsource/xplat/js/react-native-github/Libraries/Renderer/REVISION
- name: "Get current version string"
id: get_current_version
run: |
@@ -171,7 +160,7 @@ jobs:
commit_www_artifacts:
needs: download_artifacts
if: inputs.force == true || (github.ref == 'refs/heads/main' && needs.download_artifacts.outputs.www_branch_count == '0')
if: ${{ (github.ref == 'refs/heads/main' && needs.download_artifacts.outputs.www_branch_count == '0') || github.ref == 'refs/heads/meta-www' }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
@@ -201,7 +190,6 @@ jobs:
grep -rl "$CURRENT_VERSION_MODERN" ./compiled | xargs -r sed -i -e "s/$CURRENT_VERSION_MODERN/$LAST_VERSION_MODERN/g"
grep -rl "$CURRENT_VERSION_MODERN" ./compiled || echo "Modern version reverted"
- name: Check for changes
if: inputs.force != true
id: check_should_commit
run: |
echo "Full git status"
@@ -219,7 +207,7 @@ jobs:
echo "should_commit=false" >> "$GITHUB_OUTPUT"
fi
- name: Re-apply version changes
if: inputs.force == true || (steps.check_should_commit.outputs.should_commit == 'true' && needs.download_artifacts.outputs.last_version_classic != '' && needs.download_artifacts.outputs.last_version_modern != '')
if: steps.check_should_commit.outputs.should_commit == 'true' && needs.download_artifacts.outputs.last_version_classic != '' && needs.download_artifacts.outputs.last_version_modern != ''
env:
CURRENT_VERSION_CLASSIC: ${{ needs.download_artifacts.outputs.current_version_classic }}
CURRENT_VERSION_MODERN: ${{ needs.download_artifacts.outputs.current_version_modern }}
@@ -236,26 +224,26 @@ jobs:
grep -rl "$LAST_VERSION_MODERN" ./compiled | xargs -r sed -i -e "s/$LAST_VERSION_MODERN/$CURRENT_VERSION_MODERN/g"
grep -rl "$LAST_VERSION_MODERN" ./compiled || echo "Classic version re-applied"
- name: Will commit these changes
if: inputs.force == true || steps.check_should_commit.outputs.should_commit == 'true'
if: steps.check_should_commit.outputs.should_commit == 'true'
run: |
echo ":"
git status -u
- name: Commit changes to branch
if: inputs.force == true || steps.check_should_commit.outputs.should_commit == 'true'
uses: stefanzweifel/git-auto-commit-action@v5
if: steps.check_should_commit.outputs.should_commit == 'true'
uses: stefanzweifel/git-auto-commit-action@v4
with:
commit_message: |
${{ github.event.workflow_run.head_commit.message || format('Manual build of {0}', github.event.workflow_run.head_sha || github.sha) }}
${{ github.event.workflow_run.head_commit.message }}
DiffTrain build for [${{ github.event.workflow_run.head_sha || github.sha }}](https://github.com/facebook/react/commit/${{ github.event.workflow_run.head_sha || github.sha }})
DiffTrain build for [${{ github.event.workflow_run.head_sha }}](https://github.com/facebook/react/commit/${{ github.event.workflow_run.head_sha }})
branch: builds/facebook-www
commit_user_name: ${{ github.triggering_actor }}
commit_user_email: ${{ format('{0}@users.noreply.github.com', github.triggering_actor) }}
commit_user_name: ${{ github.event.workflow_run.triggering_actor.login }}
commit_user_email: ${{ github.event.workflow_run.triggering_actor.email || format('{0}@users.noreply.github.com', github.event.workflow_run.triggering_actor.login) }}
create_branch: true
commit_fbsource_artifacts:
needs: download_artifacts
if: inputs.force == true || (github.ref == 'refs/heads/main' && needs.download_artifacts.outputs.fbsource_branch_count == '0')
if: ${{ (github.ref == 'refs/heads/main' && needs.download_artifacts.outputs.fbsource_branch_count == '0') || github.ref == 'refs/heads/meta-fbsource' }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
@@ -278,7 +266,6 @@ jobs:
grep -rl "$CURRENT_VERSION" ./compiled-rn | xargs -r sed -i -e "s/$CURRENT_VERSION/$LAST_VERSION/g"
grep -rl "$CURRENT_VERSION" ./compiled-rn || echo "Version reverted"
- name: Check for changes
if: inputs.force != 'true'
id: check_should_commit
run: |
echo "Full git status"
@@ -297,7 +284,7 @@ jobs:
echo "should_commit=false" >> "$GITHUB_OUTPUT"
fi
- name: Re-apply version changes
if: inputs.force == true || (steps.check_should_commit.outputs.should_commit == 'true' && needs.download_artifacts.outputs.last_version_rn != '')
if: steps.check_should_commit.outputs.should_commit == 'true' && needs.download_artifacts.outputs.last_version_rn != ''
env:
CURRENT_VERSION: ${{ needs.download_artifacts.outputs.current_version_rn }}
LAST_VERSION: ${{ needs.download_artifacts.outputs.last_version_rn }}
@@ -307,12 +294,12 @@ jobs:
grep -rl "$LAST_VERSION" ./compiled-rn | xargs -r sed -i -e "s/$LAST_VERSION/$CURRENT_VERSION/g"
grep -rl "$LAST_VERSION" ./compiled-rn || echo "Version re-applied"
- name: Add files for signing
if: inputs.force == true || steps.check_should_commit.outputs.should_commit == 'true'
if: steps.check_should_commit.outputs.should_commit == 'true'
run: |
echo ":"
git add .
- name: Signing files
if: inputs.force == true || steps.check_should_commit.outputs.should_commit == 'true'
if: steps.check_should_commit.outputs.should_commit == 'true'
uses: actions/github-script@v7
with:
script: |
@@ -366,9 +353,8 @@ jobs:
console.log('Signing files in directory:', directory);
try {
const result = execSync(`git status --porcelain ${directory}`, {encoding: 'utf8'});
console.log(result);
// Parse the git status output to get file paths!
// Parse the git status output to get file paths
const files = result.split('\n').filter(file => file.endsWith('.js'));
if (files.length === 0) {
@@ -377,14 +363,7 @@ jobs:
);
} else {
files.forEach(line => {
let file = null;
if (line.startsWith('D ')) {
return;
} else if (line.startsWith('R ')) {
file = line.slice(line.indexOf('->') + 3);
} else {
file = line.slice(3).trim();
}
const file = line.slice(3).trim();
if (file) {
console.log(' Signing file:', file);
const originalContents = fs.readFileSync(file, 'utf8');
@@ -403,19 +382,19 @@ jobs:
console.error('Error signing files:', e);
}
- name: Will commit these changes
if: inputs.force == true || steps.check_should_commit.outputs.should_commit == 'true'
if: steps.check_should_commit.outputs.should_commit == 'true'
run: |
git add .
git status
- name: Commit changes to branch
if: inputs.force == true || steps.check_should_commit.outputs.should_commit == 'true'
uses: stefanzweifel/git-auto-commit-action@v5
if: steps.check_should_commit.outputs.should_commit == 'true'
uses: stefanzweifel/git-auto-commit-action@v4
with:
commit_message: |
${{ github.event.workflow_run.head_commit.message || format('Manual build of {0}', github.event.workflow_run.head_sha || github.sha) }}
${{ github.event.workflow_run.head_commit.message }}
DiffTrain build for [${{ github.event.workflow_run.head_sha || github.sha }}](https://github.com/facebook/react/commit/${{ github.event.workflow_run.head_sha || github.sha }})
DiffTrain build for commit https://github.com/facebook/react/commit/${{ github.event.workflow_run.head_sha }}.
branch: builds/facebook-fbsource
commit_user_name: ${{ github.triggering_actor }}
commit_user_email: ${{ format('{0}@users.noreply.github.com', github.triggering_actor) }}
commit_user_name: ${{ github.event.workflow_run.triggering_actor.login }}
commit_user_email: ${{ github.event.workflow_run.triggering_actor.email || format('{0}@users.noreply.github.com', github.event.workflow_run.triggering_actor.login) }}
create_branch: true

View File

@@ -43,7 +43,7 @@ jobs:
uses: actions/cache@v4
with:
path: "**/node_modules"
key: ${{ runner.arch }}-${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }}
key: ${{ runner.arch }}-${{ runner.os }}-modules-${{ hashFiles('yarn.lock') }}
- run: yarn install --frozen-lockfile
- run: node ./scripts/tasks/eslint
@@ -61,7 +61,7 @@ jobs:
uses: actions/cache@v4
with:
path: "**/node_modules"
key: ${{ runner.arch }}-${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }}
key: ${{ runner.arch }}-${{ runner.os }}-modules-${{ hashFiles('yarn.lock') }}
- run: yarn install --frozen-lockfile
- run: ./scripts/ci/check_license.sh
@@ -79,6 +79,6 @@ jobs:
uses: actions/cache@v4
with:
path: "**/node_modules"
key: ${{ runner.arch }}-${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }}
key: ${{ runner.arch }}-${{ runner.os }}-modules-${{ hashFiles('yarn.lock') }}
- run: yarn install --frozen-lockfile
- run: ./scripts/ci/test_print_warnings.sh

View File

@@ -24,7 +24,6 @@ compiler/**/.next
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-kitchensink.js
compiler/crates
compiler/target
compiler/apps/playground/public
compiler/**/LICENSE

View File

@@ -52,7 +52,7 @@ const stablePackages = {
// These packages do not exist in the @canary or @latest channel, only
// @experimental. We don't use semver, just the commit sha, so this is just a
// list of package names instead of a map.
const experimentalPackages = ['react-markup'];
const experimentalPackages = [];
module.exports = {
ReactVersion,

0
compiler/.gitmodules vendored Normal file
View File

1
compiler/.watchmanconfig Normal file
View File

@@ -0,0 +1 @@
{}

View File

@@ -0,0 +1,80 @@
# Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to make participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, sex characteristics, gender identity and expression,
level of experience, education, socio-economic status, nationality, personal
appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment
include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or
advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.
## Scope
This Code of Conduct applies within all project spaces, and it also applies when
an individual is representing the project or its community in public spaces.
Examples of representing a project or community include using an official
project e-mail address, posting via an official social media account, or acting
as an appointed representative at an online or offline event. Representation of
a project may be further defined and clarified by project maintainers.
This Code of Conduct also applies outside the project spaces when there is a
reasonable belief that an individual's behavior may have a negative impact on
the project or its community.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at <opensource-conduct@fb.com>. All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see
https://www.contributor-covenant.org/faq

31
compiler/CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,31 @@
# Contributing to react-forget
We want to make contributing to this project as easy and transparent as
possible.
## Pull Requests
We actively welcome your pull requests.
1. Fork the repo and create your branch from `main`.
2. If you've added code that should be tested, add tests.
3. If you've changed APIs, update the documentation.
4. Ensure the test suite passes.
5. Make sure your code lints.
6. If you haven't already, complete the Contributor License Agreement ("CLA").
## Contributor License Agreement ("CLA")
In order to accept your pull request, we need you to submit a CLA. You only need
to do this once to work on any of Facebook's open source projects.
Complete your CLA here: <https://code.facebook.com/cla>
## Issues
We use GitHub issues to track public bugs. Please ensure your description is
clear and has sufficient instructions to be able to reproduce the issue.
Facebook has a [bounty program](https://www.facebook.com/whitehat/) for the safe
disclosure of security bugs. In those cases, please go through the process
outlined on that page and do not file a public issue.
## License
By contributing to react-forget, you agree that your contributions will be licensed
under the LICENSE file in the root directory of this source tree.

21
compiler/LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) Meta Platforms, Inc. and affiliates.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -7,11 +7,7 @@
import '../styles/globals.css';
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}): JSX.Element {
export default function RootLayout({children}: {children: React.ReactNode}) {
'use no memo';
return (
<html lang="en">

View File

@@ -11,7 +11,7 @@ import {SnackbarProvider} from 'notistack';
import {Editor, Header, StoreProvider} from '../components';
import MessageSnackbar from '../components/Message';
export default function Page(): JSX.Element {
export default function Hoot() {
return (
<StoreProvider>
<SnackbarProvider

View File

@@ -13,7 +13,7 @@ module.exports = function (api) {
[
'babel-plugin-react-compiler',
{
target: '18',
runtimeModule: 'react-compiler-runtime',
},
],
],

View File

@@ -5,7 +5,7 @@
* LICENSE file in the root directory of this source tree.
*/
import {parse as babelParse} from '@babel/parser';
import {parse as babelParse, ParserPlugin} from '@babel/parser';
import * as HermesParser from 'hermes-parser';
import traverse, {NodePath} from '@babel/traverse';
import * as t from '@babel/types';
@@ -15,8 +15,10 @@ import {
Effect,
ErrorSeverity,
parseConfigPragma,
printHIR,
printReactiveFunction,
run,
ValueKind,
runPlayground,
type Hook,
} from 'babel-plugin-react-compiler/src';
import {type ReactFunctionType} from 'babel-plugin-react-compiler/src/HIR/Environment';
@@ -43,7 +45,7 @@ import {
import {printFunctionWithOutlined} from 'babel-plugin-react-compiler/src/HIR/PrintHIR';
import {printReactiveFunctionWithOutlined} from 'babel-plugin-react-compiler/src/ReactiveScopes/PrintReactiveFunction';
function parseInput(input: string, language: 'flow' | 'typescript'): any {
function parseInput(input: string, language: 'flow' | 'typescript') {
// Extract the first line to quickly check for custom test directives
if (language === 'flow') {
return HermesParser.parse(input, {
@@ -64,14 +66,14 @@ function parseFunctions(
source: string,
language: 'flow' | 'typescript',
): Array<
| NodePath<t.FunctionDeclaration>
| NodePath<t.ArrowFunctionExpression>
| NodePath<t.FunctionExpression>
NodePath<
t.FunctionDeclaration | t.ArrowFunctionExpression | t.FunctionExpression
>
> {
const items: Array<
| NodePath<t.FunctionDeclaration>
| NodePath<t.ArrowFunctionExpression>
| NodePath<t.FunctionExpression>
NodePath<
t.FunctionDeclaration | t.ArrowFunctionExpression | t.FunctionExpression
>
> = [];
try {
const ast = parseInput(source, language);
@@ -153,37 +155,26 @@ function isHookName(s: string): boolean {
return /^use[A-Z0-9]/.test(s);
}
function getReactFunctionType(id: t.Identifier | null): ReactFunctionType {
if (id != null) {
if (isHookName(id.name)) {
function getReactFunctionType(
id: NodePath<t.Identifier | null | undefined>,
): ReactFunctionType {
if (id && id.node && id.isIdentifier()) {
if (isHookName(id.node.name)) {
return 'Hook';
}
const isPascalCaseNameSpace = /^[A-Z].*/;
if (isPascalCaseNameSpace.test(id.name)) {
if (isPascalCaseNameSpace.test(id.node.name)) {
return 'Component';
}
}
return 'Other';
}
function getFunctionIdentifier(
fn:
| NodePath<t.FunctionDeclaration>
| NodePath<t.ArrowFunctionExpression>
| NodePath<t.FunctionExpression>,
): t.Identifier | null {
if (fn.isArrowFunctionExpression()) {
return null;
}
const id = fn.get('id');
return Array.isArray(id) === false && id.isIdentifier() ? id.node : null;
}
function compile(source: string): [CompilerOutput, 'flow' | 'typescript'] {
const results = new Map<string, Array<PrintedCompilerPipelineValue>>();
const results = new Map<string, PrintedCompilerPipelineValue[]>();
const error = new CompilerError();
const upsert: (result: PrintedCompilerPipelineValue) => void = result => {
const upsert = (result: PrintedCompilerPipelineValue) => {
const entry = results.get(result.name);
if (Array.isArray(entry)) {
entry.push(result);
@@ -197,30 +188,40 @@ function compile(source: string): [CompilerOutput, 'flow' | 'typescript'] {
} else {
language = 'typescript';
}
let count = 0;
const withIdentifier = (id: t.Identifier | null): t.Identifier => {
if (id != null && id.name != null) {
return id;
} else {
return t.identifier(`anonymous_${count++}`);
}
};
try {
// Extract the first line to quickly check for custom test directives
const pragma = source.substring(0, source.indexOf('\n'));
const config = parseConfigPragma(pragma);
for (const fn of parseFunctions(source, language)) {
const id = withIdentifier(getFunctionIdentifier(fn));
for (const result of runPlayground(
if (!fn.isFunctionDeclaration()) {
error.pushErrorDetail(
new CompilerErrorDetail({
reason: `Unexpected function type ${fn.node.type}`,
description:
'Playground only supports parsing function declarations',
severity: ErrorSeverity.Todo,
loc: fn.node.loc ?? null,
suggestions: null,
}),
);
continue;
}
const id = fn.get('id');
for (const result of run(
fn,
{
...config,
customHooks: new Map([...COMMON_HOOKS]),
},
getReactFunctionType(id),
'_c',
null,
null,
null,
)) {
const fnName = id.name;
const fnName = fn.node.id?.name ?? null;
switch (result.kind) {
case 'ast': {
upsert({
@@ -229,7 +230,7 @@ function compile(source: string): [CompilerOutput, 'flow' | 'typescript'] {
name: result.name,
value: {
type: 'FunctionDeclaration',
id: withIdentifier(result.value.id),
id: result.value.id,
async: result.value.async,
generator: result.value.generator,
body: result.value.body,
@@ -273,17 +274,13 @@ function compile(source: string): [CompilerOutput, 'flow' | 'typescript'] {
}
}
} catch (err) {
/**
* error might be an invariant violation or other runtime error
* (i.e. object shape that is not CompilerError)
*/
// 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
*/
// Handle unexpected failures by logging (to get a stack trace)
// and reporting
console.error(err);
error.details.push(
new CompilerErrorDetail({
@@ -301,7 +298,7 @@ function compile(source: string): [CompilerOutput, 'flow' | 'typescript'] {
return [{kind: 'ok', results}, language];
}
export default function Editor(): JSX.Element {
export default function Editor() {
const store = useStore();
const deferredStore = useDeferredValue(store);
const dispatchStore = useStoreDispatch();

View File

@@ -15,17 +15,18 @@ import {useEffect, useState} from 'react';
import {renderReactCompilerMarkers} from '../../lib/reactCompilerMonacoDiagnostics';
import {useStore, useStoreDispatch} from '../StoreContext';
import {monacoOptions} from './monacoOptions';
// @ts-expect-error TODO: Make TS recognize .d.ts files, in addition to loading them with webpack.
// TODO: Make TS recognize .d.ts files, in addition to loading them with webpack.
// @ts-ignore
import React$Types from '../../node_modules/@types/react/index.d.ts';
loader.config({monaco});
type Props = {
errors: Array<CompilerErrorDetail>;
errors: CompilerErrorDetail[];
language: 'flow' | 'typescript';
};
export default function Input({errors, language}: Props): JSX.Element {
export default function Input({errors, language}: Props) {
const [monaco, setMonaco] = useState<Monaco | null>(null);
const store = useStore();
const dispatchStore = useStoreDispatch();
@@ -37,19 +38,18 @@ export default function Input({errors, language}: Props): JSX.Element {
const model = monaco.editor.getModel(uri);
invariant(model, 'Model must exist for the selected input file.');
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.
*/
// 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]);
const flowDiagnosticDisable = [
7028 /* unused label */, 6133 /* var declared but not read */,
];
useEffect(() => {
/**
* Ignore "can only be used in TypeScript files." errors, since
* we want to support syntax highlighting for Flow (*.js) files
* and Flow is not a built-in language.
*/
// Ignore "can only be used in TypeScript files." errors, since
// we want to support syntax highlighting for Flow (*.js) files
// and Flow is not a built-in language.
if (!monaco) return;
monaco.languages.typescript.javascriptDefaults.setDiagnosticsOptions({
diagnosticCodesToIgnore: [
@@ -64,9 +64,7 @@ export default function Input({errors, language}: Props): JSX.Element {
8011,
8012,
8013,
...(language === 'flow'
? [7028 /* unused label */, 6133 /* var declared but not read */]
: []),
...(language === 'flow' ? flowDiagnosticDisable : []),
],
noSemanticValidation: true,
// Monaco can't validate Flow component syntax
@@ -74,7 +72,7 @@ export default function Input({errors, language}: Props): JSX.Element {
});
}, [monaco, language]);
const handleChange: (value: string | undefined) => void = value => {
const handleChange = (value: string | undefined) => {
if (!value) return;
dispatchStore({
@@ -85,10 +83,7 @@ export default function Input({errors, language}: Props): JSX.Element {
});
};
const handleMount: (
_: editor.IStandaloneCodeEditor,
monaco: Monaco,
) => void = (_, monaco) => {
const handleMount = (_: editor.IStandaloneCodeEditor, monaco: Monaco) => {
setMonaco(monaco);
const tscOptions = {
@@ -116,12 +111,10 @@ export default function Input({errors, language}: Props): JSX.Element {
monaco.languages.typescript.javascriptDefaults.addExtraLib(...reactLib);
monaco.languages.typescript.typescriptDefaults.addExtraLib(...reactLib);
/**
* Remeasure the font in case the custom font is loaded only after
* Monaco Editor is mounted.
* N.B. that this applies also to the output editor as it seems
* Monaco Editor instances share the same font config.
*/
// Remeasure the font in case the custom font is loaded only after
// Monaco Editor is mounted.
// N.B. that this applies also to the output editor as it seems
// Monaco Editor instances share the same font config.
document.fonts.ready.then(() => {
monaco.editor.remeasureFonts();
});
@@ -132,18 +125,14 @@ export default function Input({errors, language}: Props): JSX.Element {
<Resizable
minWidth={650}
enable={{right: true}}
/**
* Restrict MonacoEditor's height, since the config autoLayout:true
* will grow the editor to fit within parent element
*/
// 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.
*/
// .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}

View File

@@ -17,7 +17,7 @@ import {type CompilerError} from 'babel-plugin-react-compiler/src';
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 {memo, useEffect, useState} from 'react';
import {type Store} from '../../lib/stores';
import TabbedWindow from '../TabbedWindow';
import {monacoOptions} from './monacoOptions';
@@ -42,10 +42,10 @@ export type PrintedCompilerPipelineValue =
| {kind: 'debug'; name: string; fnName: string | null; value: string};
export type CompilerOutput =
| {kind: 'ok'; results: Map<string, Array<PrintedCompilerPipelineValue>>}
| {kind: 'ok'; results: Map<string, PrintedCompilerPipelineValue[]>}
| {
kind: 'err';
results: Map<string, Array<PrintedCompilerPipelineValue>>;
results: Map<string, PrintedCompilerPipelineValue[]>;
error: CompilerError;
};
@@ -54,10 +54,7 @@ type Props = {
compilerOutput: CompilerOutput;
};
async function tabify(
source: string,
compilerOutput: CompilerOutput,
): Promise<Map<string, ReactNode>> {
async function tabify(source: string, compilerOutput: CompilerOutput) {
const tabs = new Map<string, React.ReactNode>();
const reorderedTabs = new Map<string, React.ReactNode>();
const concattedResults = new Map<string, string>();
@@ -115,10 +112,8 @@ async function tabify(
}
// Ensure that JS and the JS source map come first
if (topLevelFnDecls.length > 0) {
/**
* Make a synthetic Program so we can have a single AST with all the top level
* FunctionDeclarations
*/
// Make a synthetic Program so we can have a single AST with all the top level
// FunctionDeclarations
const ast = t.program(topLevelFnDecls);
const {code, sourceMapUrl} = await codegen(ast, source);
reorderedTabs.set(
@@ -180,7 +175,7 @@ function getSourceMapUrl(code: string, map: string): string | null {
)}`;
}
function Output({store, compilerOutput}: Props): JSX.Element {
function Output({store, compilerOutput}: Props) {
const [tabsOpen, setTabsOpen] = useState<Set<string>>(() => new Set(['JS']));
const [tabs, setTabs] = useState<Map<string, React.ReactNode>>(
() => new Map(),
@@ -241,13 +236,11 @@ function TextTabContent({
output: string;
diff: string | null;
showInfoPanel: boolean;
}): JSX.Element {
}) {
const [diffMode, setDiffMode] = useState(false);
return (
/**
* Restrict MonacoEditor's height, since the config autoLayout:true
* will grow the editor to fit within parent element
*/
// Restrict MonacoEditor's height, since the config autoLayout:true
// will grow the editor to fit within parent element
<div className="w-full h-monaco_small sm:h-monaco">
{showInfoPanel ? (
<div className="flex items-center gap-1 bg-amber-50 p-2">

View File

@@ -7,10 +7,8 @@
import dynamic from 'next/dynamic';
/**
* monaco-editor is currently not compatible with ssr
* https://github.com/vercel/next.js/issues/31692
*/
// monaco-editor is currently not compatible with ssr
// https://github.com/vercel/next.js/issues/31692
const Editor = dynamic(() => import('./EditorImpl'), {
ssr: false,
});

View File

@@ -16,26 +16,26 @@ import {IconGitHub} from './Icons/IconGitHub';
import Logo from './Logo';
import {useStoreDispatch} from './StoreContext';
export default function Header(): JSX.Element {
export default function Header() {
const [showCheck, setShowCheck] = useState(false);
const dispatchStore = useStoreDispatch();
const {enqueueSnackbar, closeSnackbar} = useSnackbar();
const handleReset: () => void = () => {
const handleReset = () => {
if (confirm('Are you sure you want to reset the playground?')) {
/**
* Close open snackbars if any. This is necessary because when displaying
* outputs (Preview or not), we only close previous snackbars if we received
* new messages, which is needed in order to display "Bad URL" or success
* messages when loading Playground for the first time. Otherwise, messages
* such as "Bad URL" will be closed by the outputs calling `closeSnackbar`.
*/
/*
Close open snackbars if any. This is necessary because when displaying
outputs (Preview or not), we only close previous snackbars if we received
new messages, which is needed in order to display "Bad URL" or success
messages when loading Playground for the first time. Otherwise, messages
such as "Bad URL" will be closed by the outputs calling `closeSnackbar`.
*/
closeSnackbar();
dispatchStore({type: 'setStore', payload: {store: defaultStore}});
}
};
const handleShare: () => void = () => {
const handleShare = () => {
navigator.clipboard.writeText(location.href).then(() => {
enqueueSnackbar('URL copied to clipboard');
setShowCheck(true);

View File

@@ -7,7 +7,7 @@
// https://github.com/reactjs/reactjs.org/blob/main/beta/src/components/Logo.tsx
export default function Logo(props: JSX.IntrinsicElements['svg']): JSX.Element {
export default function Logo(props: JSX.IntrinsicElements['svg']) {
return (
<svg
viewBox="0 0 410 369"

View File

@@ -29,7 +29,7 @@ export const useStoreDispatch = StoreDispatchContext.useContext;
/**
* Make Store and dispatch function available to all sub-components in children.
*/
export function StoreProvider({children}: {children: ReactNode}): JSX.Element {
export function StoreProvider({children}: {children: ReactNode}) {
const [store, dispatch] = useReducer(storeReducer, emptyStore);
return (

View File

@@ -69,9 +69,6 @@ function TabbedWindowItem({
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 ? (
@@ -83,7 +80,7 @@ function TabbedWindowItem({
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}
- {name}
</h2>
{tabs.get(name) ?? <div>No output for {name}</div>}
</Resizable>
@@ -97,7 +94,7 @@ function TabbedWindowItem({
className={`flex-grow-0 w-5 transition-colors duration-150 ease-in ${
hasChanged ? 'font-bold' : 'font-light'
} text-secondary hover:text-link`}>
{displayName}
{name}
</button>
</div>
)}

View File

@@ -23,13 +23,10 @@ import React from 'react';
* Instead, it throws an error when `useContext` is not called within a
* Provider with a value.
*/
export default function createContext<T>(): {
useContext: () => NonNullable<T>;
Provider: React.Provider<T | null>;
} {
export default function createContext<T>() {
const context = React.createContext<T | null>(null);
function useContext(): NonNullable<T> {
function useContext() {
const c = React.useContext(context);
if (!c)
throw new Error('useContext must be within a Provider with a value');

View File

@@ -46,9 +46,9 @@ function mapReactCompilerDiagnosticToMonacoMarker(
type ReactCompilerMarkerConfig = {
monaco: Monaco;
model: editor.ITextModel;
details: Array<CompilerErrorDetail>;
details: CompilerErrorDetail[];
};
let decorations: Array<string> = [];
let decorations: string[] = [];
export function renderReactCompilerMarkers({
monaco,
model,

View File

@@ -28,7 +28,7 @@ export function decodeStore(hash: string): Store {
/**
* Serialize, encode, and save @param store to localStorage and update URL.
*/
export function saveStore(store: Store): void {
export function saveStore(store: Store) {
const hash = encodeStore(store);
localStorage.setItem('playgroundStore', hash);
history.replaceState({}, '', `#${hash}`);
@@ -56,10 +56,8 @@ export function initStoreFromUrlOrLocalStorage(): Store {
const encodedSourceFromLocal = localStorage.getItem('playgroundStore');
const encodedSource = encodedSourceFromUrl || encodedSourceFromLocal;
/**
* No data in the URL and no data in the localStorage to fallback to.
* Initialize with the default store.
*/
// No data in the URL and no data in the localStorage to fallback to.
// Initialize with the default store.
if (!encodedSource) return defaultStore;
const raw = decodeStore(encodedSource);

View File

@@ -3,27 +3,24 @@
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "cd ../.. && concurrently --kill-others -n compiler,runtime,playground \"yarn workspace babel-plugin-react-compiler run build --watch\" \"yarn workspace react-compiler-runtime run build --watch\" \"wait-on packages/babel-plugin-react-compiler/dist/index.js && cd apps/playground && NODE_ENV=development next dev\"",
"build:compiler": "cd ../.. && concurrently -n compiler,runtime \"yarn workspace babel-plugin-react-compiler run build\" \"yarn workspace react-compiler-runtime run build\"",
"build": "yarn build:compiler && next build",
"postbuild": "node ./scripts/downloadFonts.js",
"postinstall": "./scripts/link-compiler.sh",
"vercel-build": "yarn build",
"dev": "NODE_ENV=development next dev",
"build": "next build && node ./scripts/downloadFonts.js",
"vercel-build": "yarn workspaces run build",
"start": "next start",
"lint": "next lint",
"test": "playwright test"
},
"dependencies": {
"@babel/core": "^7.18.9",
"@babel/generator": "^7.18.9",
"@babel/parser": "^7.18.9",
"@babel/plugin-syntax-typescript": "^7.18.9",
"@babel/core": "^7.19.1",
"@babel/generator": "^7.19.1",
"@babel/parser": "^7.19.1",
"@babel/plugin-syntax-typescript": "^7.18.6",
"@babel/plugin-transform-block-scoping": "^7.18.9",
"@babel/plugin-transform-modules-commonjs": "^7.18.9",
"@babel/preset-react": "^7.18.9",
"@babel/preset-typescript": "^7.18.9",
"@babel/traverse": "^7.18.9",
"@babel/types": "7.18.9",
"@babel/plugin-transform-modules-commonjs": "^7.18.6",
"@babel/preset-react": "^7.18.6",
"@babel/preset-typescript": "^7.18.6",
"@babel/traverse": "^7.19.1",
"@babel/types": "^7.19.0",
"@heroicons/react": "^1.0.6",
"@monaco-editor/react": "^4.4.6",
"@playwright/test": "^1.42.1",
@@ -39,23 +36,25 @@
"prettier": "^3.3.3",
"pretty-format": "^29.3.1",
"re-resizable": "^6.9.16",
"react": "18.3.1",
"react": "18.2.0",
"react-compiler-runtime": "*",
"react-dom": "18.3.1"
"react-dom": "18.2.0"
},
"devDependencies": {
"@types/node": "18.11.9",
"@types/react": "18.3.9",
"@types/react-dom": "18.3.0",
"@types/react": "18.0.25",
"@types/react-dom": "18.0.9",
"autoprefixer": "^10.4.13",
"clsx": "^1.2.1",
"concurrently": "^7.4.0",
"eslint": "^8.28.0",
"eslint-config-next": "^13.5.6",
"hermes-parser": "^0.22.0",
"monaco-editor-webpack-plugin": "^7.1.0",
"postcss": "^8.4.31",
"tailwindcss": "^3.2.4",
"wait-on": "^7.2.0"
"tailwindcss": "^3.2.4"
},
"resolutions": {
"./**/@babel/parser": "7.7.4",
"./**/@babel/types": "7.7.4"
}
}

View File

@@ -30,7 +30,8 @@ export default defineConfig({
// Run your local dev server before starting the tests:
// https://playwright.dev/docs/test-advanced#launching-a-development-web-server-during-the-tests
webServer: {
command: 'yarn dev',
command:
'yarn workspace babel-plugin-react-compiler build && yarn workspace react-compiler-runtime build && yarn dev',
url: baseURL,
timeout: 300 * 1000,
reuseExistingServer: !process.env.CI,

View File

@@ -1,15 +0,0 @@
#!/usr/bin/env bash
# Copyright (c) Meta Platforms, Inc. and affiliates.
#
# This source code is licensed under the MIT license found in the
# LICENSE file in the root directory of this source tree.
set -eo pipefail
HERE=$(pwd)
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

@@ -31,7 +31,6 @@
".next/types/**/*.ts"
],
"exclude": [
"node_modules",
"../../../**"
"node_modules"
]
}

File diff suppressed because it is too large Load Diff

View File

@@ -151,7 +151,6 @@ impl MergedBlocks {
#[derive(Debug, Error)]
#[error("Expected predecessor {predecessor} to exist")]
#[allow(dead_code)]
pub struct ExpectedPredecessorToExist {
predecessor: BlockId,
}

View File

@@ -159,7 +159,6 @@ impl<'m> std::fmt::Debug for ScopeView<'m> {
}
#[derive(Clone, Copy)]
#[allow(dead_code)]
pub struct LabelView<'m> {
#[allow(dead_code)]
pub(crate) manager: &'m ScopeManager,

View File

@@ -11,7 +11,7 @@ The idea of React Compiler is to allow developers to use React's familiar declar
* Retain React's familiar declarative, component-oriented programming model. Ie, the solution should not fundamentally change how developers think about writing React, and should generally _remove_ concepts (the need to use React.memo(), useMemo(), and useCallback()) rather than introduce new concepts.
* "Just work" on idiomatic React code that follows React's rules (pure render functions, the rules of hooks, etc).
* Support typical debugging and profiling tools and workflows.
* Be predictable and understandable enough by React developers — i.e. developers should be able to quickly develop a rough intuition of how React Compiler works.
* Be predictable and understandable enough by React developers — ie developers should be able to quickly develop a rough intuition of how React Compiler works.
* Not require explicit annotations (types or otherwise) for typical product code. We may provide features that allow developers to opt-in to using type information to enable additional optimizations, but the compiler should work well without type information or other annotations.
## Non-Goals
@@ -24,15 +24,15 @@ The following are explicitly *not* goals for React Compiler:
* The amount of code may regress startup times, which would conflict with our goal of neutral startup performance.
* Support code that violates React's rules. React's rules exist to help developers build robust, scalable applications and form a contract that allows us to continue improving React without breaking applications. React Compiler depends on these rules to safely transform code, and violations of rules will therefore break React Compiler's optimizations.
* Support legacy React features. Notably we will not support class components due to their inherent mutable state being shared across multiple methods with complex lifetimes and data flow.
* Support 100% of the JavaScript language. In particular, we will not support rarely used features and/or features which are known to be unsafe or which cannot be modeled soundly. For example, nested classes that capture values from their closure are difficult to model accurately because of mutability, and `eval()` is unsafe. We aim to support the vast majority of JavaScript code (and the TypeScript and Flow dialects)
* Support 100% of the JavaScript language. In particular, we will not support rarely used features and/or features which are known to be unsafe or which cannot be modeled soundly. For example, nested classes that capture values from their closure are difficult to model accurately bc of mutability, and `eval()` is unsafe. We aim to support the vast majority of JavaScript code (and the TypeScript and Flow dialects)
## Design Principles
Many aspects of the design follow naturally from the above goals:
* The compiler output must be high-level code that retains not just the semantics of the input but also is expressed using similar constructs to the input. For example, rather than convert logical expressions (`a ?? b`) into an `if` statement, we retain the high-level form of the logical expression. Rather than convert all looping constructs to a single form, we retain the original form of the loop. This follows from our goals:
* High-level code is more compact, and helps reduce the impact of compilation on application size.
* High-level constructs that match what the developer wrote are easier to debug.
* High-level code is more compact, and helps reduce the impact of compilation on application size
* High-level constructs that match what the developer wrote are easier to debug
* From the above, it then follows that the compiler's internal representation must also be high-level enough to be able to output the original high-level constructs. The internal representation is what we call a High-level Intermediate Representation (HIR) — a name borrowed from Rust Compiler. However, React Compiler's HIR is perhaps even more suited to this name, as it retains high-level information (distinguishing if vs logical vs ternary, or for vs while vs for..of) but also represents code as a control-flow graph with no nesting.
## Architecture
@@ -47,7 +47,7 @@ The core of the compiler is largely decoupled from Babel, using its own intermed
- Validation: We run various validation passes to check that the input is valid React, ie that it does not break the rules. This includes looking for conditional hook calls, unconditional setState calls, etc.
- **Optimization**: Various passes such as dead code elimination and constant propagation can generally improve performance and reduce the amount of instructions to be optimized later.
- **Type Inference** (InferTypes): We run a conservative type inference pass to identify certain key types of data that may appear in the program that are relevant for further analysis, such as which values are hooks, primitives, etc.
- **Inferring Reactive Scopes**: Several passes are involved in determining groups of values that are created/mutated together and the set of instructions involved in creating/mutating those values. We call these groups "reactive scopes", and each can have one or more declarations (or occasionally a reassignment).
- **Inferring Reactive Scopes**: Several passes are involved in determing groups of values that are created/mutated together and the set of instructions involved in creating/mutating those values. We call these groups "reactive scopes", and each can have one or more declarations (or occasionally a reassignment).
- **Constructing/Optimizing Reactive Scopes**: Once the compiler determines the set of reactive scopes, it then transforms the program to make these scopes explicit in the HIR. The code is later converted to a ReactiveFunction, which is a hybrid of the HIR and an AST. Scopes are further pruned and transformed. For example, the compiler cannot make hook calls conditional, so any reactive scopes that contain a hook call must be pruned. If two consecutive scopes will always invalidate together, we attempt to merge them to reduce overhead, etc.
- **Codegen**: Finally, the ReactiveFunction hybrid HIR/AST is converted back to a raw Babel AST node, and returned to the Babel plugin.
- **Babel Plugin**: The Babel plugin replaces the original node with the new version.

View File

@@ -1 +0,0 @@
Reference library compiled with React Compiler.

View File

@@ -1,24 +0,0 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

View File

@@ -1,50 +0,0 @@
# React + TypeScript + Vite
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
Currently, two official plugins are available:
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
## Expanding the ESLint configuration
If you are developing a production application, we recommend updating the configuration to enable type aware lint rules:
- Configure the top-level `parserOptions` property like this:
```js
export default tseslint.config({
languageOptions: {
// other options...
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
},
})
```
- Replace `tseslint.configs.recommended` to `tseslint.configs.recommendedTypeChecked` or `tseslint.configs.strictTypeChecked`
- Optionally add `...tseslint.configs.stylisticTypeChecked`
- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and update the config:
```js
// eslint.config.js
import react from 'eslint-plugin-react'
export default tseslint.config({
// Set the react version
settings: { react: { version: '18.3' } },
plugins: {
// Add the react plugin
react,
},
rules: {
// other rules...
// Enable its recommended rules
...react.configs.recommended.rules,
...react.configs['jsx-runtime'].rules,
},
})
```

View File

@@ -1,28 +0,0 @@
import js from '@eslint/js';
import globals from 'globals';
import reactHooks from 'eslint-plugin-react-hooks';
import reactRefresh from 'eslint-plugin-react-refresh';
import tseslint from 'typescript-eslint';
export default tseslint.config(
{ignores: ['dist']},
{
extends: [js.configs.recommended, ...tseslint.configs.recommended],
files: ['**/*.{ts,tsx}'],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
plugins: {
'react-hooks': reactHooks,
'react-refresh': reactRefresh,
},
rules: {
...reactHooks.configs.recommended.rules,
'react-refresh/only-export-components': [
'warn',
{allowConstantExport: true},
],
},
}
);

View File

@@ -1,13 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React + TS</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View File

@@ -1,32 +0,0 @@
{
"name": "app-18",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"react": "^18.3.1",
"react-compiler-runtime": "0.0.0-experimental-8d8e73f-20241009",
"react-dom": "^18.3.1",
"runtime-compat-lib": "file:../lib"
},
"devDependencies": {
"@eslint/js": "^9.11.1",
"@types/react": "^18.3.10",
"@types/react-dom": "^18.3.0",
"@vitejs/plugin-react": "^4.3.2",
"babel-plugin-react-compiler": "0.0.0-experimental-58c2b1c-20241009",
"eslint": "^9.11.1",
"eslint-plugin-react-hooks": "^5.1.0-rc.0",
"eslint-plugin-react-refresh": "^0.4.12",
"globals": "^15.9.0",
"typescript": "^5.5.3",
"typescript-eslint": "^8.7.0",
"vite": "^5.4.8"
}
}

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -1,42 +0,0 @@
#root {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
text-align: center;
}
.logo {
height: 6em;
padding: 1.5em;
will-change: filter;
transition: filter 300ms;
}
.logo:hover {
filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.react:hover {
filter: drop-shadow(0 0 2em #61dafbaa);
}
@keyframes logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
@media (prefers-reduced-motion: no-preference) {
a:nth-of-type(2) .logo {
animation: logo-spin infinite 20s linear;
}
}
.card {
padding: 2em;
}
.read-the-docs {
color: #888;
}

View File

@@ -1,16 +0,0 @@
import 'react';
import './App.css';
// @ts-expect-error no types
import {useTime} from 'runtime-compat-lib';
function App() {
const time = useTime();
return (
<>
<h1>React 18</h1>
<span>Current time: {time.toLocaleString()}</span>
</>
);
}
export default App;

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>

Before

Width:  |  Height:  |  Size: 4.0 KiB

View File

@@ -1,68 +0,0 @@
:root {
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
}
body {
margin: 0;
display: flex;
place-items: center;
min-width: 320px;
min-height: 100vh;
}
h1 {
font-size: 3.2em;
line-height: 1.1;
}
button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
background-color: #1a1a1a;
cursor: pointer;
transition: border-color 0.25s;
}
button:hover {
border-color: #646cff;
}
button:focus,
button:focus-visible {
outline: 4px auto -webkit-focus-ring-color;
}
@media (prefers-color-scheme: light) {
:root {
color: #213547;
background-color: #ffffff;
}
a:hover {
color: #747bff;
}
button {
background-color: #f9f9f9;
}
}

View File

@@ -1,10 +0,0 @@
import {StrictMode} from 'react';
import {createRoot} from 'react-dom/client';
import App from './App.tsx';
import './index.css';
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>,
);

View File

@@ -1 +0,0 @@
/// <reference types="vite/client" />

View File

@@ -1,24 +0,0 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src"]
}

View File

@@ -1 +0,0 @@
{"root":["./src/app.tsx","./src/main.tsx","./src/vite-env.d.ts"],"version":"5.6.3"}

View File

@@ -1,7 +0,0 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}

View File

@@ -1,22 +0,0 @@
{
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2023"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["vite.config.ts"]
}

View File

@@ -1 +0,0 @@
{"root":["./vite.config.ts"],"version":"5.6.3"}

View File

@@ -1,11 +0,0 @@
import {defineConfig} from 'vite';
import react from '@vitejs/plugin-react';
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
react({
babel: {plugins: [['babel-plugin-react-compiler', {target: '18'}]]},
}),
],
});

File diff suppressed because it is too large Load Diff

View File

@@ -1,24 +0,0 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

View File

@@ -1,50 +0,0 @@
# React + TypeScript + Vite
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
Currently, two official plugins are available:
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
## Expanding the ESLint configuration
If you are developing a production application, we recommend updating the configuration to enable type aware lint rules:
- Configure the top-level `parserOptions` property like this:
```js
export default tseslint.config({
languageOptions: {
// other options...
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
},
})
```
- Replace `tseslint.configs.recommended` to `tseslint.configs.recommendedTypeChecked` or `tseslint.configs.strictTypeChecked`
- Optionally add `...tseslint.configs.stylisticTypeChecked`
- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and update the config:
```js
// eslint.config.js
import react from 'eslint-plugin-react'
export default tseslint.config({
// Set the react version
settings: { react: { version: '18.3' } },
plugins: {
// Add the react plugin
react,
},
rules: {
// other rules...
// Enable its recommended rules
...react.configs.recommended.rules,
...react.configs['jsx-runtime'].rules,
},
})
```

View File

@@ -1,28 +0,0 @@
import js from '@eslint/js';
import globals from 'globals';
import reactHooks from 'eslint-plugin-react-hooks';
import reactRefresh from 'eslint-plugin-react-refresh';
import tseslint from 'typescript-eslint';
export default tseslint.config(
{ignores: ['dist']},
{
extends: [js.configs.recommended, ...tseslint.configs.recommended],
files: ['**/*.{ts,tsx}'],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
plugins: {
'react-hooks': reactHooks,
'react-refresh': reactRefresh,
},
rules: {
...reactHooks.configs.recommended.rules,
'react-refresh/only-export-components': [
'warn',
{allowConstantExport: true},
],
},
}
);

View File

@@ -1,13 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React + TS</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View File

@@ -1,31 +0,0 @@
{
"name": "app-19",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"react": "19.0.0-beta-26f2496093-20240514",
"react-dom": "19.0.0-beta-26f2496093-20240514",
"runtime-compat-lib": "file:../lib"
},
"devDependencies": {
"@eslint/js": "^9.11.1",
"@types/react": "^18.3.10",
"@types/react-dom": "^18.3.0",
"@vitejs/plugin-react": "^4.3.2",
"babel-plugin-react-compiler": "0.0.0-experimental-58c2b1c-20241009",
"eslint": "^9.11.1",
"eslint-plugin-react-hooks": "^5.1.0-rc.0",
"eslint-plugin-react-refresh": "^0.4.12",
"globals": "^15.9.0",
"typescript": "^5.5.3",
"typescript-eslint": "^8.7.0",
"vite": "^5.4.8"
}
}

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -1,42 +0,0 @@
#root {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
text-align: center;
}
.logo {
height: 6em;
padding: 1.5em;
will-change: filter;
transition: filter 300ms;
}
.logo:hover {
filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.react:hover {
filter: drop-shadow(0 0 2em #61dafbaa);
}
@keyframes logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
@media (prefers-reduced-motion: no-preference) {
a:nth-of-type(2) .logo {
animation: logo-spin infinite 20s linear;
}
}
.card {
padding: 2em;
}
.read-the-docs {
color: #888;
}

View File

@@ -1,16 +0,0 @@
import 'react';
import './App.css';
// @ts-expect-error no types
import {useTime} from 'runtime-compat-lib';
function App() {
const time = useTime();
return (
<>
<h1>React 19</h1>
<span>Current time: {time.toLocaleString()}</span>
</>
);
}
export default App;

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>

Before

Width:  |  Height:  |  Size: 4.0 KiB

View File

@@ -1,68 +0,0 @@
:root {
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
}
body {
margin: 0;
display: flex;
place-items: center;
min-width: 320px;
min-height: 100vh;
}
h1 {
font-size: 3.2em;
line-height: 1.1;
}
button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
background-color: #1a1a1a;
cursor: pointer;
transition: border-color 0.25s;
}
button:hover {
border-color: #646cff;
}
button:focus,
button:focus-visible {
outline: 4px auto -webkit-focus-ring-color;
}
@media (prefers-color-scheme: light) {
:root {
color: #213547;
background-color: #ffffff;
}
a:hover {
color: #747bff;
}
button {
background-color: #f9f9f9;
}
}

View File

@@ -1,10 +0,0 @@
import {StrictMode} from 'react';
import {createRoot} from 'react-dom/client';
import App from './App.tsx';
import './index.css';
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>,
);

View File

@@ -1 +0,0 @@
/// <reference types="vite/client" />

View File

@@ -1,24 +0,0 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src"]
}

View File

@@ -1 +0,0 @@
{"root":["./src/app.tsx","./src/main.tsx","./src/vite-env.d.ts"],"version":"5.6.3"}

View File

@@ -1,7 +0,0 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}

View File

@@ -1,22 +0,0 @@
{
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2023"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["vite.config.ts"]
}

View File

@@ -1 +0,0 @@
{"root":["./vite.config.ts"],"version":"5.6.3"}

View File

@@ -1,11 +0,0 @@
import {defineConfig} from 'vite';
import react from '@vitejs/plugin-react';
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
react({
babel: {plugins: [['babel-plugin-react-compiler', {target: '19'}]]},
}),
],
});

File diff suppressed because it is too large Load Diff

View File

@@ -1,10 +0,0 @@
const plugins = [
[
'babel-plugin-react-compiler',
{
target: '18',
},
],
];
module.exports = {plugins};

View File

@@ -1,13 +0,0 @@
import {useState, useEffect} from 'react';
export function useTime() {
const [time, setTime] = useState(() => new Date());
useEffect(() => {
const id = setInterval(() => {
setTime(new Date());
}, 1000);
return () => clearInterval(id);
}, []);
return time;
}

View File

@@ -1,32 +0,0 @@
{
"name": "runtime-compat-lib",
"version": "0.0.0",
"description": "Testing ground for libraries compiled with React Compiler",
"main": "dist/index.js",
"scripts": {
"build": "rimraf dist && rollup --config --bundleConfigAsCjs",
"test": "echo 'no tests'"
},
"license": "MIT",
"devDependencies": {
"@babel/cli": "^7.25.7",
"@babel/core": "^7.25.7",
"@babel/preset-env": "^7.25.7",
"@rollup/plugin-babel": "^6.0.4",
"@rollup/plugin-json": "^6.1.0",
"babel-plugin-react-compiler": "0.0.0-experimental-58c2b1c-20241009",
"@rollup/plugin-terser": "^0.4.4",
"react": "19.0.0-beta-26f2496093-20240514",
"react-dom": "19.0.0-beta-26f2496093-20240514",
"rimraf": "5",
"rollup": "^4.22.4",
"rollup-plugin-banner2": "^1.2.3",
"rollup-plugin-prettier": "^4.1.1"
},
"dependencies": {
"react-compiler-runtime": "0.0.0-experimental-8d8e73f-20241009"
},
"peerDependencies": {
"react": "^18 || ^19"
}
}

View File

@@ -1,51 +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 json from '@rollup/plugin-json';
import terser from '@rollup/plugin-terser';
import prettier from 'rollup-plugin-prettier';
import banner2 from 'rollup-plugin-banner2';
import babel from '@rollup/plugin-babel';
const ROLLUP_CONFIG = {
input: 'index.js',
output: {
file: 'dist/index.js',
format: 'esm',
sourcemap: false,
exports: 'named',
},
plugins: [
json(),
babel({babelHelpers: 'bundled'}),
terser({
format: {
comments: false,
},
compress: false,
mangle: false,
}),
prettier(),
banner2(
() => `/**
* 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.
*
* @lightSyntaxTransform
* @noflow
* @nolint
* @preventMunge
* @preserve-invariant-messages
*/
`
),
],
};
export default ROLLUP_CONFIG;

File diff suppressed because it is too large Load Diff

View File

@@ -1,13 +0,0 @@
#!/usr/bin/env bash
# Copyright (c) Meta Platforms, Inc. and affiliates.
#
# This source code is licensed under the MIT license found in the
# LICENSE file in the root directory of this source tree.
set -eo pipefail
HERE=$(pwd)
cd lib && yarn --silent link
cd $HERE/app-18 && yarn --silent link runtime-compat-lib
cd $HERE/app-19 && yarn --silent link runtime-compat-lib

View File

@@ -2,7 +2,12 @@
"private": true,
"workspaces": {
"packages": [
"packages/*"
"packages/*",
"apps/*"
],
"nohoist": [
"**/next",
"**/next/**"
]
},
"repository": {
@@ -15,15 +20,14 @@
"start": "yarn workspace playground run start",
"next": "yarn workspace playground run dev",
"build": "yarn workspaces run build",
"dev": "echo 'DEPRECATED: use `cd apps/playground && yarn dev` instead!' && sleep 5 && cd apps/playground && yarn dev",
"dev": "concurrently --kill-others -n compiler,runtime,playground \"yarn workspace babel-plugin-react-compiler run build --watch\" \"yarn workspace react-compiler-runtime run build --watch\" \"wait-on packages/babel-plugin-react-compiler/dist/index.js && yarn workspace playground run dev\"",
"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"
},
"dependencies": {
"fs-extra": "^4.0.2"
"postinstall": "perl -p -i -e 's/react\\.element/react.transitional.element/' packages/snap/node_modules/fbt/lib/FbtReactUtil.js && perl -p -i -e 's/didWarnAboutUsingAct = false;/didWarnAboutUsingAct = true;/' packages/babel-plugin-react-compiler/node_modules/react-dom/cjs/react-dom-test-utils.development.js",
"npm:publish": "node scripts/release/publish-manual"
},
"dependencies": {},
"devDependencies": {
"@rollup/plugin-commonjs": "^25.0.7",
"@rollup/plugin-json": "^6.1.0",
@@ -33,12 +37,11 @@
"@tsconfig/strictest": "^2.0.5",
"concurrently": "^7.4.0",
"folder-hash": "^4.0.4",
"object-assign": "^4.1.1",
"ora": "5.4.1",
"prettier": "^3.3.3",
"prettier-plugin-hermes-parser": "^0.23.0",
"prompt-promise": "^1.0.3",
"rollup": "^4.22.4",
"rollup": "^4.13.2",
"rollup-plugin-banner2": "^1.2.3",
"rollup-plugin-prettier": "^4.1.1",
"typescript": "^5.4.3",

View File

@@ -9,7 +9,7 @@
],
"scripts": {
"build": "rimraf dist && rollup --config --bundleConfigAsCjs",
"test": "./scripts/link-react-compiler-runtime.sh && yarn snap:ci",
"test": "yarn snap:ci",
"jest": "yarn build && ts-node node_modules/.bin/jest",
"snap": "node ../snap/dist/main.js",
"snap:build": "yarn workspace snap run build",
@@ -41,12 +41,12 @@
"@types/invariant": "^2.2.35",
"@types/jest": "^29.0.3",
"@types/node": "^18.7.18",
"@typescript-eslint/eslint-plugin": "^8.7.0",
"@typescript-eslint/parser": "^8.7.0",
"@typescript-eslint/eslint-plugin": "^7.4.0",
"@typescript-eslint/parser": "^7.4.0",
"babel-jest": "^29.0.3",
"babel-plugin-fbt": "^1.0.0",
"babel-plugin-fbt-runtime": "^1.0.0",
"eslint": "^8.57.1",
"eslint": "8.27.0",
"glob": "^7.1.6",
"jest": "^29.0.3",
"jest-environment-jsdom": "^29.0.3",

View File

@@ -1,10 +0,0 @@
#!/usr/bin/env bash
# Copyright (c) Meta Platforms, Inc. and affiliates.
#
# This source code is licensed under the MIT license found in the
# LICENSE file in the root directory of this source tree.
set -eo pipefail
yarn --silent workspace react-compiler-runtime link
yarn --silent workspace babel-plugin-react-compiler link react-compiler-runtime

View File

@@ -7,41 +7,10 @@
import {NodePath} from '@babel/core';
import * as t from '@babel/types';
import {CompilerError, ErrorSeverity} from '../CompilerError';
import {EnvironmentConfig, ExternalFunction, GeneratedSource} from '../HIR';
import {CompilerError} from '../CompilerError';
import {ExternalFunction, GeneratedSource} from '../HIR';
import {getOrInsertDefault} from '../Utils/utils';
export function validateRestrictedImports(
path: NodePath<t.Program>,
{validateBlocklistedImports}: EnvironmentConfig,
): CompilerError | null {
if (
validateBlocklistedImports == null ||
validateBlocklistedImports.length === 0
) {
return null;
}
const error = new CompilerError();
const restrictedImports = new Set(validateBlocklistedImports);
path.traverse({
ImportDeclaration(importDeclPath) {
if (restrictedImports.has(importDeclPath.node.source.value)) {
error.push({
severity: ErrorSeverity.Todo,
reason: 'Bailing out due to blocklisted import',
description: `Import from module ${importDeclPath.node.source.value}`,
loc: importDeclPath.node.loc ?? null,
});
}
},
});
if (error.hasErrors()) {
return error;
} else {
return null;
}
}
export function addImportsToProgram(
path: NodePath<t.Program>,
importList: Array<ExternalFunction>,

View File

@@ -7,14 +7,9 @@
import * as t from '@babel/types';
import {z} from 'zod';
import {CompilerError, CompilerErrorDetailOptions} from '../CompilerError';
import {
EnvironmentConfig,
ExternalFunction,
parseEnvironmentConfig,
} from '../HIR/Environment';
import {CompilerErrorDetailOptions} from '../CompilerError';
import {ExternalFunction, PartialEnvironmentConfig} from '../HIR/Environment';
import {hasOwnProperty} from '../Utils/utils';
import {fromZodError} from 'zod-validation-error';
const PanicThresholdOptionsSchema = z.enum([
/*
@@ -37,7 +32,7 @@ const PanicThresholdOptionsSchema = z.enum([
export type PanicThresholdOptions = z.infer<typeof PanicThresholdOptionsSchema>;
export type PluginOptions = {
environment: EnvironmentConfig;
environment: PartialEnvironmentConfig | null;
logger: Logger | null;
@@ -87,6 +82,17 @@ export type PluginOptions = {
*/
compilationMode: CompilationMode;
/*
* If enabled, Forget will import `useMemoCache` from the given module
* instead of `react/compiler-runtime`.
*
* ```
* // If set to "react-compiler-runtime"
* import {c as useMemoCache} from 'react-compiler-runtime';
* ```
*/
runtimeModule?: string | null | undefined;
/**
* By default React Compiler will skip compilation of code that suppresses the default
* React ESLint rules, since this is a strong indication that the code may be breaking React rules
@@ -111,19 +117,8 @@ export type PluginOptions = {
* Set this flag (on by default) to automatically check for this library and activate the support.
*/
enableReanimatedCheck: boolean;
/**
* The minimum major version of React that the compiler should emit code for. If the target is 19
* or higher, the compiler emits direct imports of React runtime APIs needed by the compiler. On
* versions prior to 19, an extra runtime package react-compiler-runtime is necessary to provide
* a userspace approximation of runtime APIs.
*/
target: CompilerReactTarget;
};
const CompilerReactTargetSchema = z.enum(['17', '18', '19']);
export type CompilerReactTarget = z.infer<typeof CompilerReactTargetSchema>;
const CompilationModeSchema = z.enum([
/*
* Compiles functions annotated with "use forget" or component/hook-like functions.
@@ -170,12 +165,6 @@ export type LoggerEvent =
fnLoc: t.SourceLocation | null;
detail: Omit<Omit<CompilerErrorDetailOptions, 'severity'>, 'suggestions'>;
}
| {
kind: 'CompileSkip';
fnLoc: t.SourceLocation | null;
reason: string;
loc: t.SourceLocation | null;
}
| {
kind: 'CompileSuccess';
fnLoc: t.SourceLocation | null;
@@ -199,18 +188,18 @@ export type Logger = {
export const defaultOptions: PluginOptions = {
compilationMode: 'infer',
panicThreshold: 'none',
environment: parseEnvironmentConfig({}).unwrap(),
environment: {},
logger: null,
gating: null,
noEmit: false,
runtimeModule: null,
eslintSuppressionRules: null,
flowSuppressions: true,
flowSuppressions: false,
ignoreUseNoForget: false,
sources: filename => {
return filename.indexOf('node_modules') === -1;
},
enableReanimatedCheck: true,
target: '19',
} as const;
export function parsePluginOptions(obj: unknown): PluginOptions {
@@ -224,48 +213,12 @@ export function parsePluginOptions(obj: unknown): PluginOptions {
value = value.toLowerCase();
}
if (isCompilerFlag(key)) {
switch (key) {
case 'environment': {
const environmentResult = parseEnvironmentConfig(value);
if (environmentResult.isErr()) {
CompilerError.throwInvalidConfig({
reason:
'Error in validating environment config. This is an advanced setting and not meant to be used directly',
description: environmentResult.unwrapErr().toString(),
suggestions: null,
loc: null,
});
}
parsedOptions[key] = environmentResult.unwrap();
break;
}
case 'target': {
parsedOptions[key] = parseTargetConfig(value);
break;
}
default: {
parsedOptions[key] = value;
}
}
parsedOptions[key] = value;
}
}
return {...defaultOptions, ...parsedOptions};
}
export function parseTargetConfig(value: unknown): CompilerReactTarget {
const parsed = CompilerReactTargetSchema.safeParse(value);
if (parsed.success) {
return parsed.data;
} else {
CompilerError.throwInvalidConfig({
reason: 'Not a valid target',
description: `${fromZodError(parsed.error)}`,
suggestions: null,
loc: null,
});
}
}
function isCompilerFlag(s: string): s is keyof PluginOptions {
return hasOwnProperty(defaultOptions, s);
}

View File

@@ -41,19 +41,23 @@ import {
constantPropagation,
deadCodeElimination,
pruneMaybeThrows,
inlineJsxTransform,
} from '../Optimization';
import {instructionReordering} from '../Optimization/InstructionReordering';
import {
CodegenFunction,
alignObjectMethodScopes,
alignReactiveScopesToBlockScopes,
assertScopeInstructionsWithinScopes,
assertWellFormedBreakTargets,
buildReactiveBlocks,
buildReactiveFunction,
codegenFunction,
extractScopeDeclarationsFromDestructuring,
flattenReactiveLoops,
flattenScopesWithHooksOrUse,
inferReactiveScopeVariables,
memoizeFbtAndMacroOperandsInSameScope,
memoizeFbtOperandsInSameScope,
mergeOverlappingReactiveScopes,
mergeReactiveScopesThatInvalidateTogether,
promoteUsedTemporaries,
propagateEarlyReturns,
@@ -73,11 +77,7 @@ import {flattenScopesWithHooksOrUseHIR} from '../ReactiveScopes/FlattenScopesWit
import {pruneAlwaysInvalidatingScopes} from '../ReactiveScopes/PruneAlwaysInvalidatingScopes';
import pruneInitializationDependencies from '../ReactiveScopes/PruneInitializationDependencies';
import {stabilizeBlockIds} from '../ReactiveScopes/StabilizeBlockIds';
import {
eliminateRedundantPhi,
enterSSA,
rewriteInstructionKindsBasedOnReassignment,
} from '../SSA';
import {eliminateRedundantPhi, enterSSA, leaveSSA} from '../SSA';
import {inferTypes} from '../TypeInference';
import {
logCodegenFunction,
@@ -98,11 +98,6 @@ import {
} from '../Validation';
import {validateLocalsNotReassignedAfterRender} from '../Validation/ValidateLocalsNotReassignedAfterRender';
import {outlineFunctions} from '../Optimization/OutlineFunctions';
import {propagatePhiTypes} from '../TypeInference/PropagatePhiTypes';
import {lowerContextAccess} from '../Optimization/LowerContextAccess';
import {validateNoSetStateInPassiveEffects} from '../Validation/ValidateNoSetStateInPassiveEffects';
import {validateNoJSXInTryStatement} from '../Validation/ValidateNoJSXInTryStatement';
import {propagateScopeDependenciesHIR} from '../HIR/PropagateScopeDependenciesHIR';
export type CompilerPipelineValue =
| {kind: 'ast'; name: string; value: CodegenFunction}
@@ -132,11 +127,11 @@ export function* run(
code,
useMemoCacheIdentifier,
);
yield log({
yield {
kind: 'debug',
name: 'EnvironmentConfig',
value: prettyFormat(env.config),
});
};
const ast = yield* runWithEnvironment(func, env);
return ast;
}
@@ -204,10 +199,6 @@ function* runWithEnvironment(
validateNoCapitalizedCalls(hir);
}
if (env.config.lowerContextAccess) {
lowerContextAccess(hir, env.config.lowerContextAccess);
}
analyseFunctions(hir);
yield log({kind: 'hir', name: 'AnalyseFunctions', value: hir});
@@ -243,43 +234,17 @@ function* runWithEnvironment(
validateNoSetStateInRender(hir);
}
if (env.config.validateNoSetStateInPassiveEffects) {
validateNoSetStateInPassiveEffects(hir);
}
if (env.config.validateNoJSXInTryStatements) {
validateNoJSXInTryStatement(hir);
}
inferReactivePlaces(hir);
yield log({kind: 'hir', name: 'InferReactivePlaces', value: hir});
rewriteInstructionKindsBasedOnReassignment(hir);
yield log({
kind: 'hir',
name: 'RewriteInstructionKindsBasedOnReassignment',
value: hir,
});
propagatePhiTypes(hir);
yield log({
kind: 'hir',
name: 'PropagatePhiTypes',
value: hir,
});
leaveSSA(hir);
yield log({kind: 'hir', name: 'LeaveSSA', value: hir});
inferReactiveScopeVariables(hir);
yield log({kind: 'hir', name: 'InferReactiveScopeVariables', value: hir});
const fbtOperands = memoizeFbtAndMacroOperandsInSameScope(hir);
yield log({
kind: 'hir',
name: 'MemoizeFbtAndMacroOperandsInSameScope',
value: hir,
});
if (env.config.enableFunctionOutlining) {
outlineFunctions(hir, fbtOperands);
outlineFunctions(hir);
yield log({kind: 'hir', name: 'OutlineFunctions', value: hir});
}
@@ -297,68 +262,60 @@ function* runWithEnvironment(
value: hir,
});
pruneUnusedLabelsHIR(hir);
const fbtOperands = memoizeFbtOperandsInSameScope(hir);
yield log({
kind: 'hir',
name: 'PruneUnusedLabelsHIR',
name: 'MemoizeFbtAndMacroOperandsInSameScope',
value: hir,
});
alignReactiveScopesToBlockScopesHIR(hir);
yield log({
kind: 'hir',
name: 'AlignReactiveScopesToBlockScopesHIR',
value: hir,
});
mergeOverlappingReactiveScopesHIR(hir);
yield log({
kind: 'hir',
name: 'MergeOverlappingReactiveScopesHIR',
value: hir,
});
assertValidBlockNesting(hir);
buildReactiveScopeTerminalsHIR(hir);
yield log({
kind: 'hir',
name: 'BuildReactiveScopeTerminalsHIR',
value: hir,
});
assertValidBlockNesting(hir);
flattenReactiveLoopsHIR(hir);
yield log({
kind: 'hir',
name: 'FlattenReactiveLoopsHIR',
value: hir,
});
flattenScopesWithHooksOrUseHIR(hir);
yield log({
kind: 'hir',
name: 'FlattenScopesWithHooksOrUseHIR',
value: hir,
});
assertTerminalSuccessorsExist(hir);
assertTerminalPredsExist(hir);
if (env.config.enablePropagateDepsInHIR) {
propagateScopeDependenciesHIR(hir);
if (env.config.enableReactiveScopesInHIR) {
pruneUnusedLabelsHIR(hir);
yield log({
kind: 'hir',
name: 'PropagateScopeDependenciesHIR',
name: 'PruneUnusedLabelsHIR',
value: hir,
});
}
if (env.config.inlineJsxTransform) {
inlineJsxTransform(hir, env.config.inlineJsxTransform);
alignReactiveScopesToBlockScopesHIR(hir);
yield log({
kind: 'hir',
name: 'inlineJsxTransform',
name: 'AlignReactiveScopesToBlockScopesHIR',
value: hir,
});
mergeOverlappingReactiveScopesHIR(hir);
yield log({
kind: 'hir',
name: 'MergeOverlappingReactiveScopesHIR',
value: hir,
});
assertValidBlockNesting(hir);
buildReactiveScopeTerminalsHIR(hir);
yield log({
kind: 'hir',
name: 'BuildReactiveScopeTerminalsHIR',
value: hir,
});
assertValidBlockNesting(hir);
flattenReactiveLoopsHIR(hir);
yield log({
kind: 'hir',
name: 'FlattenReactiveLoopsHIR',
value: hir,
});
flattenScopesWithHooksOrUseHIR(hir);
yield log({
kind: 'hir',
name: 'FlattenScopesWithHooksOrUseHIR',
value: hir,
});
assertTerminalSuccessorsExist(hir);
assertTerminalPredsExist(hir);
}
const reactiveFunction = buildReactiveFunction(hir);
@@ -376,17 +333,53 @@ function* runWithEnvironment(
name: 'PruneUnusedLabels',
value: reactiveFunction,
});
assertScopeInstructionsWithinScopes(reactiveFunction);
if (!env.config.enablePropagateDepsInHIR) {
propagateScopeDependencies(reactiveFunction);
if (!env.config.enableReactiveScopesInHIR) {
alignReactiveScopesToBlockScopes(reactiveFunction);
yield log({
kind: 'reactive',
name: 'PropagateScopeDependencies',
name: 'AlignReactiveScopesToBlockScopes',
value: reactiveFunction,
});
mergeOverlappingReactiveScopes(reactiveFunction);
yield log({
kind: 'reactive',
name: 'MergeOverlappingReactiveScopes',
value: reactiveFunction,
});
buildReactiveBlocks(reactiveFunction);
yield log({
kind: 'reactive',
name: 'BuildReactiveBlocks',
value: reactiveFunction,
});
flattenReactiveLoops(reactiveFunction);
yield log({
kind: 'reactive',
name: 'FlattenReactiveLoops',
value: reactiveFunction,
});
flattenScopesWithHooksOrUse(reactiveFunction);
yield log({
kind: 'reactive',
name: 'FlattenScopesWithHooks',
value: reactiveFunction,
});
}
assertScopeInstructionsWithinScopes(reactiveFunction);
propagateScopeDependencies(reactiveFunction);
yield log({
kind: 'reactive',
name: 'PropagateScopeDependencies',
value: reactiveFunction,
});
pruneNonEscapingScopes(reactiveFunction);
yield log({
kind: 'reactive',
@@ -438,13 +431,6 @@ function* runWithEnvironment(
value: reactiveFunction,
});
pruneUnusedLValues(reactiveFunction);
yield log({
kind: 'reactive',
name: 'PruneUnusedLValues',
value: reactiveFunction,
});
promoteUsedTemporaries(reactiveFunction);
yield log({
kind: 'reactive',
@@ -452,6 +438,13 @@ function* runWithEnvironment(
value: reactiveFunction,
});
pruneUnusedLValues(reactiveFunction);
yield log({
kind: 'reactive',
name: 'PruneUnusedLValues',
value: reactiveFunction,
});
extractScopeDeclarationsFromDestructuring(reactiveFunction);
yield log({
kind: 'reactive',
@@ -564,14 +557,3 @@ export function log(value: CompilerPipelineValue): CompilerPipelineValue {
}
return value;
}
export function* runPlayground(
func: NodePath<
t.FunctionDeclaration | t.ArrowFunctionExpression | t.FunctionExpression
>,
config: EnvironmentConfig,
fnType: ReactFunctionType,
): Generator<CompilerPipelineValue, CodegenFunction> {
const ast = yield* run(func, config, fnType, '_c', null, null, null);
return ast;
}

View File

@@ -13,9 +13,9 @@ import {
ErrorSeverity,
} from '../CompilerError';
import {
EnvironmentConfig,
ExternalFunction,
ReactFunctionType,
parseEnvironmentConfig,
tryParseExternalFunction,
} from '../HIR/Environment';
import {CodegenFunction} from '../ReactiveScopes';
@@ -23,11 +23,7 @@ import {isComponentDeclaration} from '../Utils/ComponentDeclaration';
import {isHookDeclaration} from '../Utils/HookDeclaration';
import {assertExhaustive} from '../Utils/utils';
import {insertGatedFunctionDeclaration} from './Gating';
import {
addImportsToProgram,
updateMemoCacheFunctionImport,
validateRestrictedImports,
} from './Imports';
import {addImportsToProgram, updateMemoCacheFunctionImport} from './Imports';
import {PluginOptions} from './Options';
import {compileFn} from './Pipeline';
import {
@@ -42,23 +38,34 @@ export type CompilerPass = {
comments: Array<t.CommentBlock | t.CommentLine>;
code: string | null;
};
const OPT_IN_DIRECTIVES = new Set(['use forget', 'use memo']);
export const OPT_OUT_DIRECTIVES = new Set(['use no forget', 'use no memo']);
function findDirectiveEnablingMemoization(
directives: Array<t.Directive>,
): Array<t.Directive> {
return directives.filter(directive =>
OPT_IN_DIRECTIVES.has(directive.value.value),
);
): t.Directive | null {
for (const directive of directives) {
const directiveValue = directive.value.value;
if (directiveValue === 'use forget' || directiveValue === 'use memo') {
return directive;
}
}
return null;
}
function findDirectiveDisablingMemoization(
directives: Array<t.Directive>,
): Array<t.Directive> {
return directives.filter(directive =>
OPT_OUT_DIRECTIVES.has(directive.value.value),
);
options: PluginOptions,
): t.Directive | null {
for (const directive of directives) {
const directiveValue = directive.value.value;
if (
(directiveValue === 'use no forget' ||
directiveValue === 'use no memo') &&
!options.ignoreUseNoForget
) {
return directive;
}
}
return null;
}
function isCriticalError(err: unknown): boolean {
@@ -90,7 +97,7 @@ export type CompileResult = {
compiledFn: CodegenFunction;
};
function logError(
function handleError(
err: unknown,
pass: CompilerPass,
fnLoc: t.SourceLocation | null,
@@ -119,13 +126,6 @@ function logError(
});
}
}
}
function handleError(
err: unknown,
pass: CompilerPass,
fnLoc: t.SourceLocation | null,
): void {
logError(err, pass, fnLoc);
if (
pass.opts.panicThreshold === 'all_errors' ||
(pass.opts.panicThreshold === 'critical_errors' && isCriticalError(err)) ||
@@ -263,7 +263,7 @@ function isFilePartOfSources(
return sources(filename);
}
for (const prefix of sources) {
for (const prefix in sources) {
if (filename.indexOf(prefix) !== -1) {
return true;
}
@@ -272,32 +272,45 @@ function isFilePartOfSources(
return false;
}
/**
* `compileProgram` is directly invoked by the react-compiler babel plugin, so
* exceptions thrown by this function will fail the babel build.
* - call `handleError` if your error is recoverable.
* Unless the error is a warning / info diagnostic, compilation of a function
* / entire file should also be skipped.
* - throw an exception if the error is fatal / not recoverable.
* Examples of this are invalid compiler configs or failure to codegen outlined
* functions *after* already emitting optimized components / hooks that invoke
* the outlined functions.
*/
export function compileProgram(
program: NodePath<t.Program>,
pass: CompilerPass,
): void {
if (shouldSkipCompilation(program, pass)) {
if (pass.opts.sources) {
if (pass.filename === null) {
const error = new CompilerError();
error.pushErrorDetail(
new CompilerErrorDetail({
reason: `Expected a filename but found none.`,
description:
"When the 'sources' config options is specified, the React compiler will only compile files with a name",
severity: ErrorSeverity.InvalidConfig,
loc: null,
}),
);
handleError(error, pass, null);
return;
}
if (!isFilePartOfSources(pass.opts.sources, pass.filename)) {
return;
}
}
// Top level "use no forget", skip this file entirely
if (
findDirectiveDisablingMemoization(program.node.directives, pass.opts) !=
null
) {
return;
}
const environment = pass.opts.environment;
const restrictedImportsErr = validateRestrictedImports(program, environment);
if (restrictedImportsErr) {
handleError(restrictedImportsErr, pass, null);
const environment = parseEnvironmentConfig(pass.opts.environment ?? {});
const useMemoCacheIdentifier = program.scope.generateUidIdentifier('c');
const moduleName = pass.opts.runtimeModule ?? 'react/compiler-runtime';
if (hasMemoCacheFunctionImport(program, moduleName)) {
return;
}
const useMemoCacheIdentifier = program.scope.generateUidIdentifier('c');
/*
* Record lint errors and critical errors as depending on Forget's config,
@@ -309,6 +322,8 @@ export function compileProgram(
pass.opts.eslintSuppressionRules ?? DEFAULT_ESLINT_SUPPRESSIONS,
pass.opts.flowSuppressions,
);
const lintError = suppressionsToCompilerError(suppressions);
let hasCriticalError = lintError != null;
const queue: Array<{
kind: 'original' | 'outlined';
fn: BabelFn;
@@ -317,7 +332,7 @@ export function compileProgram(
const compiledFns: Array<CompileResult> = [];
const traverseFunction = (fn: BabelFn, pass: CompilerPass): void => {
const fnType = getReactFunctionType(fn, pass, environment);
const fnType = getReactFunctionType(fn, pass);
if (fnType === null || ALREADY_COMPILED.has(fn.node)) {
return;
}
@@ -371,19 +386,7 @@ export function compileProgram(
fn: BabelFn,
fnType: ReactFunctionType,
): null | CodegenFunction => {
let optInDirectives: Array<t.Directive> = [];
let optOutDirectives: Array<t.Directive> = [];
if (fn.node.body.type === 'BlockStatement') {
optInDirectives = findDirectiveEnablingMemoization(
fn.node.body.directives,
);
optOutDirectives = findDirectiveDisablingMemoization(
fn.node.body.directives,
);
}
let compiledFn: CodegenFunction;
try {
if (lintError != null) {
/**
* Note that Babel does not attach comment nodes to nodes; they are dangling off of the
* Program node itself. We need to figure out whether an eslint suppression range
@@ -394,18 +397,30 @@ export function compileProgram(
fn,
);
if (suppressionsInFunction.length > 0) {
const lintError = suppressionsToCompilerError(suppressionsInFunction);
if (optOutDirectives.length > 0) {
logError(lintError, pass, fn.node.loc ?? null);
} else {
handleError(lintError, pass, fn.node.loc ?? null);
}
return null;
handleError(lintError, pass, fn.node.loc ?? null);
}
}
let compiledFn: CodegenFunction;
try {
/*
* TODO(lauren): Remove pass.opts.environment nullcheck once PluginOptions
* is validated
*/
if (environment.isErr()) {
CompilerError.throwInvalidConfig({
reason:
'Error in validating environment config. This is an advanced setting and not meant to be used directly',
description: environment.unwrapErr().toString(),
suggestions: null,
loc: null,
});
}
const config = environment.unwrap();
compiledFn = compileFn(
fn,
environment,
config,
fnType,
useMemoCacheIdentifier.name,
pass.opts.logger,
@@ -423,50 +438,12 @@ export function compileProgram(
prunedMemoValues: compiledFn.prunedMemoValues,
});
} catch (err) {
/**
* If an opt out directive is present, log only instead of throwing and don't mark as
* containing a critical error.
*/
if (fn.node.body.type === 'BlockStatement') {
if (optOutDirectives.length > 0) {
logError(err, pass, fn.node.loc ?? null);
return null;
}
}
hasCriticalError ||= isCriticalError(err);
handleError(err, pass, fn.node.loc ?? null);
return null;
}
/**
* Always compile functions with opt in directives.
*/
if (optInDirectives.length > 0) {
return compiledFn;
} else if (pass.opts.compilationMode === 'annotation') {
/**
* No opt-in directive in annotation mode, so don't insert the compiled function.
*/
return null;
}
/**
* Otherwise if 'use no forget/memo' is present, we still run the code through the compiler
* for validation but we don't mutate the babel AST. This allows us to flag if there is an
* unused 'use no forget/memo' directive.
*/
if (pass.opts.ignoreUseNoForget === false && optOutDirectives.length > 0) {
for (const directive of optOutDirectives) {
pass.opts.logger?.logEvent(pass.filename, {
kind: 'CompileSkip',
fnLoc: fn.node.body.loc ?? null,
reason: `Skipped due to '${directive.value.value}' directive.`,
loc: directive.loc ?? null,
});
}
return null;
}
if (!pass.opts.noEmit) {
if (!pass.opts.noEmit && !hasCriticalError) {
return compiledFn;
}
return null;
@@ -512,16 +489,6 @@ export function compileProgram(
});
}
/**
* Do not modify source if there is a module scope level opt out directive.
*/
const moduleScopeOptOutDirectives = findDirectiveDisablingMemoization(
program.node.directives,
);
if (moduleScopeOptOutDirectives.length > 0) {
return;
}
if (pass.opts.gating != null) {
const error = checkFunctionReferencedBeforeDeclarationAtTopLevel(
program,
@@ -535,9 +502,6 @@ export function compileProgram(
}
}
const hasLoweredContextAccess = compiledFns.some(
c => c.compiledFn.hasLoweredContextAccess,
);
const externalFunctions: Array<ExternalFunction> = [];
let gating: null | ExternalFunction = null;
try {
@@ -547,29 +511,38 @@ export function compileProgram(
externalFunctions.push(gating);
}
const lowerContextAccess = environment.lowerContextAccess;
if (lowerContextAccess && hasLoweredContextAccess) {
externalFunctions.push(lowerContextAccess);
}
const enableEmitInstrumentForget = environment.enableEmitInstrumentForget;
const enableEmitInstrumentForget =
pass.opts.environment?.enableEmitInstrumentForget;
if (enableEmitInstrumentForget != null) {
externalFunctions.push(enableEmitInstrumentForget.fn);
externalFunctions.push(
tryParseExternalFunction(enableEmitInstrumentForget.fn),
);
if (enableEmitInstrumentForget.gating != null) {
externalFunctions.push(enableEmitInstrumentForget.gating);
externalFunctions.push(
tryParseExternalFunction(enableEmitInstrumentForget.gating),
);
}
}
if (environment.enableEmitFreeze != null) {
externalFunctions.push(environment.enableEmitFreeze);
if (pass.opts.environment?.enableEmitFreeze != null) {
const enableEmitFreeze = tryParseExternalFunction(
pass.opts.environment.enableEmitFreeze,
);
externalFunctions.push(enableEmitFreeze);
}
if (environment.enableEmitHookGuards != null) {
externalFunctions.push(environment.enableEmitHookGuards);
if (pass.opts.environment?.enableEmitHookGuards != null) {
const enableEmitHookGuards = tryParseExternalFunction(
pass.opts.environment.enableEmitHookGuards,
);
externalFunctions.push(enableEmitHookGuards);
}
if (environment.enableChangeDetectionForDebugging != null) {
externalFunctions.push(environment.enableChangeDetectionForDebugging);
if (pass.opts.environment?.enableChangeDetectionForDebugging != null) {
const enableChangeDetectionForDebugging = tryParseExternalFunction(
pass.opts.environment.enableChangeDetectionForDebugging,
);
externalFunctions.push(enableChangeDetectionForDebugging);
}
} catch (err) {
handleError(err, pass, null);
@@ -604,7 +577,7 @@ export function compileProgram(
if (needsMemoCacheFunctionImport) {
updateMemoCacheFunctionImport(
program,
getReactCompilerRuntimeModule(pass.opts),
moduleName,
useMemoCacheIdentifier.name,
);
}
@@ -612,54 +585,34 @@ export function compileProgram(
}
}
function shouldSkipCompilation(
program: NodePath<t.Program>,
pass: CompilerPass,
): boolean {
if (pass.opts.sources) {
if (pass.filename === null) {
const error = new CompilerError();
error.pushErrorDetail(
new CompilerErrorDetail({
reason: `Expected a filename but found none.`,
description:
"When the 'sources' config options is specified, the React compiler will only compile files with a name",
severity: ErrorSeverity.InvalidConfig,
loc: null,
}),
);
handleError(error, pass, null);
return true;
}
if (!isFilePartOfSources(pass.opts.sources, pass.filename)) {
return true;
}
}
if (
hasMemoCacheFunctionImport(
program,
getReactCompilerRuntimeModule(pass.opts),
)
) {
return true;
}
return false;
}
function getReactFunctionType(
fn: BabelFn,
pass: CompilerPass,
/**
* TODO(mofeiZ): remove once we validate PluginOptions with Zod
*/
environment: EnvironmentConfig,
): ReactFunctionType | null {
const hookPattern = environment.hookPattern;
const hookPattern = pass.opts.environment?.hookPattern ?? null;
if (fn.node.body.type === 'BlockStatement') {
if (findDirectiveEnablingMemoization(fn.node.body.directives).length > 0)
// Opt-outs disable compilation regardless of mode
const useNoForget = findDirectiveDisablingMemoization(
fn.node.body.directives,
pass.opts,
);
if (useNoForget != null) {
pass.opts.logger?.logEvent(pass.filename, {
kind: 'CompileError',
fnLoc: fn.node.body.loc ?? null,
detail: {
severity: ErrorSeverity.Todo,
reason: 'Skipped due to "use no forget" directive.',
loc: useNoForget.loc ?? null,
suggestions: null,
},
});
return null;
}
// Otherwise opt-ins enable compilation regardless of mode
if (findDirectiveEnablingMemoization(fn.node.body.directives) != null) {
return getComponentOrHookLike(fn, hookPattern) ?? 'Other';
}
}
// Component and hook declarations are known components/hooks
@@ -1129,31 +1082,3 @@ function checkFunctionReferencedBeforeDeclarationAtTopLevel(
return errors.details.length > 0 ? errors : null;
}
type ReactCompilerRuntimeModule =
| 'react/compiler-runtime' // from react namespace
| 'react-compiler-runtime'; // npm package
function getReactCompilerRuntimeModule(
opts: PluginOptions,
): ReactCompilerRuntimeModule {
let moduleName: ReactCompilerRuntimeModule | null = null;
switch (opts.target) {
case '17':
case '18': {
moduleName = 'react-compiler-runtime';
break;
}
case '19': {
moduleName = 'react/compiler-runtime';
break;
}
default:
CompilerError.invariant(moduleName != null, {
reason: 'Expected target to already be validated',
description: null,
loc: null,
suggestions: null,
});
}
return moduleName;
}

View File

@@ -14,7 +14,6 @@ import {
ErrorSeverity,
} from '../CompilerError';
import {assertExhaustive} from '../Utils/utils';
import {GeneratedSource} from '../HIR';
/**
* Captures the start and end range of a pair of eslint-disable ... eslint-enable comments. In the
@@ -149,11 +148,10 @@ export function findProgramSuppressions(
export function suppressionsToCompilerError(
suppressionRanges: Array<SuppressionRange>,
): CompilerError {
CompilerError.invariant(suppressionRanges.length !== 0, {
reason: `Expected at least suppression comment source range`,
loc: GeneratedSource,
});
): CompilerError | null {
if (suppressionRanges.length === 0) {
return null;
}
const error = new CompilerError();
for (const suppressionRange of suppressionRanges) {
if (

View File

@@ -29,9 +29,9 @@ export function assertConsistentIdentifiers(fn: HIRFunction): void {
const assignments: Set<IdentifierId> = new Set();
for (const [, block] of fn.body.blocks) {
for (const phi of block.phis) {
validate(identifiers, phi.place.identifier);
validate(identifiers, phi.id);
for (const [, operand] of phi.operands) {
validate(identifiers, operand.identifier);
validate(identifiers, operand);
}
}
for (const instr of block.instructions) {

View File

@@ -20,9 +20,8 @@ import {
export function assertValidMutableRanges(fn: HIRFunction): void {
for (const [, block] of fn.body.blocks) {
for (const phi of block.phis) {
visitIdentifier(phi.place.identifier);
for (const [, operand] of phi.operands) {
visitIdentifier(operand.identifier);
visitIdentifier(operand);
}
}
for (const instr of block.instructions) {

View File

@@ -215,8 +215,7 @@ export function lower(
id,
params,
fnType: parent == null ? env.fnType : 'Other',
returnTypeAnnotation: null, // TODO: extract the actual return type node if present
returnType: makeType(),
returnType: null, // TODO: extract the actual return type node if present
body: builder.build(),
context,
generator: func.node.generator === true,
@@ -420,19 +419,7 @@ function lowerStatement(
// Already hoisted
continue;
}
let kind:
| InstructionKind.Let
| InstructionKind.HoistedConst
| InstructionKind.HoistedLet
| InstructionKind.HoistedFunction;
if (binding.kind === 'const' || binding.kind === 'var') {
kind = InstructionKind.HoistedConst;
} else if (binding.kind === 'let') {
kind = InstructionKind.HoistedLet;
} else if (binding.path.isFunctionDeclaration()) {
kind = InstructionKind.HoistedFunction;
} else if (!binding.path.isVariableDeclarator()) {
if (!binding.path.isVariableDeclarator()) {
builder.errors.push({
severity: ErrorSeverity.Todo,
reason: 'Unsupported declaration type for hoisting',
@@ -441,7 +428,19 @@ function lowerStatement(
loc: id.parentPath.node.loc ?? GeneratedSource,
});
continue;
} else {
} else if (!binding.path.get('id').isIdentifier()) {
builder.errors.push({
severity: ErrorSeverity.Todo,
reason: 'Unsupported variable declaration type for hoisting',
description: `variable "${
binding.identifier.name
}" declared with ${binding.path.get('id').type}`,
suggestions: null,
loc: id.parentPath.node.loc ?? GeneratedSource,
});
continue;
} else if (binding.kind !== 'const' && binding.kind !== 'var') {
// Avoid double errors on var declarations, which we do not plan to support anyways
builder.errors.push({
severity: ErrorSeverity.Todo,
reason: 'Handle non-const declarations for hoisting',
@@ -451,7 +450,6 @@ function lowerStatement(
});
continue;
}
const identifier = builder.resolveIdentifier(id);
CompilerError.invariant(identifier.kind === 'Identifier', {
reason:
@@ -468,7 +466,7 @@ function lowerStatement(
lowerValueToTemporary(builder, {
kind: 'DeclareContext',
lvalue: {
kind,
kind: InstructionKind.HoistedConst,
place,
},
loc: id.node.loc ?? GeneratedSource,
@@ -609,7 +607,6 @@ function lowerStatement(
),
consequent: bodyBlock,
alternate: continuationBlock.id,
fallthrough: continuationBlock.id,
id: makeInstructionId(0),
loc: stmt.node.loc ?? GeneratedSource,
},
@@ -659,13 +656,16 @@ function lowerStatement(
},
conditionalBlock,
);
/*
* The conditional block is empty and exists solely as conditional for
* (re)entering or exiting the loop
*/
const test = lowerExpressionToTemporary(builder, stmt.get('test'));
const terminal: BranchTerminal = {
kind: 'branch',
test,
consequent: loopBlock,
alternate: continuationBlock.id,
fallthrough: conditionalBlock.id,
id: makeInstructionId(0),
loc: stmt.node.loc ?? GeneratedSource,
};
@@ -975,7 +975,6 @@ function lowerStatement(
test,
consequent: loopBlock,
alternate: continuationBlock.id,
fallthrough: conditionalBlock.id,
id: makeInstructionId(0),
loc,
};
@@ -1001,7 +1000,7 @@ function lowerStatement(
lowerAssignment(
builder,
stmt.node.loc ?? GeneratedSource,
InstructionKind.Function,
InstructionKind.Let,
id,
fn,
'Assignment',
@@ -1119,7 +1118,6 @@ function lowerStatement(
consequent: loopBlock,
alternate: continuationBlock.id,
loc: stmt.node.loc ?? GeneratedSource,
fallthrough: continuationBlock.id,
},
continuationBlock,
);
@@ -1205,7 +1203,6 @@ function lowerStatement(
test,
consequent: loopBlock,
alternate: continuationBlock.id,
fallthrough: continuationBlock.id,
loc: stmt.node.loc ?? GeneratedSource,
},
continuationBlock,
@@ -1803,7 +1800,6 @@ function lowerExpression(
test: {...testPlace},
consequent: consequentBlock,
alternate: alternateBlock,
fallthrough: continuationBlock.id,
id: makeInstructionId(0),
loc: exprLoc,
},
@@ -1882,7 +1878,6 @@ function lowerExpression(
test: {...leftPlace},
consequent,
alternate,
fallthrough: continuationBlock.id,
id: makeInstructionId(0),
loc: exprLoc,
},
@@ -2391,57 +2386,6 @@ function lowerExpression(
case 'UpdateExpression': {
let expr = exprPath as NodePath<t.UpdateExpression>;
const argument = expr.get('argument');
if (argument.isMemberExpression()) {
const binaryOperator = expr.node.operator === '++' ? '+' : '-';
const leftExpr = argument as NodePath<t.MemberExpression>;
const {object, property, value} = lowerMemberExpression(
builder,
leftExpr,
);
// Store the previous value to a temporary
const previousValuePlace = lowerValueToTemporary(builder, value);
// Store the new value to a temporary
const updatedValue = lowerValueToTemporary(builder, {
kind: 'BinaryExpression',
operator: binaryOperator,
left: {...previousValuePlace},
right: lowerValueToTemporary(builder, {
kind: 'Primitive',
value: 1,
loc: GeneratedSource,
}),
loc: leftExpr.node.loc ?? GeneratedSource,
});
// Save the result back to the property
let newValuePlace;
if (typeof property === 'string') {
newValuePlace = lowerValueToTemporary(builder, {
kind: 'PropertyStore',
object: {...object},
property,
value: {...updatedValue},
loc: leftExpr.node.loc ?? GeneratedSource,
});
} else {
newValuePlace = lowerValueToTemporary(builder, {
kind: 'ComputedStore',
object: {...object},
property: {...property},
value: {...updatedValue},
loc: leftExpr.node.loc ?? GeneratedSource,
});
}
return {
kind: 'LoadLocal',
place: expr.node.prefix
? {...newValuePlace}
: {...previousValuePlace},
loc: exprLoc,
};
}
if (!argument.isIdentifier()) {
builder.errors.push({
reason: `(BuildHIR::lowerExpression) Handle UpdateExpression with ${argument.type} argument`,
@@ -2616,7 +2560,6 @@ function lowerOptionalMemberExpression(
test: {...object},
consequent: consequent.id,
alternate,
fallthrough: continuationBlock.id,
id: makeInstructionId(0),
loc,
};
@@ -2756,7 +2699,6 @@ function lowerOptionalCallExpression(
test: {...testPlace},
consequent: consequent.id,
alternate,
fallthrough: continuationBlock.id,
id: makeInstructionId(0),
loc,
};
@@ -2895,21 +2837,6 @@ function isReorderableExpression(
allowLocalIdentifiers,
);
}
case 'LogicalExpression': {
const logical = expr as NodePath<t.LogicalExpression>;
return (
isReorderableExpression(
builder,
logical.get('left'),
allowLocalIdentifiers,
) &&
isReorderableExpression(
builder,
logical.get('right'),
allowLocalIdentifiers,
)
);
}
case 'ConditionalExpression': {
const conditional = expr as NodePath<t.ConditionalExpression>;
return (
@@ -3343,7 +3270,7 @@ function lowerFunctionToValue(
return {
kind: 'FunctionExpression',
name,
type: expr.node.type,
expr: expr.node,
loc: exprLoc,
loweredFunc,
};
@@ -4032,7 +3959,6 @@ function lowerAssignment(
test: {...test},
consequent,
alternate,
fallthrough: continuationBlock.id,
id: makeInstructionId(0),
loc,
},

View File

@@ -14,7 +14,6 @@ import {
ScopeId,
} from './HIR';
import {
fixScopeAndIdentifierRanges,
markInstructionIds,
markPredecessors,
reversePostorderBlocks,
@@ -177,7 +176,20 @@ export function buildReactiveScopeTerminalsHIR(fn: HIRFunction): void {
* Step 5:
* Fix scope and identifier ranges to account for renumbered instructions
*/
fixScopeAndIdentifierRanges(fn.body);
for (const [, block] of fn.body.blocks) {
const terminal = block.terminal;
if (terminal.kind === 'scope' || terminal.kind === 'pruned-scope') {
/*
* Scope ranges should always align to start at the 'scope' terminal
* and end at the first instruction of the fallthrough block
*/
const fallthroughBlock = fn.body.blocks.get(terminal.fallthrough)!;
const firstId =
fallthroughBlock.instructions[0]?.id ?? fallthroughBlock.terminal.id;
terminal.scope.range.start = terminal.id;
terminal.scope.range.end = firstId;
}
}
}
type TerminalRewriteInfo =

View File

@@ -1,608 +0,0 @@
import {CompilerError} from '../CompilerError';
import {inRange} from '../ReactiveScopes/InferReactiveScopeVariables';
import {
Set_equal,
Set_filter,
Set_intersect,
Set_union,
getOrInsertDefault,
} from '../Utils/utils';
import {collectOptionalChainSidemap} from './CollectOptionalChainDependencies';
import {
BasicBlock,
BlockId,
DependencyPathEntry,
GeneratedSource,
HIRFunction,
Identifier,
IdentifierId,
InstructionId,
InstructionValue,
ReactiveScopeDependency,
ScopeId,
} from './HIR';
import {collectTemporariesSidemap} from './PropagateScopeDependenciesHIR';
/**
* Helper function for `PropagateScopeDependencies`. Uses control flow graph
* analysis to determine which `Identifier`s can be assumed to be non-null
* objects, on a per-block basis.
*
* Here is an example:
* ```js
* function useFoo(x, y, z) {
* // NOT safe to hoist PropertyLoads here
* if (...) {
* // safe to hoist loads from x
* read(x.a);
* return;
* }
* // safe to hoist loads from y, z
* read(y.b);
* if (...) {
* // safe to hoist loads from y, z
* read(z.a);
* } else {
* // safe to hoist loads from y, z
* read(z.b);
* }
* // safe to hoist loads from y, z
* return;
* }
* ```
*
* Note that we currently do NOT account for mutable / declaration range when
* doing the CFG-based traversal, producing results that are technically
* incorrect but filtered by PropagateScopeDeps (which only takes dependencies
* on constructed value -- i.e. a scope's dependencies must have mutable ranges
* ending earlier than the scope start).
*
* Take this example, this function will infer x.foo.bar as non-nullable for
* bb0, via the intersection of bb1 & bb2 which in turn comes from bb3. This is
* technically incorrect bb0 is before / during x's mutable range.
* ```
* bb0:
* const x = ...;
* if cond then bb1 else bb2
* bb1:
* ...
* goto bb3
* bb2:
* ...
* goto bb3:
* bb3:
* x.foo.bar
* ```
*
* @param fn
* @param temporaries sidemap of identifier -> baseObject.a.b paths. Does not
* contain optional chains.
* @param hoistableFromOptionals sidemap of optionalBlock -> baseObject?.a
* optional paths for which it's safe to evaluate non-optional loads (see
* CollectOptionalChainDependencies).
* @returns
*/
export function collectHoistablePropertyLoads(
fn: HIRFunction,
temporaries: ReadonlyMap<IdentifierId, ReactiveScopeDependency>,
hoistableFromOptionals: ReadonlyMap<BlockId, ReactiveScopeDependency>,
nestedFnImmutableContext: ReadonlySet<IdentifierId> | null,
): ReadonlyMap<BlockId, BlockInfo> {
const registry = new PropertyPathRegistry();
const functionExpressionLoads = collectFunctionExpressionFakeLoads(fn);
const actuallyEvaluatedTemporaries = new Map(
[...temporaries].filter(([id]) => !functionExpressionLoads.has(id)),
);
/**
* Due to current limitations of mutable range inference, there are edge cases in
* which we infer known-immutable values (e.g. props or hook params) to have a
* mutable range and scope.
* (see `destructure-array-declaration-to-context-var` fixture)
* We track known immutable identifiers to reduce regressions (as PropagateScopeDeps
* is being rewritten to HIR).
*/
const knownImmutableIdentifiers = new Set<IdentifierId>();
if (fn.fnType === 'Component' || fn.fnType === 'Hook') {
for (const p of fn.params) {
if (p.kind === 'Identifier') {
knownImmutableIdentifiers.add(p.identifier.id);
}
}
}
const nodes = collectNonNullsInBlocks(fn, {
temporaries: actuallyEvaluatedTemporaries,
knownImmutableIdentifiers,
hoistableFromOptionals,
registry,
nestedFnImmutableContext,
});
propagateNonNull(fn, nodes, registry);
return nodes;
}
export function keyByScopeId<T>(
fn: HIRFunction,
source: ReadonlyMap<BlockId, T>,
): ReadonlyMap<ScopeId, T> {
const keyedByScopeId = new Map<ScopeId, T>();
for (const [_, block] of fn.body.blocks) {
if (block.terminal.kind === 'scope') {
keyedByScopeId.set(
block.terminal.scope.id,
source.get(block.terminal.block)!,
);
}
}
return keyedByScopeId;
}
export type BlockInfo = {
block: BasicBlock;
assumedNonNullObjects: ReadonlySet<PropertyPathNode>;
};
/**
* PropertyLoadRegistry data structure to dedupe property loads (e.g. a.b.c)
* and make computing sets intersections simpler.
*/
type RootNode = {
properties: Map<string, PropertyPathNode>;
optionalProperties: Map<string, PropertyPathNode>;
parent: null;
// Recorded to make later computations simpler
fullPath: ReactiveScopeDependency;
hasOptional: boolean;
root: IdentifierId;
};
type PropertyPathNode =
| {
properties: Map<string, PropertyPathNode>;
optionalProperties: Map<string, PropertyPathNode>;
parent: PropertyPathNode;
fullPath: ReactiveScopeDependency;
hasOptional: boolean;
}
| RootNode;
class PropertyPathRegistry {
roots: Map<IdentifierId, RootNode> = new Map();
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).
*/
let rootNode = this.roots.get(identifier.id);
if (rootNode === undefined) {
rootNode = {
root: identifier.id,
properties: new Map(),
optionalProperties: new Map(),
fullPath: {
identifier,
path: [],
},
hasOptional: false,
parent: null,
};
this.roots.set(identifier.id, rootNode);
}
return rootNode;
}
static getOrCreatePropertyEntry(
parent: PropertyPathNode,
entry: DependencyPathEntry,
): PropertyPathNode {
const map = entry.optional ? parent.optionalProperties : parent.properties;
let child = map.get(entry.property);
if (child == null) {
child = {
properties: new Map(),
optionalProperties: new Map(),
parent: parent,
fullPath: {
identifier: parent.fullPath.identifier,
path: parent.fullPath.path.concat(entry),
},
hasOptional: parent.hasOptional || entry.optional,
};
map.set(entry.property, child);
}
return child;
}
getOrCreateProperty(n: ReactiveScopeDependency): PropertyPathNode {
/**
* We add ReactiveScopeDependencies according to instruction ordering,
* 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);
if (n.path.length === 0) {
return currNode;
}
for (let i = 0; i < n.path.length - 1; i++) {
currNode = PropertyPathRegistry.getOrCreatePropertyEntry(
currNode,
n.path[i],
);
}
return PropertyPathRegistry.getOrCreatePropertyEntry(
currNode,
n.path.at(-1)!,
);
}
}
function getMaybeNonNullInInstruction(
instr: InstructionValue,
context: CollectNonNullsInBlocksContext,
): PropertyPathNode | null {
let path = null;
if (instr.kind === 'PropertyLoad') {
path = context.temporaries.get(instr.object.identifier.id) ?? {
identifier: instr.object.identifier,
path: [],
};
} else if (instr.kind === 'Destructure') {
path = context.temporaries.get(instr.value.identifier.id) ?? null;
} else if (instr.kind === 'ComputedLoad') {
path = context.temporaries.get(instr.object.identifier.id) ?? null;
}
return path != null ? context.registry.getOrCreateProperty(path) : null;
}
function isImmutableAtInstr(
identifier: Identifier,
instr: InstructionId,
context: CollectNonNullsInBlocksContext,
): boolean {
if (context.nestedFnImmutableContext != null) {
/**
* Comparing instructions ids across inner-outer function bodies is not valid, as they are numbered
*/
return context.nestedFnImmutableContext.has(identifier.id);
} else {
/**
* Since this runs *after* buildReactiveScopeTerminals, identifier mutable ranges
* are not valid with respect to current instruction id numbering.
* We use attached reactive scope ranges as a proxy for mutable range, but this
* is an overestimate as (1) scope ranges merge and align to form valid program
* blocks and (2) passes like MemoizeFbtAndMacroOperands may assign scopes to
* non-mutable identifiers.
*
* See comment in exported function for why we track known immutable identifiers.
*/
const mutableAtInstr =
identifier.mutableRange.end > identifier.mutableRange.start + 1 &&
identifier.scope != null &&
inRange(
{
id: instr,
},
identifier.scope.range,
);
return (
!mutableAtInstr || context.knownImmutableIdentifiers.has(identifier.id)
);
}
}
type CollectNonNullsInBlocksContext = {
temporaries: ReadonlyMap<IdentifierId, ReactiveScopeDependency>;
knownImmutableIdentifiers: ReadonlySet<IdentifierId>;
hoistableFromOptionals: ReadonlyMap<BlockId, ReactiveScopeDependency>;
registry: PropertyPathRegistry;
/**
* (For nested / inner function declarations)
* Context variables (i.e. captured from an outer scope) that are immutable.
* Note that this technically could be merged into `knownImmutableIdentifiers`,
* but are currently kept separate for readability.
*/
nestedFnImmutableContext: ReadonlySet<IdentifierId> | null;
};
function collectNonNullsInBlocks(
fn: HIRFunction,
context: CollectNonNullsInBlocksContext,
): ReadonlyMap<BlockId, BlockInfo> {
/**
* Known non-null objects such as functional component props can be safely
* read from any block.
*/
const knownNonNullIdentifiers = new Set<PropertyPathNode>();
if (
fn.fnType === 'Component' &&
fn.params.length > 0 &&
fn.params[0].kind === 'Identifier'
) {
const identifier = fn.params[0].identifier;
knownNonNullIdentifiers.add(
context.registry.getOrCreateIdentifier(identifier),
);
}
const nodes = new Map<BlockId, BlockInfo>();
for (const [_, block] of fn.body.blocks) {
const assumedNonNullObjects = new Set<PropertyPathNode>(
knownNonNullIdentifiers,
);
const maybeOptionalChain = context.hoistableFromOptionals.get(block.id);
if (maybeOptionalChain != null) {
assumedNonNullObjects.add(
context.registry.getOrCreateProperty(maybeOptionalChain),
);
}
for (const instr of block.instructions) {
const maybeNonNull = getMaybeNonNullInInstruction(instr.value, context);
if (
maybeNonNull != null &&
isImmutableAtInstr(maybeNonNull.fullPath.identifier, instr.id, context)
) {
assumedNonNullObjects.add(maybeNonNull);
}
if (
instr.value.kind === 'FunctionExpression' &&
!fn.env.config.enableTreatFunctionDepsAsConditional
) {
const innerFn = instr.value.loweredFunc;
const innerTemporaries = collectTemporariesSidemap(
innerFn.func,
new Set(),
);
const innerOptionals = collectOptionalChainSidemap(innerFn.func);
const innerHoistableMap = collectHoistablePropertyLoads(
innerFn.func,
innerTemporaries,
innerOptionals.hoistableObjects,
context.nestedFnImmutableContext ??
new Set(
innerFn.func.context
.filter(place =>
isImmutableAtInstr(place.identifier, instr.id, context),
)
.map(place => place.identifier.id),
),
);
const innerHoistables = assertNonNull(
innerHoistableMap.get(innerFn.func.body.entry),
);
for (const entry of innerHoistables.assumedNonNullObjects) {
assumedNonNullObjects.add(entry);
}
}
}
nodes.set(block.id, {
block,
assumedNonNullObjects,
});
}
return nodes;
}
function propagateNonNull(
fn: HIRFunction,
nodes: ReadonlyMap<BlockId, BlockInfo>,
registry: PropertyPathRegistry,
): void {
const blockSuccessors = new Map<BlockId, Set<BlockId>>();
const terminalPreds = new Set<BlockId>();
for (const [blockId, block] of fn.body.blocks) {
for (const pred of block.preds) {
getOrInsertDefault(blockSuccessors, pred, new Set()).add(blockId);
}
if (block.terminal.kind === 'throw' || block.terminal.kind === 'return') {
terminalPreds.add(blockId);
}
}
/**
* In the context of a control flow graph, the identifiers that a block
* can assume are non-null can be calculated from the following:
* X = Union(Intersect(X_neighbors), X)
*/
function recursivelyPropagateNonNull(
nodeId: BlockId,
direction: 'forward' | 'backward',
traversalState: Map<BlockId, 'active' | 'done'>,
): boolean {
/**
* Avoid re-visiting computed or currently active nodes, which can
* occur when the control flow graph has backedges.
*/
if (traversalState.has(nodeId)) {
return false;
}
traversalState.set(nodeId, 'active');
const node = nodes.get(nodeId);
if (node == null) {
CompilerError.invariant(false, {
reason: `Bad node ${nodeId}, kind: ${direction}`,
loc: GeneratedSource,
});
}
const neighbors = Array.from(
direction === 'backward'
? (blockSuccessors.get(nodeId) ?? [])
: node.block.preds,
);
let changed = false;
for (const pred of neighbors) {
if (!traversalState.has(pred)) {
const neighborChanged = recursivelyPropagateNonNull(
pred,
direction,
traversalState,
);
changed ||= neighborChanged;
}
}
/**
* Note that a predecessor / successor can only be active (status != 'done')
* if it is a self-loop or other transitive cycle. Active neighbors can be
* filtered out (i.e. not included in the intersection)
* Example: self loop.
* X = Union(Intersect(X, ...X_other_neighbors), X)
*
* Example: transitive cycle through node Y, for some Y that is a
* predecessor / successor of X.
* X = Union(
* Intersect(
* Union(Intersect(X, ...Y_other_neighbors), Y),
* ...X_neighbors
* ),
* X
* )
*
* Non-active neighbors with no recorded results can occur due to backedges.
* it's not safe to assume they can be filtered out (e.g. not included in
* the intersection)
*/
const neighborAccesses = Set_intersect(
Array.from(neighbors)
.filter(n => traversalState.get(n) === 'done')
.map(n => assertNonNull(nodes.get(n)).assumedNonNullObjects),
);
const prevObjects = assertNonNull(nodes.get(nodeId)).assumedNonNullObjects;
const mergedObjects = Set_union(prevObjects, neighborAccesses);
reduceMaybeOptionalChains(mergedObjects, registry);
assertNonNull(nodes.get(nodeId)).assumedNonNullObjects = mergedObjects;
traversalState.set(nodeId, 'done');
/**
* Note that it's not sufficient to compare set sizes since
* reduceMaybeOptionalChains may replace optional-chain loads with
* unconditional loads. This could in turn change `assumedNonNullObjects` of
* downstream blocks and backedges.
*/
changed ||= !Set_equal(prevObjects, mergedObjects);
return changed;
}
const traversalState = new Map<BlockId, 'done' | 'active'>();
const reversedBlocks = [...fn.body.blocks];
reversedBlocks.reverse();
let changed;
let i = 0;
do {
CompilerError.invariant(i++ < 100, {
reason:
'[CollectHoistablePropertyLoads] fixed point iteration did not terminate after 100 loops',
loc: GeneratedSource,
});
changed = false;
for (const [blockId] of fn.body.blocks) {
const forwardChanged = recursivelyPropagateNonNull(
blockId,
'forward',
traversalState,
);
changed ||= forwardChanged;
}
traversalState.clear();
for (const [blockId] of reversedBlocks) {
const backwardChanged = recursivelyPropagateNonNull(
blockId,
'backward',
traversalState,
);
changed ||= backwardChanged;
}
traversalState.clear();
} while (changed);
}
export function assertNonNull<T extends NonNullable<U>, U>(
value: T | null | undefined,
source?: string,
): T {
CompilerError.invariant(value != null, {
reason: 'Unexpected null',
description: source != null ? `(from ${source})` : null,
loc: GeneratedSource,
});
return value;
}
/**
* Any two optional chains with different operations . vs ?. but the same set of
* property strings paths de-duplicates.
*
* Intuitively: given <base>?.b, we know <base> to be either hoistable or not.
* If unconditional reads from <base> are hoistable, we can replace all
* <base>?.PROPERTY_STRING subpaths with <base>.PROPERTY_STRING
*/
function reduceMaybeOptionalChains(
nodes: Set<PropertyPathNode>,
registry: PropertyPathRegistry,
): void {
let optionalChainNodes = Set_filter(nodes, n => n.hasOptional);
if (optionalChainNodes.size === 0) {
return;
}
let changed: boolean;
do {
changed = false;
for (const original of optionalChainNodes) {
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
const nextEntry: DependencyPathEntry =
entry.optional && nodes.has(currNode)
? {property: entry.property, optional: false}
: entry;
currNode = PropertyPathRegistry.getOrCreatePropertyEntry(
currNode,
nextEntry,
);
}
if (currNode !== original) {
changed = true;
optionalChainNodes.delete(original);
optionalChainNodes.add(currNode);
nodes.delete(original);
nodes.add(currNode);
}
}
} while (changed);
}
function collectFunctionExpressionFakeLoads(
fn: HIRFunction,
): Set<IdentifierId> {
const sources = new Map<IdentifierId, IdentifierId>();
const functionExpressionReferences = new Set<IdentifierId>();
for (const [_, block] of fn.body.blocks) {
for (const {lvalue, value} of block.instructions) {
if (value.kind === 'FunctionExpression') {
for (const reference of value.loweredFunc.dependencies) {
let curr: IdentifierId | undefined = reference.identifier.id;
while (curr != null) {
functionExpressionReferences.add(curr);
curr = sources.get(curr);
}
}
} else if (value.kind === 'PropertyLoad') {
sources.set(lvalue.identifier.id, value.object.identifier.id);
}
}
}
return functionExpressionReferences;
}

View File

@@ -1,382 +0,0 @@
import {CompilerError} from '..';
import {assertNonNull} from './CollectHoistablePropertyLoads';
import {
BlockId,
BasicBlock,
InstructionId,
IdentifierId,
ReactiveScopeDependency,
BranchTerminal,
TInstruction,
PropertyLoad,
StoreLocal,
GotoVariant,
TBasicBlock,
OptionalTerminal,
HIRFunction,
DependencyPathEntry,
} from './HIR';
import {printIdentifier} from './PrintHIR';
export function collectOptionalChainSidemap(
fn: HIRFunction,
): OptionalChainSidemap {
const context: OptionalTraversalContext = {
blocks: fn.body.blocks,
seenOptionals: new Set(),
processedInstrsInOptional: new Set(),
temporariesReadInOptional: new Map(),
hoistableObjects: new Map(),
};
for (const [_, block] of fn.body.blocks) {
if (
block.terminal.kind === 'optional' &&
!context.seenOptionals.has(block.id)
) {
traverseOptionalBlock(
block as TBasicBlock<OptionalTerminal>,
context,
null,
);
}
}
return {
temporariesReadInOptional: context.temporariesReadInOptional,
processedInstrsInOptional: context.processedInstrsInOptional,
hoistableObjects: context.hoistableObjects,
};
}
export type OptionalChainSidemap = {
/**
* Stores the correct property mapping (e.g. `a?.b` instead of `a.b`) for
* dependency calculation. Note that we currently do not store anything on
* outer phi nodes.
*/
temporariesReadInOptional: ReadonlyMap<IdentifierId, ReactiveScopeDependency>;
/**
* Records instructions (PropertyLoads, StoreLocals, and test terminals)
* processed in this pass. When extracting dependencies in
* PropagateScopeDependencies, these instructions are skipped.
*
* E.g. given a?.b
* ```
* bb0
* $0 = LoadLocal 'a'
* test $0 then=bb1 <- Avoid adding dependencies from these instructions, as
* bb1 the sidemap produced by readOptionalBlock already maps
* $1 = PropertyLoad $0.'b' <- $1 and $2 back to a?.b. Instead, we want to add a?.b
* StoreLocal $2 = $1 <- as a dependency when $1 or $2 are later used in either
* - an unhoistable expression within an outer optional
* block e.g. MethodCall
* - a phi node (if the entire optional value is hoistable)
* ```
*
* Note that mapping blockIds to their evaluated dependency path does not
* work, since values produced by inner optional chains may be referenced in
* outer ones
* ```
* a?.b.c()
* ->
* bb0
* $0 = LoadLocal 'a'
* test $0 then=bb1
* bb1
* $1 = PropertyLoad $0.'b'
* StoreLocal $2 = $1
* goto bb2
* bb2
* test $2 then=bb3
* bb3:
* $3 = PropertyLoad $2.'c'
* StoreLocal $4 = $3
* goto bb4
* bb4
* test $4 then=bb5
* bb5:
* $5 = MethodCall $2.$4() <--- here, we want to take a dep on $2 and $4!
* ```
*/
processedInstrsInOptional: ReadonlySet<InstructionId>;
/**
* Records optional chains for which we can safely evaluate non-optional
* PropertyLoads. e.g. given `a?.b.c`, we can evaluate any load from `a?.b` at
* the optional terminal in bb1.
* ```js
* bb1:
* ...
* Optional optional=false test=bb2 fallth=...
* bb2:
* Optional optional=true test=bb3 fallth=...
* ...
* ```
*/
hoistableObjects: ReadonlyMap<BlockId, ReactiveScopeDependency>;
};
type OptionalTraversalContext = {
blocks: ReadonlyMap<BlockId, BasicBlock>;
// Track optional blocks to avoid outer calls into nested optionals
seenOptionals: Set<BlockId>;
processedInstrsInOptional: Set<InstructionId>;
temporariesReadInOptional: Map<IdentifierId, ReactiveScopeDependency>;
hoistableObjects: Map<BlockId, ReactiveScopeDependency>;
};
/**
* Match the consequent and alternate blocks of an optional.
* @returns propertyload computed by the consequent block, or null if the
* consequent block is not a simple PropertyLoad.
*/
function matchOptionalTestBlock(
terminal: BranchTerminal,
blocks: ReadonlyMap<BlockId, BasicBlock>,
): {
consequentId: IdentifierId;
property: string;
propertyId: IdentifierId;
storeLocalInstrId: InstructionId;
consequentGoto: BlockId;
} | null {
const consequentBlock = assertNonNull(blocks.get(terminal.consequent));
if (
consequentBlock.instructions.length === 2 &&
consequentBlock.instructions[0].value.kind === 'PropertyLoad' &&
consequentBlock.instructions[1].value.kind === 'StoreLocal'
) {
const propertyLoad: TInstruction<PropertyLoad> = consequentBlock
.instructions[0] as TInstruction<PropertyLoad>;
const storeLocal: StoreLocal = consequentBlock.instructions[1].value;
const storeLocalInstrId = consequentBlock.instructions[1].id;
CompilerError.invariant(
propertyLoad.value.object.identifier.id === terminal.test.identifier.id,
{
reason:
'[OptionalChainDeps] Inconsistent optional chaining property load',
description: `Test=${printIdentifier(terminal.test.identifier)} PropertyLoad base=${printIdentifier(propertyLoad.value.object.identifier)}`,
loc: propertyLoad.loc,
},
);
CompilerError.invariant(
storeLocal.value.identifier.id === propertyLoad.lvalue.identifier.id,
{
reason: '[OptionalChainDeps] Unexpected storeLocal',
loc: propertyLoad.loc,
},
);
if (
consequentBlock.terminal.kind !== 'goto' ||
consequentBlock.terminal.variant !== GotoVariant.Break
) {
return null;
}
const alternate = assertNonNull(blocks.get(terminal.alternate));
CompilerError.invariant(
alternate.instructions.length === 2 &&
alternate.instructions[0].value.kind === 'Primitive' &&
alternate.instructions[1].value.kind === 'StoreLocal',
{
reason: 'Unexpected alternate structure',
loc: terminal.loc,
},
);
return {
consequentId: storeLocal.lvalue.place.identifier.id,
property: propertyLoad.value.property,
propertyId: propertyLoad.lvalue.identifier.id,
storeLocalInstrId,
consequentGoto: consequentBlock.terminal.block,
};
}
return null;
}
/**
* Traverse into the optional block and all transitively referenced blocks to
* collect sidemaps of optional chain dependencies.
*
* @returns the IdentifierId representing the optional block if the block and
* all transitively referenced optional blocks precisely represent a chain of
* property loads. If any part of the optional chain is not hoistable, returns
* null.
*/
function traverseOptionalBlock(
optional: TBasicBlock<OptionalTerminal>,
context: OptionalTraversalContext,
outerAlternate: BlockId | null,
): IdentifierId | null {
context.seenOptionals.add(optional.id);
const maybeTest = context.blocks.get(optional.terminal.test)!;
let test: BranchTerminal;
let baseObject: ReactiveScopeDependency;
if (maybeTest.terminal.kind === 'branch') {
CompilerError.invariant(optional.terminal.optional, {
reason: '[OptionalChainDeps] Expect base case to be always optional',
loc: optional.terminal.loc,
});
/**
* Optional base expressions are currently within value blocks which cannot
* be interrupted by scope boundaries. As such, the only dependencies we can
* hoist out of optional chains are property load chains with no intervening
* instructions.
*
* Ideally, we would be able to flatten base instructions out of optional
* blocks, but this would require changes to HIR.
*
* For now, only match base expressions that are straightforward
* PropertyLoad chains
*/
if (
maybeTest.instructions.length === 0 ||
maybeTest.instructions[0].value.kind !== 'LoadLocal'
) {
return null;
}
const path: Array<DependencyPathEntry> = [];
for (let i = 1; i < maybeTest.instructions.length; i++) {
const instrVal = maybeTest.instructions[i].value;
const prevInstr = maybeTest.instructions[i - 1];
if (
instrVal.kind === 'PropertyLoad' &&
instrVal.object.identifier.id === prevInstr.lvalue.identifier.id
) {
path.push({property: instrVal.property, optional: false});
} else {
return null;
}
}
CompilerError.invariant(
maybeTest.terminal.test.identifier.id ===
maybeTest.instructions.at(-1)!.lvalue.identifier.id,
{
reason: '[OptionalChainDeps] Unexpected test expression',
loc: maybeTest.terminal.loc,
},
);
baseObject = {
identifier: maybeTest.instructions[0].value.place.identifier,
path,
};
test = maybeTest.terminal;
} else if (maybeTest.terminal.kind === 'optional') {
/**
* This is either
* - <inner_optional>?.property (optional=true)
* - <inner_optional>.property (optional=false)
* - <inner_optional> <other operation>
* - a optional base block with a separate nested optional-chain (e.g. a(c?.d)?.d)
*/
const testBlock = context.blocks.get(maybeTest.terminal.fallthrough)!;
if (testBlock!.terminal.kind !== 'branch') {
/**
* Fallthrough of the inner optional should be a block with no
* instructions, terminating with Test($<temporary written to from
* StoreLocal>)
*/
CompilerError.throwTodo({
reason: `Unexpected terminal kind \`${testBlock.terminal.kind}\` for optional fallthrough block`,
loc: maybeTest.terminal.loc,
});
}
/**
* Recurse into inner optional blocks to collect inner optional-chain
* expressions, regardless of whether we can match the outer one to a
* PropertyLoad.
*/
const innerOptional = traverseOptionalBlock(
maybeTest as TBasicBlock<OptionalTerminal>,
context,
testBlock.terminal.alternate,
);
if (innerOptional == null) {
return null;
}
/**
* Check that the inner optional is part of the same optional-chain as the
* outer one. This is not guaranteed, e.g. given a(c?.d)?.d
* ```
* bb0:
* Optional test=bb1
* bb1:
* $0 = LoadLocal a <-- part 1 of the outer optional-chaining base
* Optional test=bb2 fallth=bb5 <-- start of optional chain for c?.d
* bb2:
* ... (optional chain for c?.d)
* ...
* bb5:
* $1 = phi(c.d, undefined) <-- part 2 (continuation) of the outer optional-base
* $2 = Call $0($1)
* Branch $2 ...
* ```
*/
if (testBlock.terminal.test.identifier.id !== innerOptional) {
return null;
}
if (!optional.terminal.optional) {
/**
* If this is an non-optional load participating in an optional chain
* (e.g. loading the `c` property in `a?.b.c`), record that PropertyLoads
* from the inner optional value are hoistable.
*/
context.hoistableObjects.set(
optional.id,
assertNonNull(context.temporariesReadInOptional.get(innerOptional)),
);
}
baseObject = assertNonNull(
context.temporariesReadInOptional.get(innerOptional),
);
test = testBlock.terminal;
} else {
return null;
}
if (test.alternate === outerAlternate) {
CompilerError.invariant(optional.instructions.length === 0, {
reason:
'[OptionalChainDeps] Unexpected instructions an inner optional block. ' +
'This indicates that the compiler may be incorrectly concatenating two unrelated optional chains',
loc: optional.terminal.loc,
});
}
const matchConsequentResult = matchOptionalTestBlock(test, context.blocks);
if (!matchConsequentResult) {
// Optional chain consequent is not hoistable e.g. a?.[computed()]
return null;
}
CompilerError.invariant(
matchConsequentResult.consequentGoto === optional.terminal.fallthrough,
{
reason: '[OptionalChainDeps] Unexpected optional goto-fallthrough',
description: `${matchConsequentResult.consequentGoto} != ${optional.terminal.fallthrough}`,
loc: optional.terminal.loc,
},
);
const load = {
identifier: baseObject.identifier,
path: [
...baseObject.path,
{
property: matchConsequentResult.property,
optional: optional.terminal.optional,
},
],
};
context.processedInstrsInOptional.add(
matchConsequentResult.storeLocalInstrId,
);
context.processedInstrsInOptional.add(test.id);
context.temporariesReadInOptional.set(
matchConsequentResult.consequentId,
load,
);
context.temporariesReadInOptional.set(matchConsequentResult.propertyId, load);
return matchConsequentResult.consequentId;
}

View File

@@ -1,361 +0,0 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import {CompilerError} from '../CompilerError';
import {
DependencyPathEntry,
GeneratedSource,
Identifier,
ReactiveScopeDependency,
} from '../HIR';
import {printIdentifier} from '../HIR/PrintHIR';
import {ReactiveScopePropertyDependency} from '../ReactiveScopes/DeriveMinimalDependencies';
/**
* Simpler fork of DeriveMinimalDependencies, see PropagateScopeDependenciesHIR
* for detailed explanation.
*/
export class ReactiveScopeDependencyTreeHIR {
/**
* Paths from which we can hoist PropertyLoads. If an `identifier`,
* `identifier.path`, or `identifier?.path` is in this map, it is safe to
* evaluate (non-optional) PropertyLoads from.
*/
#hoistableObjects: Map<Identifier, HoistableNode> = new Map();
#deps: Map<Identifier, DependencyNode> = new Map();
/**
* @param hoistableObjects a set of paths from which we can safely evaluate
* PropertyLoads. Note that we expect these to not contain duplicates (e.g.
* both `a?.b` and `a.b`) only because CollectHoistablePropertyLoads merges
* duplicates when traversing the CFG.
*/
constructor(hoistableObjects: Iterable<ReactiveScopeDependency>) {
for (const {path, identifier} of hoistableObjects) {
let currNode = ReactiveScopeDependencyTreeHIR.#getOrCreateRoot(
identifier,
this.#hoistableObjects,
path.length > 0 && path[0].optional ? 'Optional' : 'NonNull',
);
for (let i = 0; i < path.length; i++) {
const prevAccessType = currNode.properties.get(
path[i].property,
)?.accessType;
const accessType =
i + 1 < path.length && path[i + 1].optional ? 'Optional' : 'NonNull';
CompilerError.invariant(
prevAccessType == null || prevAccessType === accessType,
{
reason: 'Conflicting access types',
loc: GeneratedSource,
},
);
let nextNode = currNode.properties.get(path[i].property);
if (nextNode == null) {
nextNode = {
properties: new Map(),
accessType,
};
currNode.properties.set(path[i].property, nextNode);
}
currNode = nextNode;
}
}
}
static #getOrCreateRoot<T extends string>(
identifier: Identifier,
roots: Map<Identifier, TreeNode<T>>,
defaultAccessType: T,
): TreeNode<T> {
// roots can always be accessed unconditionally in JS
let rootNode = roots.get(identifier);
if (rootNode === undefined) {
rootNode = {
properties: new Map(),
accessType: defaultAccessType,
};
roots.set(identifier, rootNode);
}
return rootNode;
}
/**
* Join a dependency with `#hoistableObjects` to record the hoistable
* dependency. This effectively truncates @param dep to its maximal
* safe-to-evaluate subpath
*/
addDependency(dep: ReactiveScopePropertyDependency): void {
const {identifier, path} = dep;
let depCursor = ReactiveScopeDependencyTreeHIR.#getOrCreateRoot(
identifier,
this.#deps,
PropertyAccessType.UnconditionalAccess,
);
/**
* hoistableCursor is null if depCursor is not an object we can hoist
* property reads from otherwise, it represents the same node in the
* hoistable / cfg-informed tree
*/
let hoistableCursor: HoistableNode | undefined =
this.#hoistableObjects.get(identifier);
// All properties read 'on the way' to a dependency are marked as 'access'
for (const entry of path) {
let nextHoistableCursor: HoistableNode | undefined;
let nextDepCursor: DependencyNode;
if (entry.optional) {
/**
* No need to check the access type since we can match both optional or non-optionals
* in the hoistable
* e.g. a?.b<rest> is hoistable if a.b<rest> is hoistable
*/
if (hoistableCursor != null) {
nextHoistableCursor = hoistableCursor?.properties.get(entry.property);
}
let accessType;
if (
hoistableCursor != null &&
hoistableCursor.accessType === 'NonNull'
) {
/**
* For an optional chain dep `a?.b`: if the hoistable tree only
* contains `a`, we can keep either `a?.b` or 'a.b' as a dependency.
* (note that we currently do the latter for perf)
*/
accessType = PropertyAccessType.UnconditionalAccess;
} else {
/**
* Given that it's safe to evaluate `depCursor` and optional load
* never throws, it's also safe to evaluate `depCursor?.entry`
*/
accessType = PropertyAccessType.OptionalAccess;
}
nextDepCursor = makeOrMergeProperty(
depCursor,
entry.property,
accessType,
);
} else if (
hoistableCursor != null &&
hoistableCursor.accessType === 'NonNull'
) {
nextHoistableCursor = hoistableCursor.properties.get(entry.property);
nextDepCursor = makeOrMergeProperty(
depCursor,
entry.property,
PropertyAccessType.UnconditionalAccess,
);
} else {
/**
* Break to truncate the dependency on its first non-optional entry that PropertyLoads are not hoistable from
*/
break;
}
depCursor = nextDepCursor;
hoistableCursor = nextHoistableCursor;
}
// mark the final node as a dependency
depCursor.accessType = merge(
depCursor.accessType,
PropertyAccessType.OptionalDependency,
);
}
deriveMinimalDependencies(): Set<ReactiveScopeDependency> {
const results = new Set<ReactiveScopeDependency>();
for (const [rootId, rootNode] of this.#deps.entries()) {
collectMinimalDependenciesInSubtree(rootNode, rootId, [], results);
}
return results;
}
/*
* Prints dependency tree to string for debugging.
* @param includeAccesses
* @returns string representation of DependencyTree
*/
printDeps(includeAccesses: boolean): string {
let res: Array<Array<string>> = [];
for (const [rootId, rootNode] of this.#deps.entries()) {
const rootResults = printSubtree(rootNode, includeAccesses).map(
result => `${printIdentifier(rootId)}.${result}`,
);
res.push(rootResults);
}
return res.flat().join('\n');
}
static debug<T extends string>(roots: Map<Identifier, TreeNode<T>>): string {
const buf: Array<string> = [`tree() [`];
for (const [rootId, rootNode] of roots) {
buf.push(`${printIdentifier(rootId)} (${rootNode.accessType}):`);
this.#debugImpl(buf, rootNode, 1);
}
buf.push(']');
return buf.length > 2 ? buf.join('\n') : buf.join('');
}
static #debugImpl<T extends string>(
buf: Array<string>,
node: TreeNode<T>,
depth: number = 0,
): void {
for (const [property, childNode] of node.properties) {
buf.push(`${' '.repeat(depth)}.${property} (${childNode.accessType}):`);
this.#debugImpl(buf, childNode, depth + 1);
}
}
}
/*
* Enum representing the access type of single property on a parent object.
* We distinguish on two independent axes:
* Optional / Unconditional:
* - whether this property is an optional load (within an optional chain)
* Access / Dependency:
* - Access: this property is read on the path of a dependency. We do not
* need to track change variables for accessed properties. Tracking accesses
* helps Forget do more granular dependency tracking.
* - Dependency: this property is read as a dependency and we must track changes
* to it for correctness.
* ```javascript
* // props.a is a dependency here and must be tracked
* deps: {props.a, props.a.b} ---> minimalDeps: {props.a}
* // props.a is just an access here and does not need to be tracked
* deps: {props.a.b} ---> minimalDeps: {props.a.b}
* ```
*/
enum PropertyAccessType {
OptionalAccess = 'OptionalAccess',
UnconditionalAccess = 'UnconditionalAccess',
OptionalDependency = 'OptionalDependency',
UnconditionalDependency = 'UnconditionalDependency',
}
function isOptional(access: PropertyAccessType): boolean {
return (
access === PropertyAccessType.OptionalAccess ||
access === PropertyAccessType.OptionalDependency
);
}
function isDependency(access: PropertyAccessType): boolean {
return (
access === PropertyAccessType.OptionalDependency ||
access === PropertyAccessType.UnconditionalDependency
);
}
function merge(
access1: PropertyAccessType,
access2: PropertyAccessType,
): PropertyAccessType {
const resultIsUnconditional = !(isOptional(access1) && isOptional(access2));
const resultIsDependency = isDependency(access1) || isDependency(access2);
/*
* Straightforward merge.
* This can be represented as bitwise OR, but is written out for readability
*
* Observe that `UnconditionalAccess | ConditionalDependency` produces an
* unconditionally accessed conditional dependency. We currently use these
* as we use unconditional dependencies. (i.e. to codegen change variables)
*/
if (resultIsUnconditional) {
if (resultIsDependency) {
return PropertyAccessType.UnconditionalDependency;
} else {
return PropertyAccessType.UnconditionalAccess;
}
} else {
// result is optional
if (resultIsDependency) {
return PropertyAccessType.OptionalDependency;
} else {
return PropertyAccessType.OptionalAccess;
}
}
}
type TreeNode<T extends string> = {
properties: Map<string, TreeNode<T>>;
accessType: T;
};
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,
rootIdentifier: Identifier,
path: Array<DependencyPathEntry>,
results: Set<ReactiveScopeDependency>,
): void {
if (isDependency(node.accessType)) {
results.add({identifier: rootIdentifier, path});
} else {
for (const [childName, childNode] of node.properties) {
collectMinimalDependenciesInSubtree(
childNode,
rootIdentifier,
[
...path,
{
property: childName,
optional: isOptional(childNode.accessType),
},
],
results,
);
}
}
}
function printSubtree(
node: DependencyNode,
includeAccesses: boolean,
): Array<string> {
const results: Array<string> = [];
for (const [propertyName, propertyNode] of node.properties) {
if (includeAccesses || isDependency(propertyNode.accessType)) {
results.push(`${propertyName} (${propertyNode.accessType})`);
}
const propertyResults = printSubtree(propertyNode, includeAccesses);
results.push(...propertyResults.map(result => `${propertyName}.${result}`));
}
return results;
}
function makeOrMergeProperty(
node: DependencyNode,
property: string,
accessType: PropertyAccessType,
): DependencyNode {
let child = node.properties.get(property);
if (child == null) {
child = {
properties: new Map(),
accessType,
};
node.properties.set(property, child);
} else {
child.accessType = merge(child.accessType, accessType);
}
return child;
}

View File

@@ -16,8 +16,7 @@ import {
DEFAULT_SHAPES,
Global,
GlobalRegistry,
getReanimatedModuleType,
installTypeConfig,
installReAnimatedTypes,
} from './Globals';
import {
BlockId,
@@ -29,11 +28,9 @@ import {
NonLocalBinding,
PolyType,
ScopeId,
SourceLocation,
Type,
ValidatedIdentifier,
ValueKind,
getHookKindForType,
makeBlockId,
makeIdentifierId,
makeIdentifierName,
@@ -48,14 +45,6 @@ import {
addHook,
} from './ObjectShape';
import {Scope as BabelScope} from '@babel/traverse';
import {TypeSchema} from './TypeSchema';
export const ReactElementSymbolSchema = z.object({
elementSymbol: z.union([
z.literal('react.element'),
z.literal('react.transitional.element'),
]),
});
export const ExternalFunctionSchema = z.object({
// Source for the imported module that exports the `importSpecifierName` functions
@@ -78,20 +67,6 @@ export const InstrumentationSchema = z
export type ExternalFunction = z.infer<typeof ExternalFunctionSchema>;
export const MacroMethodSchema = z.union([
z.object({type: z.literal('wildcard')}),
z.object({type: z.literal('name'), name: z.string()}),
]);
// Would like to change this to drop the string option, but breaks compatibility with existing configs
export const MacroSchema = z.union([
z.string(),
z.tuple([z.string(), z.array(MacroMethodSchema)]),
]);
export type Macro = z.infer<typeof MacroSchema>;
export type MacroMethod = z.infer<typeof MacroMethodSchema>;
const HookSchema = z.object({
/*
* The effect of arguments to this hook. Describes whether the hook may or may
@@ -148,12 +123,6 @@ export type Hook = z.infer<typeof HookSchema>;
const EnvironmentConfigSchema = z.object({
customHooks: z.map(z.string(), HookSchema).optional().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.function().args(z.string())).default(null),
/**
* A list of functions which the application compiles as macros, where
* the compiler must ensure they are not compiled to rename the macro or separate the
@@ -164,7 +133,7 @@ const EnvironmentConfigSchema = z.object({
* plugin since it looks specifically for the name of the function being invoked, not
* following aliases.
*/
customMacros: z.nullable(z.array(MacroSchema)).default(null),
customMacros: z.nullable(z.array(z.string())).default(null),
/**
* Enable a check that resets the memoization cache when the source code of the file changes.
@@ -230,24 +199,7 @@ const EnvironmentConfigSchema = z.object({
*/
enableUseTypeAnnotations: z.boolean().default(false),
enablePropagateDepsInHIR: z.boolean().default(false),
/**
* Enables inference of optional dependency chains. Without this flag
* a property chain such as `props?.items?.foo` will infer as a dep on
* just `props`. With this flag enabled, we'll infer that full path as
* the dependency.
*/
enableOptionalDependencies: z.boolean().default(true),
/**
* Enables inlining ReactElement object literals in place of JSX
* An alternative to the standard JSX transform which replaces JSX with React's jsxProd() runtime
* Currently a prod-only optimization, requiring Fast JSX dependencies
*
* The symbol configuration is set for backwards compatability with pre-React 19 transforms
*/
inlineJsxTransform: ReactElementSymbolSchema.nullish(),
enableReactiveScopesInHIR: z.boolean().default(true),
/*
* Enable validation of hooks to partially check that the component honors the rules of hooks.
@@ -257,7 +209,7 @@ const EnvironmentConfigSchema = z.object({
validateHooksUsage: z.boolean().default(true),
// Validate that ref values (`ref.current`) are not accessed during render.
validateRefAccessDuringRender: z.boolean().default(true),
validateRefAccessDuringRender: z.boolean().default(false),
/*
* Validates that setState is not unconditionally called during render, as it can lead to
@@ -265,18 +217,6 @@ const EnvironmentConfigSchema = z.object({
*/
validateNoSetStateInRender: z.boolean().default(true),
/**
* Validates that setState is not called directly within a passive effect (useEffect).
* Scheduling a setState (with an event listener, subscription, etc) is valid.
*/
validateNoSetStateInPassiveEffects: z.boolean().default(false),
/**
* Validates against creating JSX within a try block and recommends using an error boundary
* instead.
*/
validateNoJSXInTryStatements: z.boolean().default(false),
/**
* Validates that the dependencies of all effect hooks are memoized. This helps ensure
* that Forget does not introduce infinite renders caused by a dependency changing,
@@ -298,7 +238,6 @@ const EnvironmentConfigSchema = z.object({
* this option to the empty array.
*/
validateNoCapitalizedCalls: z.nullable(z.array(z.string())).default(null),
validateBlocklistedImports: z.nullable(z.array(z.string())).default(null),
/*
* When enabled, the compiler assumes that hooks follow the Rules of React:
@@ -495,28 +434,6 @@ const EnvironmentConfigSchema = z.object({
* Here the variables `ref` and `myRef` will be typed as Refs.
*/
enableTreatRefLikeIdentifiersAsRefs: z.boolean().nullable().default(false),
/*
* If specified a value, the compiler lowers any calls to `useContext` to use
* this value as the callee.
*
* A selector function is compiled and passed as an argument along with the
* context to this function call.
*
* The compiler automatically figures out the keys by looking for the immediate
* destructuring of the return value from the useContext call. In the future,
* this can be extended to different kinds of context access like property
* loads and accesses over multiple statements as well.
*
* ```
* // input
* const {foo, bar} = useContext(MyContext);
*
* // output
* const {foo, bar} = useCompiledContext(MyContext, (c) => [c.foo, c.bar]);
* ```
*/
lowerContextAccess: ExternalFunctionSchema.nullish(),
});
export type EnvironmentConfig = z.infer<typeof EnvironmentConfigSchema>;
@@ -549,23 +466,6 @@ export function parseConfigPragma(pragma: string): EnvironmentConfig {
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});
}
}
console.log([valSplit[0], props.map(x => x.name ?? '*').join('.')]);
maybeConfig[key] = [[valSplit[0], props]];
}
continue;
}
if (typeof defaultConfig[key as keyof EnvironmentConfig] !== 'boolean') {
// skip parsing non-boolean properties
continue;
@@ -611,7 +511,6 @@ export function printFunctionType(type: ReactFunctionType): string {
export class Environment {
#globals: GlobalRegistry;
#shapes: ShapeRegistry;
#moduleTypes: Map<string, Global | null> = new Map();
#nextIdentifer: number = 0;
#nextBlock: number = 0;
#nextScope: number = 0;
@@ -626,7 +525,6 @@ export class Environment {
config: EnvironmentConfig;
fnType: ReactFunctionType;
useMemoCacheIdentifier: string;
hasLoweredContextAccess: boolean;
#contextIdentifiers: Set<t.Identifier>;
#hoistedIdentifiers: Set<t.Identifier>;
@@ -650,7 +548,6 @@ export class Environment {
this.useMemoCacheIdentifier = useMemoCacheIdentifier;
this.#shapes = new Map(DEFAULT_SHAPES);
this.#globals = new Map(DEFAULT_GLOBALS);
this.hasLoweredContextAccess = false;
if (
config.disableMemoizationForDebugging &&
@@ -688,8 +585,7 @@ export class Environment {
}
if (config.enableCustomTypeDefinitionForReanimated) {
const reanimatedModuleType = getReanimatedModuleType(this.#shapes);
this.#moduleTypes.set(REANIMATED_MODULE_NAME, reanimatedModuleType);
installReAnimatedTypes(this.#globals, this.#shapes);
}
this.#contextIdentifiers = contextIdentifiers;
@@ -734,42 +630,7 @@ export class Environment {
return this.#outlinedFunctions;
}
#resolveModuleType(moduleName: string, loc: SourceLocation): Global | null {
let moduleType = this.#moduleTypes.get(moduleName);
if (moduleType === undefined) {
if (this.config.moduleTypeProvider == null) {
return null;
}
const unparsedModuleConfig = this.config.moduleTypeProvider(moduleName);
if (unparsedModuleConfig != null) {
const parsedModuleConfig = TypeSchema.safeParse(unparsedModuleConfig);
if (!parsedModuleConfig.success) {
CompilerError.throwInvalidConfig({
reason: `Could not parse module type, the configured \`moduleTypeProvider\` function returned an invalid module description`,
description: parsedModuleConfig.error.toString(),
loc,
});
}
const moduleConfig = parsedModuleConfig.data;
moduleType = installTypeConfig(
this.#globals,
this.#shapes,
moduleConfig,
moduleName,
loc,
);
} else {
moduleType = null;
}
this.#moduleTypes.set(moduleName, moduleType);
}
return moduleType;
}
getGlobalDeclaration(
binding: NonLocalBinding,
loc: SourceLocation,
): Global | null {
getGlobalDeclaration(binding: NonLocalBinding): Global | null {
if (this.config.hookPattern != null) {
const match = new RegExp(this.config.hookPattern).exec(binding.name);
if (
@@ -807,32 +668,6 @@ export class Environment {
(isHookName(binding.imported) ? this.#getCustomHookType() : null)
);
} else {
const moduleType = this.#resolveModuleType(binding.module, loc);
if (moduleType !== null) {
const importedType = this.getPropertyType(
moduleType,
binding.imported,
);
if (importedType != null) {
/*
* Check that hook-like export names are hook types, and non-hook names are non-hook types.
* The user-assigned alias isn't decidable by the type provider, so we ignore that for the check.
* Thus we allow `import {fooNonHook as useFoo} from ...` because the name and type both say
* that it's not a hook.
*/
const expectHook = isHookName(binding.imported);
const isHook = getHookKindForType(this, importedType) != null;
if (expectHook !== isHook) {
CompilerError.throwInvalidConfig({
reason: `Invalid type configuration for module`,
description: `Expected type for \`import {${binding.imported}} from '${binding.module}'\` ${expectHook ? 'to be a hook' : 'not to be a hook'} based on the exported name`,
loc,
});
}
return importedType;
}
}
/**
* For modules we don't own, we look at whether the original name or import alias
* are hook-like. Both of the following are likely hooks so we would return a hook
@@ -855,34 +690,6 @@ export class Environment {
(isHookName(binding.name) ? this.#getCustomHookType() : null)
);
} else {
const moduleType = this.#resolveModuleType(binding.module, loc);
if (moduleType !== null) {
let importedType: Type | null = null;
if (binding.kind === 'ImportDefault') {
const defaultType = this.getPropertyType(moduleType, 'default');
if (defaultType !== null) {
importedType = defaultType;
}
} else {
importedType = moduleType;
}
if (importedType !== null) {
/*
* Check that the hook-like modules are defined as types, and non hook-like modules are not typed as hooks.
* So `import Foo from 'useFoo'` is expected to be a hook based on the module name
*/
const expectHook = isHookName(binding.module);
const isHook = getHookKindForType(this, importedType) != null;
if (expectHook !== isHook) {
CompilerError.throwInvalidConfig({
reason: `Invalid type configuration for module`,
description: `Expected type for \`import ... from '${binding.module}'\` ${expectHook ? 'to be a hook' : 'not to be a hook'} based on the module name`,
loc,
});
}
return importedType;
}
}
return isHookName(binding.name) ? this.#getCustomHookType() : null;
}
}
@@ -892,7 +699,9 @@ export class Environment {
#isKnownReactModule(moduleName: string): boolean {
return (
moduleName.toLowerCase() === 'react' ||
moduleName.toLowerCase() === 'react-dom'
moduleName.toLowerCase() === 'react-dom' ||
(this.config.enableSharedRuntime__testonly &&
moduleName === 'shared-runtime')
);
}
@@ -958,8 +767,6 @@ export class Environment {
}
}
const REANIMATED_MODULE_NAME = 'react-native-reanimated';
// From https://github.com/facebook/react/blob/main/packages/eslint-plugin-react-hooks/src/RulesOfHooks.js#LL18C1-L23C2
export function isHookName(name: string): boolean {
return /^use[A-Z0-9]/.test(name);

View File

@@ -9,9 +9,7 @@ import {Effect, ValueKind, ValueReason} from './HIR';
import {
BUILTIN_SHAPES,
BuiltInArrayId,
BuiltInMixedReadonlyId,
BuiltInUseActionStateId,
BuiltInUseContextHookId,
BuiltInUseEffectHookId,
BuiltInUseInsertionEffectHookId,
BuiltInUseLayoutEffectHookId,
@@ -19,17 +17,12 @@ import {
BuiltInUseReducerId,
BuiltInUseRefId,
BuiltInUseStateId,
BuiltInUseTransitionId,
ShapeRegistry,
addFunction,
addHook,
addObject,
} from './ObjectShape';
import {BuiltInType, ObjectType, PolyType} from './Types';
import {TypeConfig} from './TypeSchema';
import {assertExhaustive} from '../Utils/utils';
import {isHookName} from './Environment';
import {CompilerError, SourceLocation} from '..';
import {BuiltInType, PolyType} from './Types';
/*
* This file exports types and defaults for JavaScript global objects.
@@ -142,57 +135,6 @@ const TYPED_GLOBALS: Array<[string, BuiltInType]> = [
returnValueKind: ValueKind.Primitive,
}),
],
[
'min',
// Math.min(value0, ..., valueN)
addFunction(DEFAULT_SHAPES, [], {
positionalParams: [],
restParam: Effect.Read,
returnType: {kind: 'Primitive'},
calleeEffect: Effect.Read,
returnValueKind: ValueKind.Primitive,
}),
],
[
'trunc',
addFunction(DEFAULT_SHAPES, [], {
positionalParams: [],
restParam: Effect.Read,
returnType: {kind: 'Primitive'},
calleeEffect: Effect.Read,
returnValueKind: ValueKind.Primitive,
}),
],
[
'ceil',
addFunction(DEFAULT_SHAPES, [], {
positionalParams: [],
restParam: Effect.Read,
returnType: {kind: 'Primitive'},
calleeEffect: Effect.Read,
returnValueKind: ValueKind.Primitive,
}),
],
[
'floor',
addFunction(DEFAULT_SHAPES, [], {
positionalParams: [],
restParam: Effect.Read,
returnType: {kind: 'Primitive'},
calleeEffect: Effect.Read,
returnValueKind: ValueKind.Primitive,
}),
],
[
'pow',
addFunction(DEFAULT_SHAPES, [], {
positionalParams: [],
restParam: Effect.Read,
returnType: {kind: 'Primitive'},
calleeEffect: Effect.Read,
returnValueKind: ValueKind.Primitive,
}),
],
]),
],
['Infinity', {kind: 'Primitive'}],
@@ -303,19 +245,15 @@ const TYPED_GLOBALS: Array<[string, BuiltInType]> = [
const REACT_APIS: Array<[string, BuiltInType]> = [
[
'useContext',
addHook(
DEFAULT_SHAPES,
{
positionalParams: [],
restParam: Effect.Read,
returnType: {kind: 'Poly'},
calleeEffect: Effect.Read,
hookKind: 'useContext',
returnValueKind: ValueKind.Frozen,
returnValueReason: ValueReason.Context,
},
BuiltInUseContextHookId,
),
addHook(DEFAULT_SHAPES, {
positionalParams: [],
restParam: Effect.Read,
returnType: {kind: 'Poly'},
calleeEffect: Effect.Read,
hookKind: 'useContext',
returnValueKind: ValueKind.Frozen,
returnValueReason: ValueReason.Context,
}),
],
[
'useState',
@@ -364,17 +302,6 @@ const REACT_APIS: Array<[string, BuiltInType]> = [
returnValueKind: ValueKind.Mutable,
}),
],
[
'useImperativeHandle',
addHook(DEFAULT_SHAPES, {
positionalParams: [],
restParam: Effect.Freeze,
returnType: {kind: 'Primitive'},
calleeEffect: Effect.Read,
hookKind: 'useImperativeHandle',
returnValueKind: ValueKind.Frozen,
}),
],
[
'useMemo',
addHook(DEFAULT_SHAPES, {
@@ -442,17 +369,6 @@ const REACT_APIS: Array<[string, BuiltInType]> = [
BuiltInUseInsertionEffectHookId,
),
],
[
'useTransition',
addHook(DEFAULT_SHAPES, {
positionalParams: [],
restParam: null,
returnType: {kind: 'Object', shapeId: BuiltInUseTransitionId},
calleeEffect: Effect.Read,
hookKind: 'useTransition',
returnValueKind: ValueKind.Frozen,
}),
],
[
'use',
addFunction(
@@ -534,125 +450,16 @@ for (const [name, type_] of TYPED_GLOBALS) {
DEFAULT_GLOBALS.set(name, type_);
}
// Recursive global types
// Recursive global type
DEFAULT_GLOBALS.set(
'globalThis',
addObject(DEFAULT_SHAPES, 'globalThis', TYPED_GLOBALS),
);
DEFAULT_GLOBALS.set(
'global',
addObject(DEFAULT_SHAPES, 'global', TYPED_GLOBALS),
);
export function installTypeConfig(
export function installReAnimatedTypes(
globals: GlobalRegistry,
shapes: ShapeRegistry,
typeConfig: TypeConfig,
moduleName: string,
loc: SourceLocation,
): Global {
switch (typeConfig.kind) {
case 'type': {
switch (typeConfig.name) {
case 'Array': {
return {kind: 'Object', shapeId: BuiltInArrayId};
}
case 'MixedReadonly': {
return {kind: 'Object', shapeId: BuiltInMixedReadonlyId};
}
case 'Primitive': {
return {kind: 'Primitive'};
}
case 'Ref': {
return {kind: 'Object', shapeId: BuiltInUseRefId};
}
case 'Any': {
return {kind: 'Poly'};
}
default: {
assertExhaustive(
typeConfig.name,
`Unexpected type '${(typeConfig as any).name}'`,
);
}
}
}
case 'function': {
return addFunction(shapes, [], {
positionalParams: typeConfig.positionalParams,
restParam: typeConfig.restParam,
calleeEffect: typeConfig.calleeEffect,
returnType: installTypeConfig(
globals,
shapes,
typeConfig.returnType,
moduleName,
loc,
),
returnValueKind: typeConfig.returnValueKind,
noAlias: typeConfig.noAlias === true,
mutableOnlyIfOperandsAreMutable:
typeConfig.mutableOnlyIfOperandsAreMutable === true,
});
}
case 'hook': {
return addHook(shapes, {
hookKind: 'Custom',
positionalParams: typeConfig.positionalParams ?? [],
restParam: typeConfig.restParam ?? Effect.Freeze,
calleeEffect: Effect.Read,
returnType: installTypeConfig(
globals,
shapes,
typeConfig.returnType,
moduleName,
loc,
),
returnValueKind: typeConfig.returnValueKind ?? ValueKind.Frozen,
noAlias: typeConfig.noAlias === true,
});
}
case 'object': {
return addObject(
shapes,
null,
Object.entries(typeConfig.properties ?? {}).map(([key, value]) => {
const type = installTypeConfig(
globals,
shapes,
value,
moduleName,
loc,
);
const expectHook = isHookName(key);
let isHook = false;
if (type.kind === 'Function' && type.shapeId !== null) {
const functionType = shapes.get(type.shapeId);
if (functionType?.functionType?.hookKind !== null) {
isHook = true;
}
}
if (expectHook !== isHook) {
CompilerError.throwInvalidConfig({
reason: `Invalid type configuration for module`,
description: `Expected type for object property '${key}' from module '${moduleName}' ${expectHook ? 'to be a hook' : 'not to be a hook'} based on the property name`,
loc,
});
}
return [key, type];
}),
);
}
default: {
assertExhaustive(
typeConfig,
`Unexpected type kind '${(typeConfig as any).kind}'`,
);
}
}
}
export function getReanimatedModuleType(registry: ShapeRegistry): ObjectType {
registry: ShapeRegistry,
): void {
// hooks that freeze args and return frozen value
const frozenHooks = [
'useFrameCallback',
@@ -662,9 +469,8 @@ export function getReanimatedModuleType(registry: ShapeRegistry): ObjectType {
'useAnimatedReaction',
'useWorkletCallback',
];
const reanimatedType: Array<[string, BuiltInType]> = [];
for (const hook of frozenHooks) {
reanimatedType.push([
globals.set(
hook,
addHook(registry, {
positionalParams: [],
@@ -675,7 +481,7 @@ export function getReanimatedModuleType(registry: ShapeRegistry): ObjectType {
calleeEffect: Effect.Read,
hookKind: 'Custom',
}),
]);
);
}
/**
@@ -684,7 +490,7 @@ export function getReanimatedModuleType(registry: ShapeRegistry): ObjectType {
*/
const mutableHooks = ['useSharedValue', 'useDerivedValue'];
for (const hook of mutableHooks) {
reanimatedType.push([
globals.set(
hook,
addHook(registry, {
positionalParams: [],
@@ -695,7 +501,7 @@ export function getReanimatedModuleType(registry: ShapeRegistry): ObjectType {
calleeEffect: Effect.Read,
hookKind: 'Custom',
}),
]);
);
}
// functions that return mutable value
@@ -709,7 +515,7 @@ export function getReanimatedModuleType(registry: ShapeRegistry): ObjectType {
'executeOnUIRuntimeSync',
];
for (const fn of funcs) {
reanimatedType.push([
globals.set(
fn,
addFunction(registry, [], {
positionalParams: [],
@@ -719,8 +525,6 @@ export function getReanimatedModuleType(registry: ShapeRegistry): ObjectType {
returnValueKind: ValueKind.Mutable,
noAlias: true,
}),
]);
);
}
return addObject(registry, null, reanimatedType);
}

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