Compare commits

..

1 Commits

Author SHA1 Message Date
Josh Story
6230622a1a [Float][Fiber] Assume stylesheets in document are already loaded (cherrypick #29811) (#29835)
Cherrypick #29811

When we made stylesheets suspend even during high priority updates we
exposed a bug in the loading tracking of stylesheets that are loaded as
part of the preamble. This allowed these stylesheets to put suspense
boundaries into fallback mode more often than expected because cases
where a stylesheet was server rendered could now cause a fallback to
trigger which was never intended to happen.

This fix updates resource construction to evaluate whether the instance
exists in the DOM prior to construction and if so marks the resource as
loaded and inserted.

One ambiguity that needed to be solved still is how to tell whether a
stylesheet rendered as part of a late Suspense boundary reveal is
already loaded. I updated the instruction to clear out the loading
promise after successfully loading. This is useful because later if we
encounter this same resource again we can avoid the microtask if it is
already loaded. It also means that we can concretely understand that if
a stylesheet is in the DOM without this marker then it must have loaded
(or errored) already.
2024-06-10 11:41:15 -07:00
2625 changed files with 26414 additions and 43473 deletions

699
.circleci/config.yml Normal file
View File

@@ -0,0 +1,699 @@
version: 2.1
aliases:
- &docker
- image: cimg/node:18.20.1-browsers
- &environment
TZ: /usr/share/zoneinfo/America/Los_Angeles
- &restore_yarn_cache_fixtures_dom
restore_cache:
name: Restore yarn cache for fixtures/dom
keys:
- v2-yarn_cache-{{ arch }}-{{ checksum "yarn.lock" }}-fixtures/dom
- &yarn_install_fixtures_dom
run:
name: Install dependencies in fixtures/dom
working_directory: fixtures/dom
command: yarn install --frozen-lockfile --cache-folder ~/.cache/yarn
- &yarn_install_fixtures_dom_retry
run:
name: Install dependencies in fixtures/dom (retry)
when: on_fail
working_directory: fixtures/dom
command: yarn install --frozen-lockfile --cache-folder ~/.cache/yarn
- &save_yarn_cache_fixtures_dom
save_cache:
name: Save yarn cache for fixtures/dom
key: v2-yarn_cache-{{ arch }}-{{ checksum "yarn.lock" }}-fixtures/dom
paths:
- ~/.cache/yarn
- &TEST_PARALLELISM 20
- &attach_workspace
at: build
commands:
setup_node_modules:
description: "Restore node_modules"
steps:
- restore_cache:
name: Restore yarn cache
keys:
- v2-yarn_cache-{{ arch }}-{{ checksum "yarn.lock" }}
- run:
name: Install dependencies
command: |
yarn install --frozen-lockfile --cache-folder ~/.cache/yarn
if [ $? -ne 0 ]; then
yarn install --frozen-lockfile --cache-folder ~/.cache/yarn
fi
environment:
# If we start needing the Electron binary, please ensure the binary is cached in CI following https://www.electronjs.org/docs/latest/tutorial/installation
ELECTRON_SKIP_BINARY_DOWNLOAD: 1
- save_cache:
name: Save yarn cache
key: v2-yarn_cache-{{ arch }}-{{ checksum "yarn.lock" }}
paths:
- ~/.cache/yarn
# The CircleCI API doesn't yet support triggering a specific workflow, but it
# does support triggering a pipeline. So as a workaround you can triggger the
# entire pipeline and use parameters to disable everything except the workflow
# you want. CircleCI recommends this workaround here:
# https://support.circleci.com/hc/en-us/articles/360050351292-How-to-trigger-a-workflow-via-CircleCI-API-v2-
parameters:
# This is only set when triggering the CI pipeline via an API request.
prerelease_commit_sha:
type: string
default: ''
jobs:
yarn_lint:
docker: *docker
environment: *environment
steps:
- checkout
- setup_node_modules
- run: node ./scripts/prettier/index
- run: node ./scripts/tasks/eslint
- run: ./scripts/circleci/check_license.sh
- run: ./scripts/circleci/test_print_warnings.sh
yarn_flow:
docker: *docker
environment: *environment
parallelism: 5
steps:
- checkout
- setup_node_modules
- run: node ./scripts/tasks/flow-ci
yarn_flags:
docker: *docker
environment: *environment
steps:
- checkout
- setup_node_modules
- run: yarn flags
scrape_warning_messages:
docker: *docker
environment: *environment
steps:
- checkout
- setup_node_modules
- run:
command: |
mkdir -p ./build/__test_utils__
node ./scripts/print-warnings/print-warnings.js > build/__test_utils__/ReactAllWarnings.js
- persist_to_workspace:
root: .
paths:
- build
yarn_build:
docker: *docker
environment: *environment
parallelism: 40
steps:
- checkout
- setup_node_modules
- run: yarn build
- persist_to_workspace:
root: .
paths:
- build
download_build:
docker: *docker
environment: *environment
parameters:
revision:
type: string
steps:
- checkout
- setup_node_modules
- run:
name: Download artifacts for revision
command: |
git fetch origin main
cd ./scripts/release && yarn && cd ../../
scripts/release/download-experimental-build.js --commit=<< parameters.revision >> --allowBrokenCI
- persist_to_workspace:
root: .
paths:
- build
download_base_build_for_sizebot:
docker: *docker
environment: *environment
steps:
- checkout
- setup_node_modules
- run:
name: Download artifacts for base revision
command: |
git fetch origin main
cd ./scripts/release && yarn && cd ../../
scripts/release/download-experimental-build.js --commit=$(git merge-base HEAD origin/main) --allowBrokenCI
mv ./build ./base-build
- run:
# TODO: The `download-experimental-build` script copies the npm
# packages into the `node_modules` directory. This is a historical
# quirk of how the release script works. Let's pretend they
# don't exist.
name: Delete extraneous files
command: rm -rf ./base-build/node_modules
- persist_to_workspace:
root: .
paths:
- base-build
process_artifacts_combined:
docker: *docker
environment: *environment
steps:
- checkout
- attach_workspace:
at: .
- setup_node_modules
- run: echo "<< pipeline.git.revision >>" >> build/COMMIT_SHA
# Compress build directory into a single tarball for easy download
- run: tar -zcvf ./build.tgz ./build
# TODO: Migrate scripts to use `build` directory instead of `build2`
- run: cp ./build.tgz ./build2.tgz
- store_artifacts:
path: ./build2.tgz
- store_artifacts:
path: ./build.tgz
sizebot:
docker: *docker
environment: *environment
steps:
- checkout
- attach_workspace:
at: .
- run: echo "<< pipeline.git.revision >>" >> build/COMMIT_SHA
- setup_node_modules
- run:
command: node ./scripts/tasks/danger
- store_artifacts:
path: sizebot-message.md
build_devtools_and_process_artifacts:
docker: *docker
environment: *environment
steps:
- checkout
- attach_workspace:
at: .
- setup_node_modules
- run:
environment:
RELEASE_CHANNEL: experimental
command: ./scripts/circleci/pack_and_store_devtools_artifacts.sh
- store_artifacts:
path: ./build/devtools.tgz
# Simplifies getting the extension for local testing
- store_artifacts:
path: ./build/devtools/chrome-extension.zip
destination: react-devtools-chrome-extension.zip
- store_artifacts:
path: ./build/devtools/firefox-extension.zip
destination: react-devtools-firefox-extension.zip
run_devtools_e2e_tests:
docker: *docker
environment: *environment
steps:
- checkout
- attach_workspace:
at: .
- setup_node_modules
- run:
name: Playwright install deps
command: |
npx playwright install
sudo npx playwright install-deps
- run:
environment:
RELEASE_CHANNEL: experimental
command: ./scripts/circleci/run_devtools_e2e_tests.js
run_fixtures_flight_tests:
docker: *docker
environment: *environment
steps:
- checkout
- attach_workspace:
at: .
# Fixture copies some built packages from the workroot after install.
# That means dependencies of the built packages are not installed.
# We need to install dependencies of the workroot to fulfill all dependency constraints
- setup_node_modules
- restore_cache:
name: Restore yarn cache of fixture
keys:
- v2-yarn_cache_fixtures_flight-{{ arch }}-{{ checksum "yarn.lock" }}
- run:
name: Install fixture dependencies
working_directory: fixtures/flight
command: |
yarn install --frozen-lockfile --cache-folder ~/.cache/yarn
if [ $? -ne 0 ]; then
yarn install --frozen-lockfile --cache-folder ~/.cache/yarn
fi
- save_cache:
name: Save yarn cache of fixture
key: v2-yarn_cache_fixtures_flight-{{ arch }}-{{ checksum "yarn.lock" }}
paths:
- ~/.cache/yarn
- run:
working_directory: fixtures/flight
name: Playwright install deps
command: |
npx playwright install
sudo npx playwright install-deps
- run:
name: Run tests
working_directory: fixtures/flight
command: yarn test
environment:
# Otherwise the webserver is a blackbox
DEBUG: pw:webserver
- store_artifacts:
path: fixtures/flight/playwright-report
- store_artifacts:
path: fixtures/flight/test-results
run_devtools_tests_for_versions:
docker: *docker
environment: *environment
parallelism: *TEST_PARALLELISM
parameters:
version:
type: string
steps:
- checkout
- attach_workspace:
at: .
- setup_node_modules
- run: ./scripts/circleci/download_devtools_regression_build.js << parameters.version >> --replaceBuild
- run: node ./scripts/jest/jest-cli.js --build --project devtools --release-channel=experimental --reactVersion << parameters.version >> --ci
run_devtools_e2e_tests_for_versions:
docker: *docker
environment: *environment
parallelism: *TEST_PARALLELISM
parameters:
version:
type: string
steps:
- checkout
- attach_workspace:
at: .
- setup_node_modules
- run:
name: Playwright install deps
command: |
npx playwright install
sudo npx playwright install-deps
- run: ./scripts/circleci/download_devtools_regression_build.js << parameters.version >>
- run:
environment:
RELEASE_CHANNEL: experimental
command: ./scripts/circleci/run_devtools_e2e_tests.js << parameters.version >>
- run:
name: Cleanup build regression folder
command: rm -r ./build-regression
- store_artifacts:
path: ./tmp/screenshots
yarn_lint_build:
docker: *docker
environment: *environment
steps:
- checkout
- attach_workspace:
at: .
- setup_node_modules
- run: yarn lint-build
yarn_check_release_dependencies:
docker: *docker
environment: *environment
steps:
- checkout
- attach_workspace:
at: .
- setup_node_modules
- run: yarn check-release-dependencies
check_error_codes:
docker: *docker
environment: *environment
steps:
- checkout
- attach_workspace: *attach_workspace
- setup_node_modules
- run:
name: Search build artifacts for unminified errors
command: |
yarn extract-errors
git diff --quiet || (echo "Found unminified errors. Either update the error codes map or disable error minification for the affected build, if appropriate." && false)
check_generated_fizz_runtime:
docker: *docker
environment: *environment
steps:
- checkout
- attach_workspace: *attach_workspace
- setup_node_modules
- run:
name: Confirm generated inline Fizz runtime is up to date
command: |
yarn generate-inline-fizz-runtime
git diff --quiet || (echo "There was a change to the Fizz runtime. Run `yarn generate-inline-fizz-runtime` and check in the result." && false)
yarn_test:
docker: *docker
environment: *environment
parallelism: *TEST_PARALLELISM
parameters:
args:
type: string
steps:
- checkout
- setup_node_modules
- run: yarn test <<parameters.args>> --ci
yarn_test_build:
docker: *docker
environment: *environment
parallelism: *TEST_PARALLELISM
parameters:
args:
type: string
steps:
- checkout
- attach_workspace:
at: .
- setup_node_modules
- run: yarn test --build <<parameters.args>> --ci
RELEASE_CHANNEL_stable_yarn_test_dom_fixtures:
docker: *docker
environment: *environment
steps:
- checkout
- attach_workspace:
at: .
- setup_node_modules
- *restore_yarn_cache_fixtures_dom
- *yarn_install_fixtures_dom
- *yarn_install_fixtures_dom_retry
- *save_yarn_cache_fixtures_dom
- run:
name: Run DOM fixture tests
environment:
RELEASE_CHANNEL: stable
working_directory: fixtures/dom
command: |
yarn predev
yarn test --maxWorkers=2
publish_prerelease:
parameters:
commit_sha:
type: string
release_channel:
type: string
dist_tag:
type: string
docker: *docker
environment: *environment
steps:
- checkout
- setup_node_modules
- run:
name: Run publish script
command: |
git fetch origin main
cd ./scripts/release && yarn && cd ../../
scripts/release/prepare-release-from-ci.js --skipTests -r << parameters.release_channel >> --commit=<< parameters.commit_sha >>
cp ./scripts/release/ci-npmrc ~/.npmrc
scripts/release/publish.js --ci --tags << parameters.dist_tag >>
workflows:
build_and_test:
unless: << pipeline.parameters.prerelease_commit_sha >>
jobs:
- yarn_flags:
filters:
branches:
ignore:
- builds/facebook-www
- yarn_flow:
filters:
branches:
ignore:
- builds/facebook-www
- check_generated_fizz_runtime:
filters:
branches:
ignore:
- builds/facebook-www
- yarn_lint:
filters:
branches:
ignore:
- builds/facebook-www
- yarn_test:
filters:
branches:
ignore:
- builds/facebook-www
matrix:
parameters:
args:
# Intentionally passing these as strings instead of creating a
# separate parameter per CLI argument, since it's easier to
# control/see which combinations we want to run.
- "-r=stable --env=development"
- "-r=stable --env=production"
- "-r=experimental --env=development"
- "-r=experimental --env=production"
- "-r=www-classic --env=development --variant=false"
- "-r=www-classic --env=production --variant=false"
- "-r=www-classic --env=development --variant=true"
- "-r=www-classic --env=production --variant=true"
- "-r=www-modern --env=development --variant=false"
- "-r=www-modern --env=production --variant=false"
- "-r=www-modern --env=development --variant=true"
- "-r=www-modern --env=production --variant=true"
- "-r=xplat --env=development --variant=false"
- "-r=xplat --env=development --variant=true"
- "-r=xplat --env=production --variant=false"
- "-r=xplat --env=production --variant=true"
# TODO: Test more persistent configurations?
- '-r=stable --env=development --persistent'
- '-r=experimental --env=development --persistent'
- yarn_build:
filters:
branches:
ignore:
- builds/facebook-www
- scrape_warning_messages:
filters:
branches:
ignore:
- builds/facebook-www
- process_artifacts_combined:
requires:
- scrape_warning_messages
- yarn_build
- yarn_test_build:
requires:
- yarn_build
matrix:
parameters:
args:
# Intentionally passing these as strings instead of creating a
# separate parameter per CLI argument, since it's easier to
# control/see which combinations we want to run.
- "-r=stable --env=development"
- "-r=stable --env=production"
- "-r=experimental --env=development"
- "-r=experimental --env=production"
# Dev Tools
- "--project=devtools -r=experimental"
# TODO: Update test config to support www build tests
# - "-r=www-classic --env=development --variant=false"
# - "-r=www-classic --env=production --variant=false"
# - "-r=www-classic --env=development --variant=true"
# - "-r=www-classic --env=production --variant=true"
# - "-r=www-modern --env=development --variant=false"
# - "-r=www-modern --env=production --variant=false"
# - "-r=www-modern --env=development --variant=true"
# - "-r=www-modern --env=production --variant=true"
# TODO: Update test config to support xplat build tests
# - "-r=xplat --env=development --variant=false"
# - "-r=xplat --env=development --variant=true"
# - "-r=xplat --env=production --variant=false"
# - "-r=xplat --env=production --variant=true"
# TODO: Test more persistent configurations?
- download_base_build_for_sizebot:
filters:
branches:
ignore:
- main
- builds/facebook-www
- sizebot:
filters:
branches:
ignore:
- main
requires:
- download_base_build_for_sizebot
- yarn_build
- yarn_lint_build:
requires:
- yarn_build
- yarn_check_release_dependencies:
requires:
- yarn_build
- check_error_codes:
requires:
- yarn_build
- RELEASE_CHANNEL_stable_yarn_test_dom_fixtures:
requires:
- yarn_build
- build_devtools_and_process_artifacts:
requires:
- yarn_build
- run_devtools_e2e_tests:
requires:
- build_devtools_and_process_artifacts
- run_fixtures_flight_tests:
requires:
- yarn_build
devtools_regression_tests:
unless: << pipeline.parameters.prerelease_commit_sha >>
triggers:
- schedule:
# DevTools regression tests run once a day
cron: "0 0 * * *"
filters:
branches:
only:
- main
jobs:
- download_build:
revision: << pipeline.git.revision >>
- build_devtools_and_process_artifacts:
requires:
- download_build
- run_devtools_tests_for_versions:
requires:
- build_devtools_and_process_artifacts
matrix:
parameters:
version:
- "16.0"
- "16.5" # schedule package
- "16.8" # hooks
- "17.0"
- "18.0"
- run_devtools_e2e_tests_for_versions:
requires:
- build_devtools_and_process_artifacts
matrix:
parameters:
version:
- "16.0"
- "16.5" # schedule package
- "16.8" # hooks
- "17.0"
- "18.0"
# Used to publish a prerelease manually via the command line
publish_preleases:
when: << pipeline.parameters.prerelease_commit_sha >>
jobs:
- publish_prerelease:
name: Publish to Canary channel
commit_sha: << pipeline.parameters.prerelease_commit_sha >>
release_channel: stable
# The tags to use when publishing canaries. The main one we should
# always include is "canary" but we can use multiple (e.g. alpha,
# beta, rc). To declare multiple, use a comma-separated string, like
# this:
# dist_tag: "canary,alpha,beta,rc"
#
# TODO: We currently tag canaries with "next" in addition to "canary"
# because this used to be called the "next" channel and some
# downstream consumers might still expect that tag. We can remove this
# after some time has elapsed and the change has been communicated.
dist_tag: "canary,next,rc"
- publish_prerelease:
name: Publish to Experimental channel
requires:
# NOTE: Intentionally running these jobs sequentially because npm
# will sometimes fail if you try to concurrently publish two
# different versions of the same package, even if they use different
# dist tags.
- Publish to Canary channel
commit_sha: << pipeline.parameters.prerelease_commit_sha >>
release_channel: experimental
dist_tag: experimental
# Publishes on a cron schedule
publish_preleases_nightly:
unless: << pipeline.parameters.prerelease_commit_sha >>
triggers:
- schedule:
# At 10 minutes past 16:00 on Mon, Tue, Wed, Thu, and Fri
cron: "10 16 * * 1,2,3,4,5"
filters:
branches:
only:
- main
jobs:
- publish_prerelease:
name: Publish to Canary channel
commit_sha: << pipeline.git.revision >>
release_channel: stable
dist_tag: "canary,next,rc"
- publish_prerelease:
name: Publish to Experimental channel
requires:
# NOTE: Intentionally running these jobs sequentially because npm
# will sometimes fail if you try to concurrently publish two
# different versions of the same package, even if they use different
# dist tags.
- Publish to Canary channel
commit_sha: << pipeline.git.revision >>
release_channel: experimental
dist_tag: experimental

View File

@@ -12,13 +12,11 @@ const WARNING = 1;
const ERROR = 2;
module.exports = {
extends: ['prettier', 'plugin:jest/recommended'],
extends: ['prettier'],
// Stop ESLint from looking for a configuration file in parent folders
root: true,
reportUnusedDisableDirectives: true,
plugins: [
'babel',
'ft-flow',
@@ -247,7 +245,7 @@ module.exports = {
},
],
'no-shadow': ERROR,
'no-unused-vars': [ERROR, {args: 'none', ignoreRestSiblings: true}],
'no-unused-vars': [ERROR, {args: 'none'}],
'no-use-before-define': OFF,
'no-useless-concat': OFF,
quotes: [ERROR, 'single', {avoidEscape: true, allowTemplateLiterals: true}],
@@ -378,49 +376,16 @@ module.exports = {
files: ['**/__tests__/*.js'],
rules: {
// https://github.com/jest-community/eslint-plugin-jest
// Meh, who cares.
'jest/consistent-test-it': OFF,
// Meh, we have a lot of these, who cares.
'jest/no-alias-methods': OFF,
// We do conditions based on feature flags.
'jest/no-conditional-expect': OFF,
// We have our own assertion helpers.
'jest/expect-expect': OFF,
// Lame rule that fires in itRender helpers or in render methods.
'jest/no-standalone-expect': OFF,
'jest/no-focused-tests': ERROR,
'jest/valid-expect': ERROR,
'jest/valid-expect-in-promise': ERROR,
},
},
{
// Rules specific to test setup helper files.
files: [
'**/setupTests.js',
'**/setupEnv.js',
'**/jest/TestFlags.js',
'**/dom-event-testing-library/testHelpers.js',
'**/utils/ReactDOMServerIntegrationTestUtils.js',
'**/babel/transform-react-version-pragma.js',
'**/babel/transform-test-gate-pragma.js',
],
// disable no focused tests for test setup helper files even if they are inside __tests__ directory
files: ['**/setupTests.js'],
rules: {
// Some helpers intentionally focus tests.
'jest/no-focused-tests': OFF,
// Test fn helpers don't use static text names.
'jest/valid-title': OFF,
// We have our own assertion helpers.
'jest/expect-expect': OFF,
// Some helpers intentionally disable tests.
'jest/no-disabled-tests': OFF,
// Helpers export text function helpers.
'jest/no-export': OFF,
// The examples in comments trigger false errors.
'jest/no-commented-out-tests': OFF,
},
},
{
files: ['**/jest/TestFlags.js'],
rules: {
// The examples in comments trigger false errors.
'jest/no-commented-out-tests': OFF,
},
},
{
@@ -486,12 +451,7 @@ module.exports = {
},
},
{
files: [
'packages/react-devtools-extensions/**/*.js',
'packages/react-devtools-shared/src/hook.js',
'packages/react-devtools-shared/src/backend/console.js',
'packages/react-devtools-shared/src/backend/shared/DevToolsComponentStackFrame.js',
],
files: ['packages/react-devtools-extensions/**/*.js'],
globals: {
__IS_CHROME__: 'readonly',
__IS_FIREFOX__: 'readonly',
@@ -578,7 +538,6 @@ module.exports = {
TimeoutID: 'readonly',
WheelEventHandler: 'readonly',
FinalizationRegistry: 'readonly',
Omit: 'readonly',
spyOnDev: 'readonly',
spyOnDevAndProd: 'readonly',

View File

@@ -1,2 +1 @@
c998bb1ed4b3285398c9c7797135d3f060243c6a
fd2b3e13d330a4559f5aa21462e1cb2cbbcf144b

View File

@@ -1,21 +1,8 @@
name: (Runtime) Commit Artifacts for Meta WWW and fbsource V2
name: Commit Artifacts for Meta WWW and fbsource
on:
workflow_run:
workflows: ["(Runtime) Build and Test"]
types: [completed]
branches:
- main
workflow_dispatch:
inputs:
commit_sha:
required: false
type: string
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
push:
branches: [main, meta-www, meta-fbsource]
jobs:
download_artifacts:
@@ -59,27 +46,101 @@ jobs:
run: |
echo "www_branch_count=$(git ls-remote --heads origin "refs/heads/meta-www" | wc -l)" >> "$GITHUB_OUTPUT"
echo "fbsource_branch_count=$(git ls-remote --heads origin "refs/heads/meta-fbsource" | wc -l)" >> "$GITHUB_OUTPUT"
- uses: actions/setup-node@v4
- name: Download and unzip artifacts
uses: actions/github-script@v6
env:
CIRCLECI_TOKEN: ${{secrets.CIRCLECI_TOKEN_DIFFTRAIN}}
with:
node-version-file: '.nvmrc'
cache: yarn
cache-dependency-path: yarn.lock
- name: Restore cached node_modules
uses: actions/cache@v4
id: node_modules
with:
path: "**/node_modules"
key: ${{ runner.arch }}-${{ runner.os }}-modules-${{ hashFiles('yarn.lock', 'scripts/release/yarn.lock') }}
- run: yarn install --frozen-lockfile
name: yarn install (react)
- run: yarn install --frozen-lockfile
name: yarn install (scripts/release)
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 }}
- name: Display structure of build
run: ls -R build
script: |
const cp = require('child_process');
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
function execHelper(command, options, streamStdout = false) {
return new Promise((resolve, reject) => {
const proc = cp.exec(
command,
options,
(error, stdout) => (error ? reject(error) : resolve(stdout.trim())),
);
if (streamStdout) {
proc.stdout.pipe(process.stdout);
}
});
}
let artifactsUrl = null;
// This is a temporary, dirty hack to avoid needing a GitHub auth token in the circleci
// workflow to notify this GitHub action. Sorry!
let iter = 0;
spinloop: while (iter < 15) {
const res = await github.rest.repos.listCommitStatusesForRef({
owner: context.repo.owner,
repo: context.repo.repo,
ref: context.sha
});
for (const status of res.data) {
if (/process_artifacts_combined/.test(status.context)) {
switch (status.state) {
case 'pending': {
console.log(`${status.context} is still pending`);
break;
}
case 'failure':
case 'error': {
throw new Error(`${status.context} has failed or errored`);
}
case 'success': {
// The status does not include a build ID, but we can extract it
// from the URL. I couldn't find a better way to do this.
const ciBuildId = /\/facebook\/react\/([0-9]+)/.exec(
status.target_url,
)[1];
if (Number.parseInt(ciBuildId, 10) + '' === ciBuildId) {
artifactsUrl =
`https://circleci.com/api/v1.1/project/github/facebook/react/${ciBuildId}/artifacts`;
console.log(`Found artifactsUrl: ${artifactsUrl}`);
break spinloop;
} else {
throw new Error(`${ciBuildId} isn't a number`);
}
break;
}
default: {
throw new Error(`Unhandled status state: ${status.state}`);
break;
}
}
}
}
iter++;
console.log("Sleeping for 60s...");
await sleep(60_000);
}
if (artifactsUrl != null) {
const {CIRCLECI_TOKEN} = process.env;
const res = await fetch(artifactsUrl, {
headers: {
'Circle-Token': CIRCLECI_TOKEN
}
});
const data = await res.json();
if (!Array.isArray(data) && data.message != null) {
throw `CircleCI returned: ${data.message}`;
}
for (const artifact of data) {
if (artifact.path === 'build.tgz') {
console.log(`Downloading and unzipping ${artifact.url}`);
await execHelper(
`curl -L ${artifact.url} -H "Circle-Token: ${CIRCLECI_TOKEN}" | tar -xvz`
);
}
}
} else {
process.exitCode = 1;
}
- name: Strip @license from eslint plugin and react-refresh
run: |
sed -i -e 's/ @license React*//' \
@@ -134,9 +195,9 @@ jobs:
ls -R ./compiled-rn
- name: Add REVISION files
run: |
echo ${{ github.event.workflow_run.head_sha }} >> ./compiled/facebook-www/REVISION
echo ${{ github.sha }} >> ./compiled/facebook-www/REVISION
cp ./compiled/facebook-www/REVISION ./compiled/facebook-www/REVISION_TRANSFORMS
echo ${{ github.event.workflow_run.head_sha}} >> ./compiled-rn/facebook-fbsource/xplat/js/react-native-github/Libraries/Renderer/REVISION
echo ${{ github.sha }} >> ./compiled-rn/facebook-fbsource/xplat/js/react-native-github/Libraries/Renderer/REVISION
- name: "Get current version string"
id: get_current_version
run: |
@@ -149,11 +210,11 @@ jobs:
echo "current_version_classic=$VERSION_CLASSIC" >> "$GITHUB_OUTPUT"
echo "current_version_modern=$VERSION_MODERN" >> "$GITHUB_OUTPUT"
echo "current_version_rn=$VERSION_NATIVE_FB" >> "$GITHUB_OUTPUT"
- uses: actions/upload-artifact@v4
- uses: actions/upload-artifact@v3
with:
name: compiled
path: compiled/
- uses: actions/upload-artifact@v4
- uses: actions/upload-artifact@v3
with:
name: compiled-rn
path: compiled-rn/
@@ -168,7 +229,7 @@ jobs:
ref: builds/facebook-www
- name: Ensure clean directory
run: rm -rf compiled
- uses: actions/download-artifact@v4
- uses: actions/download-artifact@v3
with:
name: compiled
path: compiled/
@@ -189,18 +250,14 @@ jobs:
grep -rl "$CURRENT_VERSION_MODERN" ./compiled || echo "No files found with $CURRENT_VERSION_MODERN"
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
- name: Check if only the REVISION file has changed
id: check_should_commit
run: |
echo "Full git status"
git add .
git status
echo "===================="
if git status --porcelain | grep -qv '/REVISION'; then
echo "Changes detected"
echo "===== Changes ====="
git --no-pager diff -U0 | grep '^[+-]' | head -n 50
echo "==================="
echo "should_commit=true" >> "$GITHUB_OUTPUT"
else
echo "No Changes detected"
@@ -233,12 +290,12 @@ jobs:
uses: stefanzweifel/git-auto-commit-action@v4
with:
commit_message: |
${{ github.event.workflow_run.head_commit.message }}
${{ github.event.head_commit.message }}
DiffTrain build for [${{ github.event.workflow_run.head_sha }}](https://github.com/facebook/react/commit/${{ github.event.workflow_run.head_sha }})
DiffTrain build for [${{ github.sha }}](https://github.com/facebook/react/commit/${{ github.sha }})
branch: builds/facebook-www
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) }}
commit_user_name: ${{ github.actor }}
commit_user_email: ${{ github.actor }}@users.noreply.github.com
create_branch: true
commit_fbsource_artifacts:
@@ -251,7 +308,7 @@ jobs:
ref: builds/facebook-fbsource
- name: Ensure clean directory
run: rm -rf compiled-rn
- uses: actions/download-artifact@v4
- uses: actions/download-artifact@v3
with:
name: compiled-rn
path: compiled-rn/
@@ -265,19 +322,17 @@ jobs:
grep -rl "$CURRENT_VERSION" ./compiled-rn || echo "No files found with $CURRENT_VERSION"
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
- name: Check if only the REVISION file has changed
id: check_should_commit
run: |
echo "Full git status"
git add .
git --no-pager diff -U0 --cached | grep '^[+-]' | head -n 100
git status
echo "===================="
# Ignore REVISION or lines removing @generated headers.
if git diff --cached ':(exclude)*REVISION' | grep -vE "^(@@|diff|index|\-\-\-|\+\+\+|\- \* @generated SignedSource)" | grep "^[+-]" > /dev/null; then
echo "Checking for changes"
# Check if there are changes in the files other than REVISION or @generated headers
# We also filter out the file name lines with "---" and "+++".
if git diff -- . ':(exclude)*REVISION' | grep -vE "^(@@|diff|index|\-\-\-|\+\+\+|@generated SignedSource)" | grep "^[+-]" > /dev/null; then
echo "Changes detected"
echo "===== Changes ====="
git --no-pager diff --cached ':(exclude)*REVISION' | grep -vE "^(@@|diff|index|\-\-\-|\+\+\+|\- \* @generated SignedSource)" | grep "^[+-]" | head -n 50
echo "==================="
echo "should_commit=true" >> "$GITHUB_OUTPUT"
else
echo "No Changes detected"
@@ -293,108 +348,20 @@ jobs:
grep -rl "$LAST_VERSION" ./compiled-rn || echo "No files found with $LAST_VERSION"
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: steps.check_should_commit.outputs.should_commit == 'true'
run: |
echo ":"
git add .
- name: Signing files
if: steps.check_should_commit.outputs.should_commit == 'true'
uses: actions/github-script@v7
with:
script: |
// TODO: Move this to a script file.
// We currently can't call scripts from the repo because
// at this point in the workflow, we're on the compiled
// artifact branch (so the scripts don't exist).
// We can fix this with a composite action in the main repo.
// This script is duplicated above.
const fs = require('fs');
const crypto = require('crypto');
const {execSync} = require('child_process');
// TODO: when we move this to a script, we can use this from npm.
// Copy of signedsource since we can't install deps on this branch.
const GENERATED = '@' + 'generated';
const NEWTOKEN = '<<SignedSource::*O*zOeWoEQle#+L!plEphiEmie@IsG>>';
const PATTERN = new RegExp(`${GENERATED} (?:SignedSource<<([a-f0-9]{32})>>)`);
const TokenNotFoundError = new Error(
`SignedSource.signFile(...): Cannot sign file without token: ${NEWTOKEN}`
);
function hash(data, encoding) {
const md5sum = crypto.createHash('md5');
md5sum.update(data, encoding);
return md5sum.digest('hex');
}
const SignedSource = {
getSigningToken() {
return `${GENERATED} ${NEWTOKEN}`;
},
isSigned(data) {
return PATTERN.exec(data) != null;
},
signFile(data) {
if (!data.includes(NEWTOKEN)) {
if (SignedSource.isSigned(data)) {
// Signing a file that was previously signed.
data = data.replace(PATTERN, SignedSource.getSigningToken());
} else {
throw TokenNotFoundError;
}
}
return data.replace(NEWTOKEN, `SignedSource<<${hash(data, 'utf8')}>>`);
},
};
const directory = './compiled-rn';
console.log('Signing files in directory:', directory);
try {
const result = execSync(`git status --porcelain ${directory}`, {encoding: 'utf8'});
// Parse the git status output to get file paths
const files = result.split('\n').filter(file => file.endsWith('.js'));
if (files.length === 0) {
throw new Error(
'git status returned no files to sign. this job should not have run.'
);
} else {
files.forEach(line => {
const file = line.slice(3).trim();
if (file) {
console.log(' Signing file:', file);
const originalContents = fs.readFileSync(file, 'utf8');
const signedContents = SignedSource.signFile(
originalContents
// Need to add the header in, since it's not inserted at build time.
.replace(' */\n', ` * ${SignedSource.getSigningToken()}\n */\n`)
);
fs.writeFileSync(file, signedContents, 'utf8');
}
});
}
} catch (e) {
process.exitCode = 1;
console.error('Error signing files:', e);
}
- name: Will commit these changes
if: steps.check_should_commit.outputs.should_commit == 'true'
run: |
git add .
git status
echo ":"
git status -u
- name: Commit changes to branch
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 }}
${{ github.event.head_commit.message }}
DiffTrain build for commit https://github.com/facebook/react/commit/${{ github.event.workflow_run.head_sha }}.
DiffTrain build for commit https://github.com/facebook/react/commit/${{ github.sha }}.
branch: builds/facebook-fbsource
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) }}
commit_user_name: ${{ github.actor }}
commit_user_email: ${{ github.actor }}@users.noreply.github.com
create_branch: true

View File

@@ -1,18 +1,13 @@
name: (Compiler) Playground
name: Compiler Playground
on:
push:
branches: [main]
pull_request:
paths:
- compiler/**
- "compiler/**"
- .github/workflows/compiler-playground.yml
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
defaults:
run:
working-directory: compiler
@@ -25,8 +20,8 @@ jobs:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: yarn
node-version: 18.x
cache: "yarn"
cache-dependency-path: compiler/yarn.lock
- name: Restore cached node_modules
uses: actions/cache@v4

View File

@@ -1,4 +1,4 @@
name: (Compiler) Rust
name: React Compiler (Rust)
on:
push:
@@ -18,7 +18,6 @@ on:
env:
CARGO_TERM_COLOR: always
RUSTFLAGS: -Dwarnings
TZ: /usr/share/zoneinfo/America/Los_Angeles
defaults:
run:

View File

@@ -1,18 +1,13 @@
name: (Compiler) TypeScript
name: React Compiler (TypeScript)
on:
push:
branches: [main]
pull_request:
paths:
- compiler/**
- "compiler/**"
- .github/workflows/compiler-typescript.yml
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
defaults:
run:
working-directory: compiler
@@ -28,6 +23,25 @@ jobs:
- id: set-matrix
run: echo "matrix=$(find packages -mindepth 1 -maxdepth 1 -type d | sed 's!packages/!!g' | tr '\n' ',' | sed s/.$// | jq -Rsc '. / "," - [""]')" >> $GITHUB_OUTPUT
# Hardcoded to improve parallelism for babel-plugin-react-compiler
prettier:
name: Run prettier
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 18.x
cache: "yarn"
cache-dependency-path: compiler/yarn.lock
- name: Restore cached node_modules
uses: actions/cache@v4
with:
path: "**/node_modules"
key: ${{ runner.arch }}-${{ runner.os }}-modules-${{ hashFiles('compiler/**/yarn.lock') }}
- run: yarn install --frozen-lockfile
- run: yarn prettier:ci
# Hardcoded to improve parallelism
lint:
name: Lint babel-plugin-react-compiler
@@ -36,8 +50,8 @@ jobs:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: yarn
node-version: 18.x
cache: "yarn"
cache-dependency-path: compiler/yarn.lock
- name: Restore cached node_modules
uses: actions/cache@v4
@@ -55,8 +69,8 @@ jobs:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: yarn
node-version: 18.x
cache: "yarn"
cache-dependency-path: compiler/yarn.lock
- name: Restore cached node_modules
uses: actions/cache@v4
@@ -79,8 +93,8 @@ jobs:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: yarn
node-version: 18.x
cache: "yarn"
cache-dependency-path: compiler/yarn.lock
- name: Restore cached node_modules
uses: actions/cache@v4

View File

@@ -0,0 +1,205 @@
name: DevTools Check for bug repro
on:
issues:
types: [opened, edited]
issue_comment:
types: [created, edited]
jobs:
check-repro:
runs-on: ubuntu-latest
steps:
- uses: actions/github-script@v3
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const URL_REGEXP = /### Website or app[\r\n]+([^#]+)###/m;
const REPRO_STEPS_REGEXP = /### Repro steps[\r\n]+([^#]+)###/m;
const LABEL_NEEDS_MORE_INFORMATION = "Resolution: Needs More Information";
const LABEL_UNCONFIRMED = "Status: Unconfirmed";
function debug(...args) {
core.info(args.map(JSON.stringify).join(' '));
}
if (context.payload.comment) {
debug('Ignoring comment update.');
return;
}
const user = context.payload.sender.login;
const issue = context.payload.issue;
const body = issue.body;
const urlMatch = body.match(URL_REGEXP);
const reproStepsMatch = body.match(REPRO_STEPS_REGEXP);
const url = urlMatch !== null ? urlMatch[1].trim() : null;
const reproSteps = reproStepsMatch !== null ? reproStepsMatch[1].trim() : null;
if (!url || !reproSteps) {
debug('This issue is not a DevTools bug report.');
return;
}
debug(`found URL "${url}"`);
debug(`found repro steps "${reproSteps}"`);
async function createComment(comment) {
// Format
comment = comment
.split("\n")
.map((line) => line.trim())
.join("\n")
.trim();
await github.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: comment,
});
}
async function getGitHubActionComments() {
debug(`Loading existing comments...`);
const comments = await github.issues.listComments({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
});
return comments.data.filter(comment => {
debug(`comment by user: "${comment.user.login}"`);
return comment.user.login === 'github-actions[bot]';
});
}
async function getIssueLabels() {
const issues = await github.issues.listLabelsOnIssue({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
});
return issues.data;
}
async function updateIssue(state, assignees = []) {
await github.issues.update({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
state,
assignees,
});
}
async function closeWithComment(comment) {
if (issue.state !== 'open') {
debug(`Issue is not open`);
return;
}
const labels = await getIssueLabels();
const label = labels.find(label => label.name === LABEL_UNCONFIRMED);
if (!label) {
debug(`Issue was not opened via DevTools bug report template`);
return;
}
const comments = await getGitHubActionComments();
if (comments.length > 0) {
debug(`Already commented on issue; won't comment again`);
return;
}
debug(`Missing required information`);
await github.issues.addLabels({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
labels: [LABEL_NEEDS_MORE_INFORMATION],
});
await createComment(comment);
await updateIssue('closed', [user]);
}
async function openWithComment(comment) {
if (issue.state !== 'closed') {
debug(`Issue is already open`);
return;
}
const labels = await getIssueLabels();
const label = labels.find(label => label.name === LABEL_NEEDS_MORE_INFORMATION);
if (!label) {
debug(`Issue was not tagged as needs information`);
return;
}
const comments = await getGitHubActionComments();
if (comments.length === 0) {
debug(`Issue was closed by someone else; won't reopen`);
return;
}
debug(`Re-opening closed issue`);
await github.issues.removeLabel({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
name: LABEL_NEEDS_MORE_INFORMATION,
});
await createComment(comment);
await updateIssue('open');
}
const PROBABLY_NOT_A_URL_REGEX = /(^Chrome$|^Firefox$| Website)/i;
const COMMENT_HEADER = `
@${user}: We're sorry you've seen this error. ❤️
`.trim();
const COMMENT_FOOTER = `
Please help us by providing a link to a CodeSandbox (https://codesandbox.io/s/new), a repository on GitHub, or a minimal code example that reproduces the problem. (Screenshots or videos can also be helpful if they help provide context on how to repro the bug.)
Here are some tips for providing a minimal example: https://stackoverflow.com/help/mcve
Issues without repros are automatically closed but we will re-open if you update with repro info.
`.trim();
if (url.includes("localhost")) {
closeWithComment(`
${COMMENT_HEADER}
Unfortunately the URL you provided ("localhost") is not publicly accessible. (This means that we will not be able to reproduce the problem you're reporting.)
${COMMENT_FOOTER}
`);
} else if (url.length < 10 || url.match(PROBABLY_NOT_A_URL_REGEX)) {
closeWithComment(`
${COMMENT_HEADER}
It looks like you forgot to specify a valid URL. (This means that we will not be able to reproduce the problem you're reporting.)
${COMMENT_FOOTER}
`);
} else if (reproSteps.length < 25) {
closeWithComment(`
${COMMENT_HEADER}
Unfortunately, it doesn't look like this issue has enough info for one of us to reproduce and fix it though.
${COMMENT_FOOTER}
`);
} else {
openWithComment(`
Thank you for providing repro steps! Re-opening issue now for triage.
`);
}

View File

@@ -1,173 +0,0 @@
name: (DevTools) Regression Tests
on:
schedule:
- cron: 0 0 * * *
workflow_dispatch:
inputs:
commit_sha:
required: false
type: string
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
jobs:
download_build:
name: Download base build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: yarn
cache-dependency-path: yarn.lock
- name: Restore cached node_modules
uses: actions/cache@v4
id: node_modules
with:
path: "**/node_modules"
key: ${{ runner.arch }}-${{ runner.os }}-modules-${{ hashFiles('yarn.lock', 'scripts/release/yarn.lock') }}
- run: yarn install --frozen-lockfile
- run: yarn install --frozen-lockfile
working-directory: scripts/release
- name: Download react-devtools artifacts for base revision
run: |
git fetch origin main
GH_TOKEN=${{ github.token }} scripts/release/download-experimental-build.js --commit=${{ inputs.commit_sha || '$(git rev-parse origin/main)' }}
- name: Display structure of build
run: ls -R build
- name: Archive build
uses: actions/upload-artifact@v4
with:
name: build
path: build
build_devtools_and_process_artifacts:
name: Build DevTools and process artifacts
needs: download_build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: yarn
cache-dependency-path: yarn.lock
- name: Restore cached node_modules
uses: actions/cache@v4
id: node_modules
with:
path: "**/node_modules"
key: ${{ runner.arch }}-${{ runner.os }}-modules-${{ hashFiles('yarn.lock') }}
- run: yarn install --frozen-lockfile
- name: Restore archived build
uses: actions/download-artifact@v4
with:
name: build
path: build
- run: ./scripts/ci/pack_and_store_devtools_artifacts.sh
env:
RELEASE_CHANNEL: experimental
- name: Display structure of build
run: ls -R build
- name: Archive devtools build
uses: actions/upload-artifact@v4
with:
name: react-devtools
path: build/devtools.tgz
# Simplifies getting the extension for local testing
- name: Archive chrome extension
uses: actions/upload-artifact@v4
with:
name: react-devtools-chrome-extension
path: build/devtools/chrome-extension.zip
- name: Archive firefox extension
uses: actions/upload-artifact@v4
with:
name: react-devtools-firefox-extension
path: build/devtools/firefox-extension.zip
run_devtools_tests_for_versions:
name: Run DevTools tests for versions
needs: build_devtools_and_process_artifacts
runs-on: ubuntu-latest
strategy:
matrix:
version:
- "16.0"
- "16.5" # schedule package
- "16.8" # hooks
- "17.0"
- "18.0"
continue-on-error: true
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: yarn
cache-dependency-path: yarn.lock
- name: Restore cached node_modules
uses: actions/cache@v4
id: node_modules
with:
path: "**/node_modules"
key: ${{ runner.arch }}-${{ runner.os }}-modules-${{ hashFiles('yarn.lock') }}
- run: yarn install --frozen-lockfile
- name: Restore all archived build artifacts
uses: actions/download-artifact@v4
- name: Display structure of build
run: ls -R build
- run: ./scripts/ci/download_devtools_regression_build.js ${{ matrix.version }} --replaceBuild
- run: node ./scripts/jest/jest-cli.js --build --project devtools --release-channel=experimental --reactVersion ${{ matrix.version }} --ci
run_devtools_e2e_tests_for_versions:
name: Run DevTools e2e tests for versions
needs: build_devtools_and_process_artifacts
runs-on: ubuntu-latest
strategy:
matrix:
version:
- "16.0"
- "16.5" # schedule package
- "16.8" # hooks
- "17.0"
- "18.0"
continue-on-error: true
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: yarn
cache-dependency-path: yarn.lock
- name: Restore cached node_modules
uses: actions/cache@v4
id: node_modules
with:
path: "**/node_modules"
key: ${{ runner.arch }}-${{ runner.os }}-modules-${{ hashFiles('yarn.lock') }}
- run: yarn install --frozen-lockfile
- name: Restore all archived build artifacts
uses: actions/download-artifact@v4
- name: Display structure of build
run: ls -R build
- name: Playwright install deps
run: |
npx playwright install
sudo npx playwright install-deps
- run: ./scripts/ci/download_devtools_regression_build.js ${{ matrix.version }}
- run: ls -R build-regression
- run: ./scripts/ci/run_devtools_e2e_tests.js ${{ matrix.version }}
env:
RELEASE_CHANNEL: experimental
- name: Cleanup build regression folder
run: rm -r ./build-regression
- uses: actions/upload-artifact@v4
with:
name: screenshots
path: ./tmp/screenshots

View File

@@ -1,19 +1,20 @@
name: (Runtime) Fuzz tests
name: facebook/react/fuzz_tests
on:
schedule:
- cron: 0 * * * *
- cron: 0 * * * *
push:
branches:
- main
workflow_dispatch:
env:
TZ: /usr/share/zoneinfo/America/Los_Angeles
inputs:
prerelease_commit_sha:
required: false
jobs:
test_fuzz:
if: inputs.prerelease_commit_sha == ''
runs-on: ubuntu-latest
env:
TZ: "/usr/share/zoneinfo/America/Los_Angeles"
steps:
- uses: actions/checkout@v4.1.0
- uses: actions/setup-node@v4

View File

@@ -1,611 +0,0 @@
name: (Runtime) Build and Test
on:
push:
branches: [main]
pull_request:
paths-ignore:
- compiler/**
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
jobs:
# ----- FLOW -----
discover_flow_inline_configs:
name: Discover flow inline configs
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.set-matrix.outputs.result }}
steps:
- uses: actions/checkout@v4
- uses: actions/github-script@v7
id: set-matrix
with:
script: |
const inlinedHostConfigs = require('./scripts/shared/inlinedHostConfigs.js');
return inlinedHostConfigs.map(config => config.shortName);
flow:
name: Flow check ${{ matrix.flow_inline_config_shortname }}
needs: discover_flow_inline_configs
runs-on: ubuntu-latest
continue-on-error: true
strategy:
matrix:
flow_inline_config_shortname: ${{ fromJSON(needs.discover_flow_inline_configs.outputs.matrix) }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: yarn
cache-dependency-path: yarn.lock
- name: Restore cached node_modules
uses: actions/cache@v4
id: node_modules
with:
path: "**/node_modules"
key: ${{ runner.arch }}-${{ runner.os }}-modules-${{ hashFiles('yarn.lock') }}
- run: yarn install --frozen-lockfile
- run: node ./scripts/tasks/flow-ci ${{ matrix.flow_inline_config_shortname }}
# ----- FIZZ -----
check_generated_fizz_runtime:
name: Confirm generated inline Fizz runtime is up to date
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: yarn
cache-dependency-path: yarn.lock
- name: Restore cached node_modules
uses: actions/cache@v4
id: node_modules
with:
path: "**/node_modules"
key: ${{ runner.arch }}-${{ runner.os }}-modules-${{ hashFiles('yarn.lock') }}
- run: yarn install --frozen-lockfile
- run: |
yarn generate-inline-fizz-runtime
git diff --quiet || (echo "There was a change to the Fizz runtime. Run `yarn generate-inline-fizz-runtime` and check in the result." && false)
# ----- FEATURE FLAGS -----
flags:
name: Check flags
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: yarn
cache-dependency-path: yarn.lock
- name: Restore cached node_modules
uses: actions/cache@v4
id: node_modules
with:
path: "**/node_modules"
key: ${{ runner.arch }}-${{ runner.os }}-modules-${{ hashFiles('yarn.lock') }}
- run: yarn install --frozen-lockfile
- run: yarn flags
# ----- TESTS -----
test:
name: yarn test ${{ matrix.params }} (Shard ${{ matrix.shard }})
runs-on: ubuntu-latest
strategy:
matrix:
params:
- "-r=stable --env=development"
- "-r=stable --env=production"
- "-r=experimental --env=development"
- "-r=experimental --env=production"
- "-r=www-classic --env=development --variant=false"
- "-r=www-classic --env=production --variant=false"
- "-r=www-classic --env=development --variant=true"
- "-r=www-classic --env=production --variant=true"
- "-r=www-modern --env=development --variant=false"
- "-r=www-modern --env=production --variant=false"
- "-r=www-modern --env=development --variant=true"
- "-r=www-modern --env=production --variant=true"
- "-r=xplat --env=development --variant=false"
- "-r=xplat --env=development --variant=true"
- "-r=xplat --env=production --variant=false"
- "-r=xplat --env=production --variant=true"
# TODO: Test more persistent configurations?
- "-r=stable --env=development --persistent"
- "-r=experimental --env=development --persistent"
shard:
- 1/5
- 2/5
- 3/5
- 4/5
- 5/5
continue-on-error: true
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: yarn
cache-dependency-path: yarn.lock
- name: Restore cached node_modules
uses: actions/cache@v4
id: node_modules
with:
path: "**/node_modules"
key: ${{ runner.arch }}-${{ runner.os }}-modules-${{ hashFiles('yarn.lock') }}
- run: yarn install --frozen-lockfile
- run: yarn test ${{ matrix.params }} --ci --shard=${{ matrix.shard }}
# ----- BUILD -----
build_and_lint:
name: yarn build and lint
runs-on: ubuntu-latest
strategy:
matrix:
# yml is dumb. update the --total arg to yarn build if you change the number of workers
worker_id: [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19]
release_channel: [stable, experimental]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: yarn
cache-dependency-path: yarn.lock
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: 11.0.22
- name: Restore cached node_modules
uses: actions/cache@v4
id: node_modules
with:
path: "**/node_modules"
key: ${{ runner.arch }}-${{ runner.os }}-modules-${{ hashFiles('yarn.lock') }}
- run: yarn install --frozen-lockfile
- run: yarn build --index=${{ matrix.worker_id }} --total=20 --r=${{ matrix.release_channel }} --ci
env:
CI: github
RELEASE_CHANNEL: ${{ matrix.release_channel }}
NODE_INDEX: ${{ matrix.worker_id }}
- name: Lint build
run: yarn lint-build
- name: Display structure of build
run: ls -R build
- name: Archive build
uses: actions/upload-artifact@v4
with:
name: _build_${{ matrix.worker_id }}_${{ matrix.release_channel }}
path: build
test_build:
name: yarn test-build
needs: build_and_lint
strategy:
matrix:
test_params: [
# Intentionally passing these as strings instead of creating a
# separate parameter per CLI argument, since it's easier to
# control/see which combinations we want to run.
-r=stable --env=development,
-r=stable --env=production,
-r=experimental --env=development,
-r=experimental --env=production,
# Dev Tools
--project=devtools -r=experimental,
# TODO: Update test config to support www build tests
# - "-r=www-classic --env=development --variant=false"
# - "-r=www-classic --env=production --variant=false"
# - "-r=www-classic --env=development --variant=true"
# - "-r=www-classic --env=production --variant=true"
# - "-r=www-modern --env=development --variant=false"
# - "-r=www-modern --env=production --variant=false"
# - "-r=www-modern --env=development --variant=true"
# - "-r=www-modern --env=production --variant=true"
# TODO: Update test config to support xplat build tests
# - "-r=xplat --env=development --variant=false"
# - "-r=xplat --env=development --variant=true"
# - "-r=xplat --env=production --variant=false"
# - "-r=xplat --env=production --variant=true"
# TODO: Test more persistent configurations?
]
shard:
- 1/3
- 2/3
- 3/3
continue-on-error: true
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: yarn
cache-dependency-path: yarn.lock
- name: Restore cached node_modules
uses: actions/cache@v4
id: node_modules
with:
path: "**/node_modules"
key: ${{ runner.arch }}-${{ runner.os }}-modules-${{ hashFiles('yarn.lock') }}
- run: yarn install --frozen-lockfile
- name: Restore archived build
uses: actions/download-artifact@v4
with:
pattern: _build_*
path: build
merge-multiple: true
- name: Display structure of build
run: ls -R build
- run: yarn test --build ${{ matrix.test_params }} --shard=${{ matrix.shard }} --ci
process_artifacts_combined:
name: Process artifacts combined
needs: build_and_lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: yarn
cache-dependency-path: yarn.lock
- name: Restore cached node_modules
uses: actions/cache@v4
id: node_modules
with:
path: "**/node_modules"
key: ${{ runner.arch }}-${{ runner.os }}-modules-${{ hashFiles('yarn.lock') }}
- run: yarn install --frozen-lockfile
- name: Restore archived build
uses: actions/download-artifact@v4
with:
pattern: _build_*
path: build
merge-multiple: true
- name: Display structure of build
run: ls -R build
- run: echo ${{ github.sha }} >> build/COMMIT_SHA
- name: Scrape warning messages
run: |
mkdir -p ./build/__test_utils__
node ./scripts/print-warnings/print-warnings.js > build/__test_utils__/ReactAllWarnings.js
# Compress build directory into a single tarball for easy download
- run: tar -zcvf ./build.tgz ./build
# TODO: Migrate scripts to use `build` directory instead of `build2`
- run: cp ./build.tgz ./build2.tgz
- name: Archive build artifacts
uses: actions/upload-artifact@v4
with:
name: artifacts_combined
path: |
./build.tgz
./build2.tgz
check_error_codes:
name: Search build artifacts for unminified errors
needs: build_and_lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: yarn
cache-dependency-path: yarn.lock
- name: Restore cached node_modules
uses: actions/cache@v4
id: node_modules
with:
path: "**/node_modules"
key: ${{ runner.arch }}-${{ runner.os }}-modules-${{ hashFiles('yarn.lock') }}
- run: yarn install --frozen-lockfile
- name: Restore archived build
uses: actions/download-artifact@v4
with:
pattern: _build_*
path: build
merge-multiple: true
- name: Display structure of build
run: ls -R build
- name: Search build artifacts for unminified errors
run: |
yarn extract-errors
git diff --quiet || (echo "Found unminified errors. Either update the error codes map or disable error minification for the affected build, if appropriate." && false)
check_release_dependencies:
name: Check release dependencies
needs: build_and_lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: yarn
cache-dependency-path: yarn.lock
- name: Restore cached node_modules
uses: actions/cache@v4
id: node_modules
with:
path: "**/node_modules"
key: ${{ runner.arch }}-${{ runner.os }}-modules-${{ hashFiles('yarn.lock') }}
- run: yarn install --frozen-lockfile
- name: Restore archived build
uses: actions/download-artifact@v4
with:
pattern: _build_*
path: build
merge-multiple: true
- name: Display structure of build
run: ls -R build
- run: yarn check-release-dependencies
RELEASE_CHANNEL_stable_yarn_test_dom_fixtures:
name: Check fixtures DOM (stable)
needs: build_and_lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: yarn
cache-dependency-path: yarn.lock
- name: Restore cached node_modules
uses: actions/cache@v4
id: node_modules
with:
path: "**/node_modules"
key: v2-yarn_cache_fixtures_dom-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
- run: yarn install --frozen-lockfile
- run: yarn install --frozen-lockfile --cache-folder ~/.cache/yarn
working-directory: fixtures/dom
- name: Restore archived build
uses: actions/download-artifact@v4
with:
pattern: _build_*
path: build
merge-multiple: true
- name: Display structure of build
run: ls -R build
- name: Run DOM fixture tests
run: |
yarn predev
yarn test
working-directory: fixtures/dom
env:
RELEASE_CHANNEL: stable
# ----- FLIGHT -----
run_fixtures_flight_tests:
name: Run fixtures Flight tests
needs: build_and_lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: yarn
cache-dependency-path: yarn.lock
# Fixture copies some built packages from the workroot after install.
# That means dependencies of the built packages are not installed.
# We need to install dependencies of the workroot to fulfill all dependency constraints
- name: Restore cached node_modules
uses: actions/cache@v4
id: node_modules
with:
path: "**/node_modules"
key: v2-yarn_cache_fixtures_flight-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
- run: yarn install --frozen-lockfile
- name: Restore archived build
uses: actions/download-artifact@v4
with:
pattern: _build_*
path: build
merge-multiple: true
- name: Display structure of build
run: ls -R build
- name: Install fixture dependencies
working-directory: fixtures/flight
run: |
yarn install --frozen-lockfile --cache-folder ~/.cache/yarn
if [ $? -ne 0 ]; then
yarn install --frozen-lockfile --cache-folder ~/.cache/yarn
fi
- name: Playwright install deps
working-directory: fixtures/flight
run: |
npx playwright install
sudo npx playwright install-deps
- name: Run tests
working-directory: fixtures/flight
run: yarn test
env:
# Otherwise the webserver is a blackbox
DEBUG: pw:webserver
- name: Archive Flight fixture artifacts
uses: actions/upload-artifact@v4
with:
name: flight-playwright-report
path: fixtures/flight/playwright-report
- name: Archive Flight fixture artifacts
uses: actions/upload-artifact@v4
with:
name: flight-test-results
path: fixtures/flight/test-results
# ----- DEVTOOLS -----
build_devtools_and_process_artifacts:
name: Build DevTools and process artifacts
needs: build_and_lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: yarn
cache-dependency-path: yarn.lock
- name: Restore cached node_modules
uses: actions/cache@v4
id: node_modules
with:
path: "**/node_modules"
key: ${{ runner.arch }}-${{ runner.os }}-modules-${{ hashFiles('yarn.lock') }}
- run: yarn install --frozen-lockfile
- name: Restore archived build
uses: actions/download-artifact@v4
with:
pattern: _build_*
path: build
merge-multiple: true
- run: ./scripts/ci/pack_and_store_devtools_artifacts.sh
env:
RELEASE_CHANNEL: experimental
- name: Display structure of build
run: ls -R build
- name: Archive devtools build
uses: actions/upload-artifact@v4
with:
name: react-devtools
path: build/devtools.tgz
# Simplifies getting the extension for local testing
- name: Archive chrome extension
uses: actions/upload-artifact@v4
with:
name: react-devtools-chrome-extension
path: build/devtools/chrome-extension.zip
- name: Archive firefox extension
uses: actions/upload-artifact@v4
with:
name: react-devtools-firefox-extension
path: build/devtools/firefox-extension.zip
run_devtools_e2e_tests:
name: Run DevTools e2e tests
needs: build_devtools_and_process_artifacts
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: yarn
cache-dependency-path: yarn.lock
- name: Restore cached node_modules
uses: actions/cache@v4
id: node_modules
with:
path: "**/node_modules"
key: ${{ runner.arch }}-${{ runner.os }}-modules-${{ hashFiles('yarn.lock') }}
- run: yarn install --frozen-lockfile
- name: Restore archived build
uses: actions/download-artifact@v4
with:
pattern: _build_*
path: build
merge-multiple: true
- run: |
npx playwright install
sudo npx playwright install-deps
- run: ./scripts/ci/run_devtools_e2e_tests.js
env:
RELEASE_CHANNEL: experimental
# ----- SIZEBOT -----
download_base_build_for_sizebot:
if: ${{ github.event_name == 'pull_request' && github.ref_name != 'main' }}
name: Download base build for sizebot
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: yarn
cache-dependency-path: yarn.lock
- name: Restore cached node_modules
uses: actions/cache@v4
id: node_modules
with:
path: "**/node_modules"
key: ${{ runner.arch }}-${{ runner.os }}-modules-${{ hashFiles('yarn.lock', 'scripts/release/yarn.lock') }}
- run: yarn install --frozen-lockfile
- run: yarn install --frozen-lockfile
working-directory: scripts/release
- name: Download artifacts for base revision
run: |
git fetch origin main
GH_TOKEN=${{ github.token }} scripts/release/download-experimental-build.js --commit=$(git rev-parse origin/main)
mv ./build ./base-build
# TODO: The `download-experimental-build` script copies the npm
# packages into the `node_modules` directory. This is a historical
# quirk of how the release script works. Let's pretend they
# don't exist.
- name: Delete extraneous files
run: rm -rf ./base-build/node_modules
- name: Display structure of base-build
run: ls -R base-build
- name: Archive base-build
uses: actions/upload-artifact@v4
with:
name: base-build
path: base-build
sizebot:
name: Run sizebot
needs: [build_and_lint, download_base_build_for_sizebot]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: yarn
cache-dependency-path: yarn.lock
- name: Restore cached node_modules
uses: actions/cache@v4
id: node_modules
with:
path: "**/node_modules"
key: ${{ runner.arch }}-${{ runner.os }}-modules-${{ hashFiles('yarn.lock') }}
- run: yarn install --frozen-lockfile
- name: Restore archived build for PR
uses: actions/download-artifact@v4
with:
pattern: _build_*
path: build
merge-multiple: true
- name: Scrape warning messages
run: |
mkdir -p ./build/__test_utils__
node ./scripts/print-warnings/print-warnings.js > build/__test_utils__/ReactAllWarnings.js
- name: Display structure of build for PR
run: ls -R build
- name: Restore archived base-build from origin/main
uses: actions/download-artifact@v4
with:
name: base-build
path: base-build
- name: Display structure of base-build from origin/main
run: ls -R base-build
- run: echo ${{ github.sha }} >> build/COMMIT_SHA
- run: node ./scripts/tasks/danger
- name: Archive sizebot results
uses: actions/upload-artifact@v4
with:
name: sizebot-message
path: sizebot-message.md

View File

@@ -1,50 +0,0 @@
name: (Runtime) 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 }}
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: yarn.lock
- name: Restore cached node_modules
uses: actions/cache@v4
id: node_modules
with:
path: "**/node_modules"
key: ${{ runner.arch }}-${{ runner.os }}-modules-${{ hashFiles('yarn.lock', 'scripts/release/yarn.lock') }}
- run: yarn install --frozen-lockfile
- run: yarn install --frozen-lockfile
working-directory: scripts/release
- run: |
scripts/release/prepare-release-from-ci.js --skipTests -r ${{ inputs.release_channel }} --commit=${{ inputs.commit_sha }}
cp ./scripts/release/ci-npmrc ~/.npmrc
scripts/release/publish.js --ci --tags ${{ inputs.dist_tag }}

View File

@@ -1,47 +0,0 @@
name: (Runtime) Publish Prereleases Manual
on:
workflow_dispatch:
inputs:
prerelease_commit_sha:
required: true
env:
TZ: /usr/share/zoneinfo/America/Los_Angeles
jobs:
publish_prerelease_canary:
name: Publish to Canary channel
uses: facebook/react/.github/workflows/runtime_prereleases.yml@main
with:
commit_sha: ${{ inputs.prerelease_commit_sha }}
release_channel: stable
# The tags to use when publishing canaries. The main one we should
# always include is "canary" but we can use multiple (e.g. alpha,
# beta, rc). To declare multiple, use a comma-separated string, like
# this:
# dist_tag: "canary,alpha,beta,rc"
#
# TODO: We currently tag canaries with "next" in addition to "canary"
# because this used to be called the "next" channel and some
# downstream consumers might still expect that tag. We can remove this
# after some time has elapsed and the change has been communicated.
dist_tag: canary,next,rc
secrets:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
publish_prerelease_experimental:
name: Publish to Experimental channel
uses: facebook/react/.github/workflows/runtime_prereleases.yml@main
# NOTE: Intentionally running these jobs sequentially because npm
# will sometimes fail if you try to concurrently publish two
# different versions of the same package, even if they use different
# dist tags.
needs: publish_prerelease_canary
with:
commit_sha: ${{ inputs.prerelease_commit_sha }}
release_channel: experimental
dist_tag: experimental
secrets:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}

View File

@@ -1,35 +0,0 @@
name: (Runtime) 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_canary:
name: Publish to Canary channel
uses: facebook/react/.github/workflows/runtime_prereleases.yml@main
with:
commit_sha: ${{ github.sha }}
release_channel: stable
dist_tag: canary,next,rc
secrets:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
publish_prerelease_experimental:
name: Publish to Experimental channel
uses: facebook/react/.github/workflows/runtime_prereleases.yml@main
# NOTE: Intentionally running these jobs sequentially because npm
# will sometimes fail if you try to concurrently publish two
# different versions of the same package, even if they use different
# dist tags.
needs: publish_prerelease_canary
with:
commit_sha: ${{ github.sha }}
release_channel: experimental
dist_tag: experimental
secrets:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}

View File

@@ -1,84 +0,0 @@
name: (Shared) Lint
on:
push:
branches: [main]
pull_request:
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
jobs:
prettier:
name: Run prettier
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: yarn
cache-dependency-path: yarn.lock
- name: Restore cached node_modules
uses: actions/cache@v4
with:
path: "**/node_modules"
key: ${{ runner.arch }}-${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }}
- run: yarn install --frozen-lockfile
- run: yarn prettier-check
eslint:
name: Run eslint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: yarn
cache-dependency-path: yarn.lock
- name: Restore cached node_modules
uses: actions/cache@v4
with:
path: "**/node_modules"
key: ${{ runner.arch }}-${{ runner.os }}-modules-${{ hashFiles('yarn.lock') }}
- run: yarn install --frozen-lockfile
- run: node ./scripts/tasks/eslint
check_license:
name: Check license
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: yarn
cache-dependency-path: yarn.lock
- name: Restore cached node_modules
uses: actions/cache@v4
with:
path: "**/node_modules"
key: ${{ runner.arch }}-${{ runner.os }}-modules-${{ hashFiles('yarn.lock') }}
- run: yarn install --frozen-lockfile
- run: ./scripts/ci/check_license.sh
test_print_warnings:
name: Test print warnings
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: yarn
cache-dependency-path: yarn.lock
- name: Restore cached node_modules
uses: actions/cache@v4
with:
path: "**/node_modules"
key: ${{ runner.arch }}-${{ runner.os }}-modules-${{ hashFiles('yarn.lock') }}
- run: yarn install --frozen-lockfile
- run: ./scripts/ci/test_print_warnings.sh

View File

@@ -1,13 +1,9 @@
# Configuration for stale action workflow - https://github.com/actions/stale
name: (Shared) Manage stale issues and PRs
name: 'Manage stale issues and PRs'
on:
schedule:
# Run hourly
- cron: '0 * * * *'
workflow_dispatch:
env:
TZ: /usr/share/zoneinfo/America/Los_Angeles
jobs:
stale:

2
.nvmrc
View File

@@ -1 +1 @@
v18.20.1
v18.20.0

View File

@@ -1,6 +1,6 @@
# react runtime
build
compiler
packages/react-devtools-core/dist
packages/react-devtools-extensions/chrome/build
packages/react-devtools-extensions/firefox/build
@@ -14,27 +14,3 @@ packages/react-devtools-shared/src/hooks/__tests__/__source__/__untransformed__/
packages/react-devtools-shell/dist
packages/react-devtools-timeline/dist
packages/react-devtools-timeline/static
# react compiler
compiler/**/dist
compiler/**/__tests__/fixtures/**/*.expect.md
compiler/**/.next
# contains invalid graphql`...` which results in a promise rejection error from `yarn prettier-all`.
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-kitchensink.js
compiler/crates
compiler/apps/playground/public
compiler/**/LICENSE
compiler/*.md*
compiler/*.json
compiler/*.css
compiler/*.webmanifest
compiler/*.map
compiler/*.sh
compiler/*.txt
compiler/*.ico
compiler/*.svg
compiler/*.lock
compiler/*.toml

View File

@@ -1,23 +1,19 @@
'use strict';
const {esNextPaths} = require('./scripts/shared/pathsByLanguageVersion');
const {
esNextPaths,
typescriptPaths,
} = require('./scripts/shared/pathsByLanguageVersion');
module.exports = {
plugins: ['prettier-plugin-hermes-parser'],
bracketSpacing: false,
singleQuote: true,
bracketSameLine: true,
trailingComma: 'es5',
printWidth: 80,
parser: 'hermes',
parser: 'flow',
arrowParens: 'avoid',
overrides: [
{
files: ['*.code-workspace'],
options: {
parser: 'json-stringify',
},
},
{
files: esNextPaths,
options: {
@@ -25,7 +21,7 @@ module.exports = {
},
},
{
files: ['*.ts', '*.tsx'],
files: typescriptPaths,
options: {
trailingComma: 'all',
parser: 'typescript',

View File

@@ -1,4 +1,4 @@
# [React](https://react.dev/) &middot; [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/facebook/react/blob/main/LICENSE) [![npm version](https://img.shields.io/npm/v/react.svg?style=flat)](https://www.npmjs.com/package/react) [![(Runtime) Build and Test](https://github.com/facebook/react/actions/workflows/runtime_build_and_test.yml/badge.svg)](https://github.com/facebook/react/actions/workflows/runtime_build_and_test.yml) [![(Compiler) TypeScript](https://github.com/facebook/react/actions/workflows/compiler_typescript.yml/badge.svg?branch=main)](https://github.com/facebook/react/actions/workflows/compiler_typescript.yml) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://legacy.reactjs.org/docs/how-to-contribute.html#your-first-pull-request)
# [React](https://react.dev/) &middot; [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/facebook/react/blob/main/LICENSE) [![npm version](https://img.shields.io/npm/v/react.svg?style=flat)](https://www.npmjs.com/package/react) [![CircleCI Status](https://circleci.com/gh/facebook/react.svg?style=shield)](https://circleci.com/gh/facebook/react) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://legacy.reactjs.org/docs/how-to-contribute.html#your-first-pull-request)
React is a JavaScript library for building user interfaces.
@@ -18,7 +18,7 @@ React has been designed for gradual adoption from the start, and **you can use a
## Documentation
You can find the React documentation [on the website](https://react.dev/).
You can find the React documentation [on the website](https://react.dev/).
Check out the [Getting Started](https://react.dev/learn) page for a quick overview.
@@ -55,7 +55,7 @@ root.render(<HelloMessage name="Taylor" />);
This example will render "Hello Taylor" into a container on the page.
You'll notice that we used an HTML-like syntax; [we call it JSX](https://react.dev/learn#writing-markup-with-jsx). JSX is not required to use React, but it makes code more readable, and writing it feels like writing HTML.
You'll notice that we used an HTML-like syntax; [we call it JSX](https://react.dev/learn#writing-markup-with-jsx). JSX is not required to use React, but it makes code more readable, and writing it feels like writing HTML.
## Contributing

View File

@@ -25,7 +25,7 @@ const ReactVersion = '19.0.0';
// choose to change it to "alpha", "beta", "rc", etc.
//
// It only affects the label used in the version string. To customize the
// npm dist tags used during publish, refer to .github/workflows/runtime_prereleases_*.yml.
// npm dist tags used during publish, refer to .circleci/config.yml.
const canaryChannelLabel = 'rc';
// If the canaryChannelLabel is "rc", the build pipeline will use this to build

View File

@@ -43,12 +43,6 @@ module.exports = {
"multiline-comment-style": ["error", "starred-block"],
/**
* We sometimes need to check for control characters in regexes for things like preserving input
* strings
*/
"no-control-regex": "off",
"@typescript-eslint/no-empty-function": "off",
/*
@@ -88,7 +82,7 @@ module.exports = {
],
"@typescript-eslint/array-type": ["error", { default: "generic" }],
"@typescript-eslint/triple-slash-reference": "off",
"@typescript-eslint/no-var-requires": "off",
"@typescript-eslint/no-var-requires": "off"
},
parser: "@typescript-eslint/parser",
plugins: ["@typescript-eslint"],

21
compiler/.prettierignore Normal file
View File

@@ -0,0 +1,21 @@
**/dist
**/__tests__/fixtures/**/*.expect.md
**/__tests__/fixtures/**/*.flow.js
**/.next
crates
apps/playground/public
**/LICENSE
.*
*.md*
*.json
*.css
*.webmanifest
*.map
*.sh
*.txt
*.ico
*.svg
*.lock
*.toml

9
compiler/.prettierrc.js Normal file
View File

@@ -0,0 +1,9 @@
const config = {
requirePragma: false,
parser: "babel-ts",
semi: true,
singleQuote: false,
trailingComma: "es5"
}
module.exports = config;

8
compiler/.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,8 @@
{
"editor.formatOnSave": true,
"[typescript][typescriptreact]": {
"editor.codeActionsOnSave": {
"source.organizeImports": "explicit"
}
}
}

View File

@@ -5,8 +5,8 @@
* LICENSE file in the root directory of this source tree.
*/
import {expect, test} from '@playwright/test';
import {encodeStore, type Store} from '../../lib/stores';
import { expect, test } from "@playwright/test";
import { encodeStore, type Store } from "../../lib/stores";
const STORE: Store = {
source: `export default function TestComponent({ x }) {
@@ -17,33 +17,33 @@ const STORE: Store = {
const HASH = encodeStore(STORE);
function concat(data: Array<string>): string {
return data.join('');
return data.join("");
}
test('editor should compile successfully', async ({page}) => {
await page.goto(`/#${HASH}`, {waitUntil: 'networkidle'});
test("editor should compile successfully", async ({ page }) => {
await page.goto(`/#${HASH}`, { waitUntil: "networkidle" });
await page.screenshot({
fullPage: true,
path: 'test-results/00-on-networkidle.png',
path: "test-results/00-on-networkidle.png",
});
// User input from hash compiles
await page.screenshot({
fullPage: true,
path: 'test-results/01-show-js-before.png',
path: "test-results/01-show-js-before.png",
});
const userInput =
(await page.locator('.monaco-editor').nth(2).allInnerTexts()) ?? [];
expect(concat(userInput)).toMatchSnapshot('user-input.txt');
(await page.locator(".monaco-editor").nth(2).allInnerTexts()) ?? [];
expect(concat(userInput)).toMatchSnapshot("user-input.txt");
// Reset button works
page.on('dialog', dialog => dialog.accept());
await page.getByRole('button', {name: 'Reset'}).click();
page.on("dialog", (dialog) => dialog.accept());
await page.getByRole("button", { name: "Reset" }).click();
await page.screenshot({
fullPage: true,
path: 'test-results/02-show-js-after.png',
path: "test-results/02-show-js-after.png",
});
const defaultInput =
(await page.locator('.monaco-editor').nth(2).allInnerTexts()) ?? [];
expect(concat(defaultInput)).toMatchSnapshot('default-input.txt');
(await page.locator(".monaco-editor").nth(2).allInnerTexts()) ?? [];
expect(concat(defaultInput)).toMatchSnapshot("default-input.txt");
});

View File

@@ -5,24 +5,25 @@
* LICENSE file in the root directory of this source tree.
*/
import type {NextPage} from 'next';
import Head from 'next/head';
import {SnackbarProvider} from 'notistack';
import {Editor, Header, StoreProvider} from '../components';
import MessageSnackbar from '../components/Message';
import type { NextPage } from "next";
import Head from "next/head";
import { SnackbarProvider } from "notistack";
import { Editor, Header, StoreProvider } from "../components";
import MessageSnackbar from "../components/Message";
const Home: NextPage = () => {
return (
<div className="flex flex-col w-screen h-screen font-light">
<Head>
<title>
{process.env.NODE_ENV === 'development'
? '[DEV] React Compiler Playground'
: 'React Compiler Playground'}
{process.env.NODE_ENV === "development"
? "[DEV] React Compiler Playground"
: "React Compiler Playground"}
</title>
<meta
name="viewport"
content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0"></meta>
content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0"
></meta>
<link rel="icon" href="/favicon.ico" />
<link rel="manifest" href="/site.webmanifest" />
<link
@@ -44,7 +45,8 @@ const Home: NextPage = () => {
<SnackbarProvider
preventDuplicate
maxSnack={10}
Components={{message: MessageSnackbar}}>
Components={{ message: MessageSnackbar }}
>
<Header />
<Editor />
</SnackbarProvider>

View File

@@ -5,21 +5,26 @@
* LICENSE file in the root directory of this source tree.
*/
import '../styles/globals.css';
import "../styles/globals.css";
export default function RootLayout({children}: {children: React.ReactNode}) {
'use no memo';
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
"use no memo";
return (
<html lang="en">
<head>
<title>
{process.env.NODE_ENV === 'development'
? '[DEV] React Compiler Playground'
: 'React Compiler Playground'}
{process.env.NODE_ENV === "development"
? "[DEV] React Compiler Playground"
: "React Compiler Playground"}
</title>
<meta
name="viewport"
content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0"></meta>
content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0"
></meta>
<link rel="icon" href="/favicon.ico" />
<link rel="manifest" href="/site.webmanifest" />
<link

View File

@@ -5,11 +5,11 @@
* LICENSE file in the root directory of this source tree.
*/
'use client';
"use client";
import {SnackbarProvider} from 'notistack';
import {Editor, Header, StoreProvider} from '../components';
import MessageSnackbar from '../components/Message';
import { SnackbarProvider } from "notistack";
import { Editor, Header, StoreProvider } from "../components";
import MessageSnackbar from "../components/Message";
export default function Hoot() {
return (
@@ -17,7 +17,8 @@ export default function Hoot() {
<SnackbarProvider
preventDuplicate
maxSnack={10}
Components={{message: MessageSnackbar}}>
Components={{ message: MessageSnackbar }}
>
<Header />
<Editor />
</SnackbarProvider>

View File

@@ -8,12 +8,12 @@
module.exports = function (api) {
api.cache(true);
return {
presets: ['next/babel'],
presets: ["next/babel"],
plugins: [
[
'babel-plugin-react-compiler',
"babel-plugin-react-compiler",
{
runtimeModule: 'react-compiler-runtime',
runtimeModule: "react-compiler-runtime",
},
],
],

View File

@@ -11,86 +11,86 @@
module.exports = {
// Text colors
primary: '#23272F', // gray-90
'primary-dark': '#F6F7F9', // gray-5
secondary: '#404756', // gray-70
'secondary-dark': '#EBECF0', // gray-10
link: '#087EA4', // blue-50
'link-dark': '#149ECA', // blue-40
syntax: '#EBECF0', // gray-10
wash: '#FFFFFF',
'wash-dark': '#23272F', // gray-90
card: '#F6F7F9', // gray-05
'card-dark': '#343A46', // gray-80
highlight: '#E6F7FF', // blue-10
'highlight-dark': 'rgba(88,175,223,.1)',
border: '#EBECF0', // gray-10
'border-dark': '#343A46', // gray-80
'secondary-button': '#EBECF0', // gray-10
'secondary-button-dark': '#404756', // gray-70
primary: "#23272F", // gray-90
"primary-dark": "#F6F7F9", // gray-5
secondary: "#404756", // gray-70
"secondary-dark": "#EBECF0", // gray-10
link: "#087EA4", // blue-50
"link-dark": "#149ECA", // blue-40
syntax: "#EBECF0", // gray-10
wash: "#FFFFFF",
"wash-dark": "#23272F", // gray-90
card: "#F6F7F9", // gray-05
"card-dark": "#343A46", // gray-80
highlight: "#E6F7FF", // blue-10
"highlight-dark": "rgba(88,175,223,.1)",
border: "#EBECF0", // gray-10
"border-dark": "#343A46", // gray-80
"secondary-button": "#EBECF0", // gray-10
"secondary-button-dark": "#404756", // gray-70
// Gray
'gray-95': '#16181D',
'gray-90': '#23272F',
'gray-80': '#343A46',
'gray-70': '#404756',
'gray-60': '#4E5769',
'gray-50': '#5E687E', // unused
'gray-40': '#78839B',
'gray-30': '#99A1B3',
'gray-20': '#BCC1CD',
'gray-10': '#EBECF0',
'gray-5': '#F6F7F9',
"gray-95": "#16181D",
"gray-90": "#23272F",
"gray-80": "#343A46",
"gray-70": "#404756",
"gray-60": "#4E5769",
"gray-50": "#5E687E", // unused
"gray-40": "#78839B",
"gray-30": "#99A1B3",
"gray-20": "#BCC1CD",
"gray-10": "#EBECF0",
"gray-5": "#F6F7F9",
// Blue
'blue-60': '#045975',
'blue-50': '#087EA4',
'blue-40': '#149ECA', // Brand Blue
'blue-30': '#58C4DC', // unused
'blue-20': '#ABE2ED',
'blue-10': '#E6F7FF', // todo: doesn't match illustrations
'blue-5': '#E6F6FA',
"blue-60": "#045975",
"blue-50": "#087EA4",
"blue-40": "#149ECA", // Brand Blue
"blue-30": "#58C4DC", // unused
"blue-20": "#ABE2ED",
"blue-10": "#E6F7FF", // todo: doesn't match illustrations
"blue-5": "#E6F6FA",
// Yellow
'yellow-60': '#B65700',
'yellow-50': '#C76A15',
'yellow-40': '#DB7D27', // unused
'yellow-30': '#FABD62', // unused
'yellow-20': '#FCDEB0', // unused
'yellow-10': '#FDE7C7',
'yellow-5': '#FEF5E7',
"yellow-60": "#B65700",
"yellow-50": "#C76A15",
"yellow-40": "#DB7D27", // unused
"yellow-30": "#FABD62", // unused
"yellow-20": "#FCDEB0", // unused
"yellow-10": "#FDE7C7",
"yellow-5": "#FEF5E7",
// Purple
'purple-60': '#2B3491', // unused
'purple-50': '#575FB7',
'purple-40': '#6B75DB',
'purple-30': '#8891EC',
'purple-20': '#C3C8F5', // unused
'purple-10': '#E7E9FB',
'purple-5': '#F3F4FD',
"purple-60": "#2B3491", // unused
"purple-50": "#575FB7",
"purple-40": "#6B75DB",
"purple-30": "#8891EC",
"purple-20": "#C3C8F5", // unused
"purple-10": "#E7E9FB",
"purple-5": "#F3F4FD",
// Green
'green-60': '#2B6E62',
'green-50': '#388F7F',
'green-40': '#44AC99',
'green-30': '#7FCCBF',
'green-20': '#ABDED5',
'green-10': '#E5F5F2',
'green-5': '#F4FBF9',
"green-60": "#2B6E62",
"green-50": "#388F7F",
"green-40": "#44AC99",
"green-30": "#7FCCBF",
"green-20": "#ABDED5",
"green-10": "#E5F5F2",
"green-5": "#F4FBF9",
// RED
'red-60': '#712D28',
'red-50': '#A6423A', // unused
'red-40': '#C1554D',
'red-30': '#D07D77',
'red-20': '#E5B7B3', // unused
'red-10': '#F2DBD9', // unused
'red-5': '#FAF1F0',
"red-60": "#712D28",
"red-50": "#A6423A", // unused
"red-40": "#C1554D",
"red-30": "#D07D77",
"red-20": "#E5B7B3", // unused
"red-10": "#F2DBD9", // unused
"red-5": "#FAF1F0",
// MISC
'code-block': '#99a1b30f', // gray-30 @ 6%
'gradient-blue': '#58C4DC', // Only used for the landing gradient for now.
"code-block": "#99a1b30f", // gray-30 @ 6%
"gradient-blue": "#58C4DC", // Only used for the landing gradient for now.
github: {
highlight: '#fffbdd',
highlight: "#fffbdd",
},
};

View File

@@ -5,10 +5,9 @@
* LICENSE file in the root directory of this source tree.
*/
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';
import { parse, ParserPlugin } from "@babel/parser";
import traverse, { NodePath } from "@babel/traverse";
import * as t from "@babel/types";
import {
CompilerError,
CompilerErrorDetail,
@@ -20,51 +19,31 @@ import {
run,
ValueKind,
type Hook,
} from 'babel-plugin-react-compiler/src';
import {type ReactFunctionType} from 'babel-plugin-react-compiler/src/HIR/Environment';
import clsx from 'clsx';
import invariant from 'invariant';
import {useSnackbar} from 'notistack';
import {useDeferredValue, useMemo} from 'react';
import {useMountEffect} from '../../hooks';
import {defaultStore} from '../../lib/defaultStore';
} from "babel-plugin-react-compiler/src";
import { type ReactFunctionType } from "babel-plugin-react-compiler/src/HIR/Environment";
import clsx from "clsx";
import invariant from "invariant";
import { useSnackbar } from "notistack";
import { useDeferredValue, useMemo } from "react";
import { useMountEffect } from "../../hooks";
import { defaultStore } from "../../lib/defaultStore";
import {
createMessage,
initStoreFromUrlOrLocalStorage,
MessageLevel,
MessageSource,
type Store,
} from '../../lib/stores';
import {useStore, useStoreDispatch} from '../StoreContext';
import Input from './Input';
} from "../../lib/stores";
import { useStore, useStoreDispatch } from "../StoreContext";
import Input from "./Input";
import {
CompilerOutput,
default as Output,
PrintedCompilerPipelineValue,
} from './Output';
import {printFunctionWithOutlined} from 'babel-plugin-react-compiler/src/HIR/PrintHIR';
import {printReactiveFunctionWithOutlined} from 'babel-plugin-react-compiler/src/ReactiveScopes/PrintReactiveFunction';
function parseInput(input: string, language: 'flow' | 'typescript') {
// Extract the first line to quickly check for custom test directives
if (language === 'flow') {
return HermesParser.parse(input, {
babel: true,
flow: 'all',
sourceType: 'module',
enableExperimentalComponentSyntax: true,
});
} else {
return babelParse(input, {
plugins: ['typescript', 'jsx'],
sourceType: 'module',
});
}
}
} from "./Output";
function parseFunctions(
source: string,
language: 'flow' | 'typescript',
source: string
): Array<
NodePath<
t.FunctionDeclaration | t.ArrowFunctionExpression | t.FunctionExpression
@@ -76,7 +55,20 @@ function parseFunctions(
>
> = [];
try {
const ast = parseInput(source, language);
const isFlow = source
.trim()
.split("\n", 1)[0]
.match(/\s*\/\/\s*\@flow\s*/);
let type_transform: ParserPlugin;
if (isFlow) {
type_transform = "flow";
} else {
type_transform = "typescript";
}
const ast = parse(source, {
plugins: [type_transform, "jsx"],
sourceType: "module",
});
traverse(ast, {
FunctionDeclaration(nodePath) {
items.push(nodePath);
@@ -105,7 +97,7 @@ function parseFunctions(
const COMMON_HOOKS: Array<[string, Hook]> = [
[
'useFragment',
"useFragment",
{
valueKind: ValueKind.Frozen,
effectKind: Effect.Freeze,
@@ -114,7 +106,7 @@ const COMMON_HOOKS: Array<[string, Hook]> = [
},
],
[
'usePaginationFragment',
"usePaginationFragment",
{
valueKind: ValueKind.Frozen,
effectKind: Effect.Freeze,
@@ -123,7 +115,7 @@ const COMMON_HOOKS: Array<[string, Hook]> = [
},
],
[
'useRefetchableFragment',
"useRefetchableFragment",
{
valueKind: ValueKind.Frozen,
effectKind: Effect.Freeze,
@@ -132,7 +124,7 @@ const COMMON_HOOKS: Array<[string, Hook]> = [
},
],
[
'useLazyLoadQuery',
"useLazyLoadQuery",
{
valueKind: ValueKind.Frozen,
effectKind: Effect.Freeze,
@@ -141,7 +133,7 @@ const COMMON_HOOKS: Array<[string, Hook]> = [
},
],
[
'usePreloadedQuery',
"usePreloadedQuery",
{
valueKind: ValueKind.Frozen,
effectKind: Effect.Freeze,
@@ -156,22 +148,22 @@ function isHookName(s: string): boolean {
}
function getReactFunctionType(
id: NodePath<t.Identifier | null | undefined>,
id: NodePath<t.Identifier | null | undefined>
): ReactFunctionType {
if (id && id.node && id.isIdentifier()) {
if (isHookName(id.node.name)) {
return 'Hook';
return "Hook";
}
const isPascalCaseNameSpace = /^[A-Z].*/;
if (isPascalCaseNameSpace.test(id.node.name)) {
return 'Component';
return "Component";
}
}
return 'Other';
return "Other";
}
function compile(source: string): [CompilerOutput, 'flow' | 'typescript'] {
function compile(source: string): CompilerOutput {
const results = new Map<string, PrintedCompilerPipelineValue[]>();
const error = new CompilerError();
const upsert = (result: PrintedCompilerPipelineValue) => {
@@ -182,33 +174,27 @@ function compile(source: string): [CompilerOutput, 'flow' | 'typescript'] {
results.set(result.name, [result]);
}
};
let language: 'flow' | 'typescript';
if (source.match(/\@flow/)) {
language = 'flow';
} else {
language = 'typescript';
}
try {
// Extract the first line to quickly check for custom test directives
const pragma = source.substring(0, source.indexOf('\n'));
const pragma = source.substring(0, source.indexOf("\n"));
const config = parseConfigPragma(pragma);
for (const fn of parseFunctions(source, language)) {
for (const fn of parseFunctions(source)) {
if (!fn.isFunctionDeclaration()) {
error.pushErrorDetail(
new CompilerErrorDetail({
reason: `Unexpected function type ${fn.node.type}`,
description:
'Playground only supports parsing function declarations',
"Playground only supports parsing function declarations",
severity: ErrorSeverity.Todo,
loc: fn.node.loc ?? null,
suggestions: null,
}),
})
);
continue;
}
const id = fn.get('id');
const id = fn.get("id");
for (const result of run(
fn,
{
@@ -216,20 +202,20 @@ function compile(source: string): [CompilerOutput, 'flow' | 'typescript'] {
customHooks: new Map([...COMMON_HOOKS]),
},
getReactFunctionType(id),
'_c',
null,
"_c",
null,
null,
null
)) {
const fnName = fn.node.id?.name ?? null;
switch (result.kind) {
case 'ast': {
case "ast": {
upsert({
kind: 'ast',
kind: "ast",
fnName,
name: result.name,
value: {
type: 'FunctionDeclaration',
type: "FunctionDeclaration",
id: result.value.id,
async: result.value.async,
generator: result.value.generator,
@@ -239,27 +225,27 @@ function compile(source: string): [CompilerOutput, 'flow' | 'typescript'] {
});
break;
}
case 'hir': {
case "hir": {
upsert({
kind: 'hir',
kind: "hir",
fnName,
name: result.name,
value: printFunctionWithOutlined(result.value),
value: printHIR(result.value.body),
});
break;
}
case 'reactive': {
case "reactive": {
upsert({
kind: 'reactive',
kind: "reactive",
fnName,
name: result.name,
value: printReactiveFunctionWithOutlined(result.value),
value: printReactiveFunction(result.value),
});
break;
}
case 'debug': {
case "debug": {
upsert({
kind: 'debug',
kind: "debug",
fnName,
name: result.name,
value: result.value,
@@ -288,24 +274,24 @@ function compile(source: string): [CompilerOutput, 'flow' | 'typescript'] {
reason: `Unexpected failure when transforming input! ${err}`,
loc: null,
suggestions: null,
}),
})
);
}
}
if (error.hasErrors()) {
return [{kind: 'err', results, error: error}, language];
return { kind: "err", results, error: error };
}
return [{kind: 'ok', results}, language];
return { kind: "ok", results };
}
export default function Editor() {
const store = useStore();
const deferredStore = useDeferredValue(store);
const dispatchStore = useStoreDispatch();
const {enqueueSnackbar} = useSnackbar();
const [compilerOutput, language] = useMemo(
const { enqueueSnackbar } = useSnackbar();
const compilerOutput = useMemo(
() => compile(deferredStore.source),
[deferredStore.source],
[deferredStore.source]
);
useMountEffect(() => {
@@ -313,35 +299,34 @@ export default function Editor() {
try {
mountStore = initStoreFromUrlOrLocalStorage();
} catch (e) {
invariant(e instanceof Error, 'Only Error may be caught.');
invariant(e instanceof Error, "Only Error may be caught.");
enqueueSnackbar(e.message, {
variant: 'message',
variant: "message",
...createMessage(
'Bad URL - fell back to the default Playground.',
"Bad URL - fell back to the default Playground.",
MessageLevel.Info,
MessageSource.Playground,
MessageSource.Playground
),
});
mountStore = defaultStore;
}
dispatchStore({
type: 'setStore',
payload: {store: mountStore},
type: "setStore",
payload: { store: mountStore },
});
});
return (
<>
<div className="relative flex basis top-14">
<div className={clsx('relative sm:basis-1/4')}>
<div className={clsx("relative sm:basis-1/4")}>
<Input
language={language}
errors={
compilerOutput.kind === 'err' ? compilerOutput.error.details : []
compilerOutput.kind === "err" ? compilerOutput.error.details : []
}
/>
</div>
<div className={clsx('flex sm:flex flex-wrap')}>
<div className={clsx("flex sm:flex flex-wrap")}>
<Output store={deferredStore} compilerOutput={compilerOutput} />
</div>
</div>

View File

@@ -5,28 +5,27 @@
* LICENSE file in the root directory of this source tree.
*/
import MonacoEditor, {loader, type Monaco} from '@monaco-editor/react';
import {CompilerErrorDetail} from 'babel-plugin-react-compiler/src';
import invariant from 'invariant';
import type {editor} from 'monaco-editor';
import * as monaco from 'monaco-editor';
import {Resizable} from 're-resizable';
import {useEffect, useState} from 'react';
import {renderReactCompilerMarkers} from '../../lib/reactCompilerMonacoDiagnostics';
import {useStore, useStoreDispatch} from '../StoreContext';
import {monacoOptions} from './monacoOptions';
import MonacoEditor, { loader, type Monaco } from "@monaco-editor/react";
import { CompilerErrorDetail } from "babel-plugin-react-compiler/src";
import invariant from "invariant";
import type { editor } from "monaco-editor";
import * as monaco from "monaco-editor";
import { Resizable } from "re-resizable";
import { useEffect, useState } from "react";
import { renderReactCompilerMarkers } from "../../lib/reactCompilerMonacoDiagnostics";
import { useStore, useStoreDispatch } from "../StoreContext";
import { monacoOptions } from "./monacoOptions";
// 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';
import React$Types from "../../node_modules/@types/react/index.d.ts";
loader.config({monaco});
loader.config({ monaco });
type Props = {
errors: CompilerErrorDetail[];
language: 'flow' | 'typescript';
};
export default function Input({errors, language}: Props) {
export default function Input({ errors }: Props) {
const [monaco, setMonaco] = useState<Monaco | null>(null);
const store = useStore();
const dispatchStore = useStoreDispatch();
@@ -36,47 +35,18 @@ export default function Input({errors, language}: Props) {
if (!monaco) return;
const uri = monaco.Uri.parse(`file:///index.js`);
const model = monaco.editor.getModel(uri);
invariant(model, 'Model must exist for the selected input file.');
renderReactCompilerMarkers({monaco, model, details: errors});
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.
model.updateOptions({tabSize: 2});
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.
if (!monaco) return;
monaco.languages.typescript.javascriptDefaults.setDiagnosticsOptions({
diagnosticCodesToIgnore: [
8002,
8003,
8004,
8005,
8006,
8008,
8009,
8010,
8011,
8012,
8013,
...(language === 'flow' ? flowDiagnosticDisable : []),
],
noSemanticValidation: true,
// Monaco can't validate Flow component syntax
noSyntaxValidation: language === 'flow',
});
}, [monaco, language]);
const handleChange = (value: string | undefined) => {
if (!value) return;
dispatchStore({
type: 'updateFile',
type: "updateFile",
payload: {
source: value,
},
@@ -86,16 +56,27 @@ export default function Input({errors, language}: Props) {
const handleMount = (_: editor.IStandaloneCodeEditor, monaco: Monaco) => {
setMonaco(monaco);
// 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.
monaco.languages.typescript.javascriptDefaults.setDiagnosticsOptions({
diagnosticCodesToIgnore: [
8002, 8003, 8004, 8005, 8006, 8008, 8009, 8010, 8011, 8012, 8013,
],
noSemanticValidation: true,
noSyntaxValidation: false,
});
const tscOptions = {
allowNonTsExtensions: true,
target: monaco.languages.typescript.ScriptTarget.ES2015,
moduleResolution: monaco.languages.typescript.ModuleResolutionKind.NodeJs,
jsx: monaco.languages.typescript.JsxEmit.Preserve,
typeRoots: ['node_modules/@types'],
typeRoots: ["node_modules/@types"],
allowSyntheticDefaultImports: true,
};
monaco.languages.typescript.javascriptDefaults.setCompilerOptions(
tscOptions,
tscOptions
);
monaco.languages.typescript.typescriptDefaults.setCompilerOptions({
...tscOptions,
@@ -106,7 +87,7 @@ export default function Input({errors, language}: Props) {
// Add React type declarations to Monaco
const reactLib = [
React$Types,
'file:///node_modules/@types/react/index.d.ts',
"file:///node_modules/@types/react/index.d.ts",
] as [any, string];
monaco.languages.typescript.javascriptDefaults.addExtraLib(...reactLib);
monaco.languages.typescript.typescriptDefaults.addExtraLib(...reactLib);
@@ -124,16 +105,17 @@ export default function Input({errors, language}: Props) {
<div className="relative flex flex-col flex-none border-r border-gray-200">
<Resizable
minWidth={650}
enable={{right: true}}
enable={{ right: true }}
// Restrict MonacoEditor's height, since the config autoLayout:true
// will grow the editor to fit within parent element
className="!h-[calc(100vh_-_3.5rem)]">
className="!h-[calc(100vh_-_3.5rem)]"
>
<MonacoEditor
path={'index.js'}
path={"index.js"}
// .js and .jsx files are specified to be TS so that Monaco can actually
// check their syntax using its TS language service. They are still JS files
// due to their extensions, so TS language features don't work.
language={'javascript'}
language={"javascript"}
value={store.source}
onMount={handleMount}
onChange={handleChange}

View File

@@ -5,46 +5,46 @@
* LICENSE file in the root directory of this source tree.
*/
import generate from '@babel/generator';
import * as t from '@babel/types';
import generate from "@babel/generator";
import * as t from "@babel/types";
import {
CodeIcon,
DocumentAddIcon,
InformationCircleIcon,
} from '@heroicons/react/outline';
import MonacoEditor, {DiffEditor} from '@monaco-editor/react';
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, useEffect, useState} from 'react';
import {type Store} from '../../lib/stores';
import TabbedWindow from '../TabbedWindow';
import {monacoOptions} from './monacoOptions';
} from "@heroicons/react/outline";
import MonacoEditor, { DiffEditor } from "@monaco-editor/react";
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, useEffect, useState } from "react";
import { type Store } from "../../lib/stores";
import TabbedWindow from "../TabbedWindow";
import { monacoOptions } from "./monacoOptions";
const MemoizedOutput = memo(Output);
export default MemoizedOutput;
export type PrintedCompilerPipelineValue =
| {
kind: 'ast';
kind: "ast";
name: string;
fnName: string | null;
value: t.FunctionDeclaration;
}
| {
kind: 'hir';
kind: "hir";
name: string;
fnName: string | null;
value: string;
}
| {kind: 'reactive'; name: string; fnName: string | null; value: string}
| {kind: 'debug'; name: string; fnName: string | null; value: string};
| { kind: "reactive"; name: string; fnName: string | null; value: string }
| { kind: "debug"; name: string; fnName: string | null; value: string };
export type CompilerOutput =
| {kind: 'ok'; results: Map<string, PrintedCompilerPipelineValue[]>}
| { kind: "ok"; results: Map<string, PrintedCompilerPipelineValue[]> }
| {
kind: 'err';
kind: "err";
results: Map<string, PrintedCompilerPipelineValue[]>;
error: CompilerError;
};
@@ -63,7 +63,7 @@ async function tabify(source: string, compilerOutput: CompilerOutput) {
for (const [passName, results] of compilerOutput.results) {
for (const result of results) {
switch (result.kind) {
case 'hir': {
case "hir": {
const prev = concattedResults.get(result.name);
const next = result.value;
const identName = `function ${result.fnName}`;
@@ -74,7 +74,7 @@ async function tabify(source: string, compilerOutput: CompilerOutput) {
}
break;
}
case 'reactive': {
case "reactive": {
const prev = concattedResults.get(passName);
const next = result.value;
if (prev != null) {
@@ -84,29 +84,29 @@ async function tabify(source: string, compilerOutput: CompilerOutput) {
}
break;
}
case 'ast':
case "ast":
topLevelFnDecls.push(result.value);
break;
case 'debug': {
case "debug": {
concattedResults.set(passName, result.value);
break;
}
default: {
const _: never = result;
throw new Error('Unexpected result kind');
throw new Error("Unexpected result kind");
}
}
}
}
let lastPassOutput: string | null = null;
let nonDiffPasses = ['HIR', 'BuildReactiveFunction', 'EnvironmentConfig'];
for (const [passName, text] of concattedResults) {
tabs.set(
passName,
<TextTabContent
output={text}
diff={lastPassOutput}
showInfoPanel={!nonDiffPasses.includes(passName)}></TextTabContent>,
diff={lastPassOutput ?? null}
showInfoPanel={true}
></TextTabContent>
);
lastPassOutput = text;
}
@@ -115,24 +115,25 @@ async function tabify(source: string, compilerOutput: CompilerOutput) {
// 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);
const { code, sourceMapUrl } = await codegen(ast, source);
reorderedTabs.set(
'JS',
"JS",
<TextTabContent
output={code}
diff={null}
showInfoPanel={false}></TextTabContent>,
showInfoPanel={false}
></TextTabContent>
);
if (sourceMapUrl) {
reorderedTabs.set(
'SourceMap',
"SourceMap",
<>
<iframe
src={sourceMapUrl}
className="w-full h-monaco_small sm:h-monaco"
title="Generated Code"
/>
</>,
</>
);
}
}
@@ -144,23 +145,23 @@ async function tabify(source: string, compilerOutput: CompilerOutput) {
async function codegen(
ast: t.Program,
source: string,
): Promise<{code: any; sourceMapUrl: string | null}> {
source: string
): Promise<{ code: any; sourceMapUrl: string | null }> {
const generated = generate(
ast,
{sourceMaps: true, sourceFileName: 'input.js'},
source,
{ sourceMaps: true, sourceFileName: "input.js" },
source
);
const sourceMapUrl = getSourceMapUrl(
generated.code,
JSON.stringify(generated.map),
JSON.stringify(generated.map)
);
const codegenOutput = await prettier.format(generated.code, {
semi: true,
parser: 'babel',
parser: "babel",
plugins: [parserBabel, prettierPluginEstree],
});
return {code: codegenOutput, sourceMapUrl};
return { code: codegenOutput, sourceMapUrl };
}
function utf16ToUTF8(s: string): string {
@@ -171,27 +172,27 @@ function getSourceMapUrl(code: string, map: string): string | null {
code = utf16ToUTF8(code);
map = utf16ToUTF8(map);
return `https://evanw.github.io/source-map-visualization/#${btoa(
`${code.length}\0${code}${map.length}\0${map}`,
`${code.length}\0${code}${map.length}\0${map}`
)}`;
}
function Output({store, compilerOutput}: Props) {
const [tabsOpen, setTabsOpen] = useState<Set<string>>(() => new Set(['JS']));
function Output({ store, compilerOutput }: Props) {
const [tabsOpen, setTabsOpen] = useState<Set<string>>(() => new Set(["JS"]));
const [tabs, setTabs] = useState<Map<string, React.ReactNode>>(
() => new Map(),
() => new Map()
);
useEffect(() => {
tabify(store.source, compilerOutput).then(tabs => {
tabify(store.source, compilerOutput).then((tabs) => {
setTabs(tabs);
});
}, [store.source, compilerOutput]);
const changedPasses: Set<string> = new Set(['JS', 'HIR']); // Initial and final passes should always be bold
let lastResult: string = '';
const changedPasses: Set<string> = new Set();
let lastResult: string = "";
for (const [passName, results] of compilerOutput.results) {
for (const result of results) {
let currResult = '';
if (result.kind === 'hir' || result.kind === 'reactive') {
let currResult = "";
if (result.kind === "hir" || result.kind === "reactive") {
currResult += `function ${result.fnName}\n\n${result.value}`;
}
if (currResult !== lastResult) {
@@ -210,16 +211,18 @@ function Output({store, compilerOutput}: Props) {
tabs={tabs}
changedPasses={changedPasses}
/>
{compilerOutput.kind === 'err' ? (
{compilerOutput.kind === "err" ? (
<div
className="flex flex-wrap absolute bottom-0 bg-white grow border-y border-grey-200 transition-all ease-in"
style={{width: 'calc(100vw - 650px)'}}>
style={{ width: "calc(100vw - 650px)" }}
>
<div className="w-full p-4 basis-full border-b">
<h2>COMPILER ERRORS</h2>
</div>
<pre
className="p-4 basis-full text-red-600 overflow-y-scroll whitespace-pre-wrap"
style={{width: 'calc(100vw - 650px)', height: '150px'}}>
style={{ width: "calc(100vw - 650px)", height: "150px" }}
>
<code>{compilerOutput.error.toString()}</code>
</pre>
</div>
@@ -247,7 +250,8 @@ function TextTabContent({
{diff != null && output !== diff ? (
<button
className="flex items-center gap-1 transition-colors duration-150 ease-in text-secondary hover:text-link"
onClick={() => setDiffMode(diffMode => !diffMode)}>
onClick={() => setDiffMode((diffMode) => !diffMode)}
>
{!diffMode ? (
<>
<DocumentAddIcon className="w-5 h-5" /> Show Diff
@@ -275,7 +279,7 @@ function TextTabContent({
options={{
...monacoOptions,
readOnly: true,
lineNumbers: 'off',
lineNumbers: "off",
glyphMargin: false,
// Undocumented see https://github.com/Microsoft/vscode/issues/30795#issuecomment-410998882
lineDecorationsWidth: 0,
@@ -289,7 +293,7 @@ function TextTabContent({
options={{
...monacoOptions,
readOnly: true,
lineNumbers: 'off',
lineNumbers: "off",
glyphMargin: false,
// Undocumented see https://github.com/Microsoft/vscode/issues/30795#issuecomment-410998882
lineDecorationsWidth: 0,

View File

@@ -5,11 +5,11 @@
* LICENSE file in the root directory of this source tree.
*/
import dynamic from 'next/dynamic';
import dynamic from "next/dynamic";
// monaco-editor is currently not compatible with ssr
// https://github.com/vercel/next.js/issues/31692
const Editor = dynamic(() => import('./EditorImpl'), {
const Editor = dynamic(() => import("./EditorImpl"), {
ssr: false,
});

View File

@@ -5,11 +5,11 @@
* LICENSE file in the root directory of this source tree.
*/
import type {EditorProps} from '@monaco-editor/react';
import type { EditorProps } from "@monaco-editor/react";
export const monacoOptions: Partial<EditorProps['options']> = {
export const monacoOptions: Partial<EditorProps["options"]> = {
fontSize: 14,
padding: {top: 8},
padding: { top: 8 },
scrollbar: {
verticalScrollbarSize: 10,
alwaysConsumeMouseWheel: false,
@@ -22,11 +22,11 @@ export const monacoOptions: Partial<EditorProps['options']> = {
fontFamily: '"Source Code Pro", monospace',
glyphMargin: true,
autoClosingBrackets: 'languageDefined',
autoClosingDelete: 'always',
autoClosingOvertype: 'always',
autoClosingBrackets: "languageDefined",
autoClosingDelete: "always",
autoClosingOvertype: "always",
automaticLayout: true,
wordWrap: 'on',
wrappingIndent: 'deepIndent',
wordWrap: "on",
wrappingIndent: "deepIndent",
};

View File

@@ -5,24 +5,24 @@
* LICENSE file in the root directory of this source tree.
*/
import {RefreshIcon, ShareIcon} from '@heroicons/react/outline';
import {CheckIcon} from '@heroicons/react/solid';
import clsx from 'clsx';
import Link from 'next/link';
import {useSnackbar} from 'notistack';
import {useState} from 'react';
import {defaultStore} from '../lib/defaultStore';
import {IconGitHub} from './Icons/IconGitHub';
import Logo from './Logo';
import {useStoreDispatch} from './StoreContext';
import { RefreshIcon, ShareIcon } from "@heroicons/react/outline";
import { CheckIcon } from "@heroicons/react/solid";
import clsx from "clsx";
import Link from "next/link";
import { useSnackbar } from "notistack";
import { useState } from "react";
import { defaultStore } from "../lib/defaultStore";
import { IconGitHub } from "./Icons/IconGitHub";
import Logo from "./Logo";
import { useStoreDispatch } from "./StoreContext";
export default function Header() {
const [showCheck, setShowCheck] = useState(false);
const dispatchStore = useStoreDispatch();
const {enqueueSnackbar, closeSnackbar} = useSnackbar();
const { enqueueSnackbar, closeSnackbar } = useSnackbar();
const handleReset = () => {
if (confirm('Are you sure you want to reset the playground?')) {
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
@@ -31,13 +31,13 @@ export default function Header() {
such as "Bad URL" will be closed by the outputs calling `closeSnackbar`.
*/
closeSnackbar();
dispatchStore({type: 'setStore', payload: {store: defaultStore}});
dispatchStore({ type: "setStore", payload: { store: defaultStore } });
}
};
const handleShare = () => {
navigator.clipboard.writeText(location.href).then(() => {
enqueueSnackbar('URL copied to clipboard');
enqueueSnackbar("URL copied to clipboard");
setShowCheck(true);
// Show the check mark icon briefly after URL is copied
setTimeout(() => setShowCheck(false), 1000);
@@ -49,8 +49,8 @@ export default function Header() {
<div className="flex items-center flex-none h-full gap-2 text-lg">
<Logo
className={clsx(
'w-8 h-8 text-link',
process.env.NODE_ENV === 'development' && 'text-yellow-600',
"w-8 h-8 text-link",
process.env.NODE_ENV === "development" && "text-yellow-600"
)}
/>
<p className="hidden select-none sm:block">React Compiler Playground</p>
@@ -60,7 +60,8 @@ export default function Header() {
title="Reset Playground"
aria-label="Reset Playground"
className="flex items-center gap-1 transition-colors duration-150 ease-in text-secondary hover:text-link"
onClick={handleReset}>
onClick={handleReset}
>
<RefreshIcon className="w-5 h-5" />
<p className="hidden sm:block">Reset</p>
</button>
@@ -69,7 +70,8 @@ export default function Header() {
aria-label="Copy sharable URL"
className="flex items-center gap-1 transition-colors duration-150 ease-in text-secondary hover:text-link"
onClick={handleShare}
disabled={showCheck}>
disabled={showCheck}
>
{!showCheck ? (
<ShareIcon className="w-5 h-5" />
) : (
@@ -82,7 +84,8 @@ export default function Header() {
target="_blank"
rel="noreferrer noopener"
aria-label="Open on GitHub"
className="flex items-center gap-1 transition-colors duration-150 ease-in text-secondary hover:text-link">
className="flex items-center gap-1 transition-colors duration-150 ease-in text-secondary hover:text-link"
>
<IconGitHub />
</Link>
</div>

View File

@@ -5,9 +5,9 @@
* LICENSE file in the root directory of this source tree.
*/
import {memo} from 'react';
import { memo } from "react";
export const IconGitHub = memo<JSX.IntrinsicElements['svg']>(
export const IconGitHub = memo<JSX.IntrinsicElements["svg"]>(
function IconGitHub(props) {
return (
<svg
@@ -16,9 +16,10 @@ export const IconGitHub = memo<JSX.IntrinsicElements['svg']>(
height="1.5em"
viewBox="0 -2 24 24"
fill="currentColor"
{...props}>
{...props}
>
<path d="M10 0a10 10 0 0 0-3.16 19.49c.5.1.68-.22.68-.48l-.01-1.7c-2.78.6-3.37-1.34-3.37-1.34-.46-1.16-1.11-1.47-1.11-1.47-.9-.62.07-.6.07-.6 1 .07 1.53 1.03 1.53 1.03.9 1.52 2.34 1.08 2.91.83.1-.65.35-1.09.63-1.34-2.22-.25-4.55-1.11-4.55-4.94 0-1.1.39-1.99 1.03-2.69a3.6 3.6 0 0 1 .1-2.64s.84-.27 2.75 1.02a9.58 9.58 0 0 1 5 0c1.91-1.3 2.75-1.02 2.75-1.02.55 1.37.2 2.4.1 2.64.64.7 1.03 1.6 1.03 2.69 0 3.84-2.34 4.68-4.57 4.93.36.31.68.92.68 1.85l-.01 2.75c0 .26.18.58.69.48A10 10 0 0 0 10 0"></path>
</svg>
);
},
}
);

View File

@@ -7,13 +7,14 @@
// https://github.com/reactjs/reactjs.org/blob/main/beta/src/components/Logo.tsx
export default function Logo(props: JSX.IntrinsicElements['svg']) {
export default function Logo(props: JSX.IntrinsicElements["svg"]) {
return (
<svg
viewBox="0 0 410 369"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}>
{...props}
>
<path
d="M204.995 224.552C226.56 224.552 244.042 207.07 244.042 185.506C244.042 163.941 226.56 146.459 204.995 146.459C183.43 146.459 165.948 163.941 165.948 185.506C165.948 207.07 183.43 224.552 204.995 224.552Z"
fill="currentColor"

View File

@@ -10,13 +10,13 @@ import {
ExclamationIcon,
InformationCircleIcon,
XIcon,
} from '@heroicons/react/solid';
import {CustomContentProps, SnackbarContent, useSnackbar} from 'notistack';
import {forwardRef} from 'react';
import {MessageLevel, MessageSource} from '../lib/stores';
} from "@heroicons/react/solid";
import { CustomContentProps, SnackbarContent, useSnackbar } from "notistack";
import { forwardRef } from "react";
import { MessageLevel, MessageSource } from "../lib/stores";
// https://notistack.com/examples/advanced/custom-component#custom-variant-(typescript)
declare module 'notistack' {
declare module "notistack" {
interface VariantOverrides {
message: {
title: string;
@@ -34,14 +34,15 @@ interface MessageProps extends CustomContentProps {
}
const Message = forwardRef<HTMLDivElement, MessageProps>(
({id, title, level, source, codeframe}, ref) => {
const {closeSnackbar} = useSnackbar();
({ id, title, level, source, codeframe }, ref) => {
const { closeSnackbar } = useSnackbar();
const isDismissible = source !== MessageSource.Playground;
return (
<SnackbarContent
ref={ref}
className="flex items-start justify-between gap-3 px-4 py-3 text-sm bg-white border rounded-md shadow w-toast">
className="flex items-start justify-between gap-3 px-4 py-3 text-sm bg-white border rounded-md shadow w-toast"
>
<div className="flex gap-3 w-toast-body">
{level === MessageLevel.Warning ? (
<div className="flex items-center justify-center flex-none rounded-md w-7 h-7 bg-amber-100">
@@ -68,15 +69,16 @@ const Message = forwardRef<HTMLDivElement, MessageProps>(
{isDismissible ? (
<button
className="flex items-center justify-center flex-none transition-colors duration-150 ease-in rounded-md justify-self-end group w-7 h-7 hover:bg-gray-200"
onClick={() => closeSnackbar(id)}>
onClick={() => closeSnackbar(id)}
>
<XIcon className="w-5 h-5 fill-gray-500 group-hover:fill-gray-800" />
</button>
) : null}
</SnackbarContent>
);
},
}
);
Message.displayName = 'MessageComponent';
Message.displayName = "MessageComponent";
export default Message;

View File

@@ -5,12 +5,12 @@
* LICENSE file in the root directory of this source tree.
*/
import type {Dispatch, ReactNode} from 'react';
import {useReducer} from 'react';
import createContext from '../lib/createContext';
import {emptyStore} from '../lib/defaultStore';
import type {Store} from '../lib/stores';
import {saveStore} from '../lib/stores';
import type { Dispatch, ReactNode } from "react";
import { useReducer } from "react";
import createContext from "../lib/createContext";
import { emptyStore } from "../lib/defaultStore";
import type { Store } from "../lib/stores";
import { saveStore } from "../lib/stores";
const StoreContext = createContext<Store>();
@@ -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}) {
export function StoreProvider({ children }: { children: ReactNode }) {
const [store, dispatch] = useReducer(storeReducer, emptyStore);
return (
@@ -43,13 +43,13 @@ export function StoreProvider({children}: {children: ReactNode}) {
type ReducerAction =
| {
type: 'setStore';
type: "setStore";
payload: {
store: Store;
};
}
| {
type: 'updateFile';
type: "updateFile";
payload: {
source: string;
};
@@ -57,14 +57,14 @@ type ReducerAction =
function storeReducer(store: Store, action: ReducerAction): Store {
switch (action.type) {
case 'setStore': {
case "setStore": {
const newStore = action.payload.store;
saveStore(newStore);
return newStore;
}
case 'updateFile': {
const {source} = action.payload;
case "updateFile": {
const { source } = action.payload;
const newStore = {
...store,

View File

@@ -5,8 +5,8 @@
* LICENSE file in the root directory of this source tree.
*/
import {Resizable} from 're-resizable';
import React, {useCallback} from 'react';
import { Resizable } from "re-resizable";
import React, { useCallback } from "react";
type TabsRecord = Map<string, React.ReactNode>;
@@ -21,14 +21,15 @@ export default function TabbedWindow(props: {
return (
<div
className="flex items-center justify-center"
style={{width: 'calc(100vw - 650px)'}}>
style={{ width: "calc(100vw - 650px)" }}
>
No compiler output detected, see errors below
</div>
);
}
return (
<div className="flex flex-row">
{Array.from(props.tabs.keys()).map(name => {
{Array.from(props.tabs.keys()).map((name) => {
return (
<TabbedWindowItem
name={name}
@@ -72,14 +73,13 @@ function TabbedWindowItem({
return (
<div key={name} className="flex flex-row">
{isShow ? (
<Resizable className="border-r" minWidth={550} enable={{right: true}}>
<Resizable className="border-r" minWidth={550} enable={{ right: true }}>
<h2
title="Minimize tab"
aria-label="Minimize tab"
onClick={toggleTabs}
className={`p-4 duration-150 ease-in border-b cursor-pointer border-grey-200 ${
hasChanged ? 'font-bold' : 'font-light'
} text-secondary hover:text-link`}>
className={`p-4 duration-150 ease-in border-b cursor-pointer border-grey-200 ${hasChanged ? "font-bold" : "font-light"} text-secondary hover:text-link`}
>
- {name}
</h2>
{tabs.get(name) ?? <div>No output for {name}</div>}
@@ -89,11 +89,10 @@ function TabbedWindowItem({
<button
title={`Expand compiler tab: ${name}`}
aria-label={`Expand compiler tab: ${name}`}
style={{transform: 'rotate(90deg) translate(-50%)'}}
style={{ transform: "rotate(90deg) translate(-50%)" }}
onClick={toggleTabs}
className={`flex-grow-0 w-5 transition-colors duration-150 ease-in ${
hasChanged ? 'font-bold' : 'font-light'
} text-secondary hover:text-link`}>
className={`flex-grow-0 w-5 transition-colors duration-150 ease-in ${hasChanged ? "font-bold" : "font-light"} text-secondary hover:text-link`}
>
{name}
</button>
</div>

View File

@@ -5,6 +5,6 @@
* LICENSE file in the root directory of this source tree.
*/
export {default as Editor} from './Editor';
export {default as Header} from './Header';
export {StoreProvider} from './StoreContext';
export { default as Editor } from "./Editor";
export { default as Header } from "./Header";
export { StoreProvider } from "./StoreContext";

View File

@@ -5,4 +5,4 @@
* LICENSE file in the root directory of this source tree.
*/
export {default as useMountEffect} from './useMountEffect';
export { default as useMountEffect } from "./useMountEffect";

View File

@@ -5,8 +5,8 @@
* LICENSE file in the root directory of this source tree.
*/
import type {EffectCallback} from 'react';
import {useEffect} from 'react';
import type { EffectCallback } from "react";
import { useEffect } from "react";
export default function useMountEffect(effect: EffectCallback) {
return useEffect(effect, []);

View File

@@ -5,7 +5,7 @@
* LICENSE file in the root directory of this source tree.
*/
import React from 'react';
import React from "react";
/**
* Replacement to React.createContext.
@@ -29,9 +29,9 @@ export default function createContext<T>() {
function useContext() {
const c = React.useContext(context);
if (!c)
throw new Error('useContext must be within a Provider with a value');
throw new Error("useContext must be within a Provider with a value");
return c;
}
return {useContext, Provider: context.Provider};
return { useContext, Provider: context.Provider };
}

View File

@@ -5,7 +5,7 @@
* LICENSE file in the root directory of this source tree.
*/
import type {Store} from './stores';
import type { Store } from "./stores";
const index = `\
export default function MyApp() {
@@ -18,5 +18,5 @@ export const defaultStore: Store = {
};
export const emptyStore: Store = {
source: '',
source: "",
};

View File

@@ -5,16 +5,16 @@
* LICENSE file in the root directory of this source tree.
*/
import {Monaco} from '@monaco-editor/react';
import { Monaco } from "@monaco-editor/react";
import {
CompilerErrorDetail,
ErrorSeverity,
} from 'babel-plugin-react-compiler/src';
import {MarkerSeverity, type editor} from 'monaco-editor';
} from "babel-plugin-react-compiler/src";
import { MarkerSeverity, type editor } from "monaco-editor";
function mapReactCompilerSeverityToMonaco(
level: ErrorSeverity,
monaco: Monaco,
monaco: Monaco
): MarkerSeverity {
switch (level) {
case ErrorSeverity.Todo:
@@ -26,9 +26,9 @@ function mapReactCompilerSeverityToMonaco(
function mapReactCompilerDiagnosticToMonacoMarker(
detail: CompilerErrorDetail,
monaco: Monaco,
monaco: Monaco
): editor.IMarkerData | null {
if (detail.loc == null || typeof detail.loc === 'symbol') {
if (detail.loc == null || typeof detail.loc === "symbol") {
return null;
}
const severity = mapReactCompilerSeverityToMonaco(detail.severity, monaco);
@@ -63,27 +63,27 @@ export function renderReactCompilerMarkers({
markers.push(marker);
}
if (markers.length > 0) {
monaco.editor.setModelMarkers(model, 'owner', markers);
const newDecorations = markers.map(marker => {
monaco.editor.setModelMarkers(model, "owner", markers);
const newDecorations = markers.map((marker) => {
return {
range: new monaco.Range(
marker.startLineNumber,
marker.startColumn,
marker.endLineNumber,
marker.endColumn,
marker.endColumn
),
options: {
isWholeLine: true,
glyphMarginClassName: 'bg-red-300',
glyphMarginClassName: "bg-red-300",
},
};
});
decorations = model.deltaDecorations(decorations, newDecorations);
} else {
monaco.editor.setModelMarkers(model, 'owner', []);
monaco.editor.setModelMarkers(model, "owner", []);
decorations = model.deltaDecorations(
model.getAllDecorations().map(d => d.id),
[],
model.getAllDecorations().map((d) => d.id),
[]
);
}
}

View File

@@ -5,5 +5,5 @@
* LICENSE file in the root directory of this source tree.
*/
export * from './messages';
export * from './store';
export * from "./messages";
export * from "./store";

View File

@@ -27,10 +27,10 @@ export interface Message {
export function createMessage(
message: string,
level: MessageLevel,
source: MessageSource,
source: MessageSource
): Message {
const [title, ...body] = message.split('\n');
const codeframe = body.length > 0 ? body.join('\n') : undefined;
const [title, ...body] = message.split("\n");
const codeframe = body.length > 0 ? body.join("\n") : undefined;
return {
source,

View File

@@ -5,12 +5,12 @@
* LICENSE file in the root directory of this source tree.
*/
import invariant from 'invariant';
import invariant from "invariant";
import {
compressToEncodedURIComponent,
decompressFromEncodedURIComponent,
} from 'lz-string';
import {defaultStore} from '../defaultStore';
} from "lz-string";
import { defaultStore } from "../defaultStore";
/**
* Global Store for Playground
@@ -30,8 +30,8 @@ export function decodeStore(hash: string): Store {
*/
export function saveStore(store: Store) {
const hash = encodeStore(store);
localStorage.setItem('playgroundStore', hash);
history.replaceState({}, '', `#${hash}`);
localStorage.setItem("playgroundStore", hash);
history.replaceState({}, "", `#${hash}`);
}
/**
@@ -41,9 +41,9 @@ export function saveStore(store: Store) {
function isValidStore(raw: unknown): raw is Store {
return (
raw != null &&
typeof raw == 'object' &&
'source' in raw &&
typeof raw['source'] === 'string'
typeof raw == "object" &&
"source" in raw &&
typeof raw["source"] === "string"
);
}
@@ -53,7 +53,7 @@ function isValidStore(raw: unknown): raw is Store {
*/
export function initStoreFromUrlOrLocalStorage(): Store {
const encodedSourceFromUrl = location.hash.slice(1);
const encodedSourceFromLocal = localStorage.getItem('playgroundStore');
const encodedSourceFromLocal = localStorage.getItem("playgroundStore");
const encodedSource = encodedSourceFromUrl || encodedSourceFromLocal;
// No data in the URL and no data in the localStorage to fallback to.
@@ -62,6 +62,6 @@ export function initStoreFromUrlOrLocalStorage(): Store {
const raw = decodeStore(encodedSource);
invariant(isValidStore(raw), 'Invalid Store');
invariant(isValidStore(raw), "Invalid Store");
return raw;
}

View File

@@ -1,20 +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.
*/
// v0.17.1
declare module "hermes-parser" {
type HermesParserOptions = {
allowReturnOutsideFunction?: boolean;
babel?: boolean;
flow?: "all" | "detect";
enableExperimentalComponentSyntax?: boolean;
sourceFilename?: string;
sourceType?: "module" | "script" | "unambiguous";
tokens?: boolean;
};
export function parse(code: string, options: Partial<HermesParserOptions>);
}

View File

@@ -5,8 +5,8 @@
* LICENSE file in the root directory of this source tree.
*/
const MonacoWebpackPlugin = require('monaco-editor-webpack-plugin');
const path = require('path');
const MonacoWebpackPlugin = require("monaco-editor-webpack-plugin");
const path = require("path");
const nextConfig = {
reactStrictMode: true,
@@ -14,36 +14,31 @@ const nextConfig = {
// Load *.d.ts files as strings using https://webpack.js.org/guides/asset-modules/#source-assets.
config.module.rules.push({
test: /\.d\.ts/,
type: 'asset/source',
type: "asset/source",
});
// Monaco Editor
if (!options.isServer) {
config.plugins.push(
new MonacoWebpackPlugin({
languages: ['typescript', 'javascript'],
filename: 'static/[name].worker.js',
languages: ["typescript", "javascript"],
filename: "static/[name].worker.js",
})
);
}
config.resolve.alias = {
...config.resolve.alias,
'react-compiler-runtime': path.resolve(
"react-compiler-runtime": path.resolve(
__dirname,
'../../packages/react-compiler-runtime'
"../../packages/react-compiler-runtime"
),
};
config.resolve.fallback = {
fs: false,
path: false,
os: false,
};
return config;
},
transpilePackages: ['monaco-editor'],
transpilePackages: ["monaco-editor"],
};
module.exports = nextConfig;

View File

@@ -8,11 +8,11 @@
"vercel-build": "yarn workspaces run build",
"start": "next start",
"lint": "next lint",
"postinstall": "yarn playwright install",
"test": "playwright test"
},
"dependencies": {
"@babel/core": "^7.19.1",
"@babel/generator": "^7.19.1",
"@babel/parser": "^7.19.1",
"@babel/plugin-syntax-typescript": "^7.18.6",
"@babel/plugin-transform-block-scoping": "^7.18.9",
@@ -25,36 +25,37 @@
"@monaco-editor/react": "^4.4.6",
"@playwright/test": "^1.42.1",
"@use-gesture/react": "^10.2.22",
"fs": "^0.0.1-security",
"hermes-eslint": "^0.14.0",
"hermes-parser": "^0.22.0",
"invariant": "^2.2.4",
"lz-string": "^1.5.0",
"monaco-editor": "^0.34.1",
"next": "^13.5.6",
"notistack": "^3.0.0-alpha.7",
"prettier": "^3.3.3",
"prettier": "3.0.3",
"pretty-format": "^29.3.1",
"re-resizable": "^6.9.16",
"react": "18.2.0",
"react-compiler-runtime": "*",
"react-dom": "18.2.0"
"react-dom": "18.2.0",
"react-compiler-runtime": "*"
},
"devDependencies": {
"@types/node": "18.11.9",
"@types/prettier": "^2.7.1",
"@types/react": "18.0.25",
"@types/react-dom": "18.0.9",
"autoprefixer": "^10.4.13",
"clsx": "^1.2.1",
"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"
},
"resolutions": {
"./**/@babel/parser": "7.7.4",
"./**/@babel/types": "7.7.4"
"./**/@babel/types": "7.7.4",
"@babel/core": "7.2.0",
"@babel/traverse": "7.1.6",
"@babel/generator": "7.2.0"
}
}

View File

@@ -5,8 +5,8 @@
* LICENSE file in the root directory of this source tree.
*/
import {defineConfig, devices} from '@playwright/test';
import path from 'path';
import { defineConfig, devices } from "@playwright/test";
import path from "path";
// Use process.env.PORT by default and fallback to port 3000
const PORT = process.env.PORT || 3000;
@@ -19,19 +19,19 @@ export default defineConfig({
// Timeout per test
timeout: 30 * 1000,
// Test directory
testDir: path.join(__dirname, '__tests__/e2e'),
testDir: path.join(__dirname, "__tests__/e2e"),
// If a test fails, retry it additional 2 times
retries: 2,
// Artifacts folder where screenshots, videos, and traces are stored.
outputDir: 'test-results/',
outputDir: "test-results/",
// Note: we only use text snapshots, so its safe to omit the host environment name
snapshotPathTemplate: '{testDir}/__snapshots__/{testFilePath}/{arg}{ext}',
snapshotPathTemplate: "{testDir}/__snapshots__/{testFilePath}/{arg}{ext}",
// 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 workspace babel-plugin-react-compiler build && yarn workspace react-compiler-runtime build && yarn dev',
"yarn workspace babel-plugin-react-compiler build && yarn workspace react-compiler-runtime build && yarn dev",
url: baseURL,
timeout: 300 * 1000,
reuseExistingServer: !process.env.CI,
@@ -44,7 +44,7 @@ export default defineConfig({
// Retry a test if its failing with enabled tracing. This allows you to analyze the DOM, console logs, network traffic etc.
// More information: https://playwright.dev/docs/trace-viewer
trace: 'retry-with-trace',
trace: "retry-with-trace",
// All available context options: https://playwright.dev/docs/api/class-browser#browser-new-context
// contextOptions: {
@@ -54,8 +54,8 @@ export default defineConfig({
projects: [
{
name: 'chromium',
use: {...devices['Desktop Chrome']},
name: "chromium",
use: { ...devices["Desktop Chrome"] },
},
// {
// name: 'Desktop Firefox',

View File

@@ -5,16 +5,16 @@
* LICENSE file in the root directory of this source tree.
*/
const {execSync} = require('child_process');
const { execSync } = require("child_process");
// So that we don't need to check them into the repo.
// See https://github.com/reactjs/reactjs.org/blob/main/beta/scripts/downloadFonts.js.
execSync(
'curl https://conf.reactjs.org/fonts/Optimistic_Display_W_Lt.woff2 --output public/fonts/Optimistic_Display_W_Lt.woff2'
"curl https://conf.reactjs.org/fonts/Optimistic_Display_W_Lt.woff2 --output public/fonts/Optimistic_Display_W_Lt.woff2"
);
execSync(
'curl https://conf.reactjs.org/fonts/Optimistic_Display_W_Md.woff2 --output public/fonts/Optimistic_Display_W_Md.woff2'
"curl https://conf.reactjs.org/fonts/Optimistic_Display_W_Md.woff2 --output public/fonts/Optimistic_Display_W_Md.woff2"
);
execSync(
'curl https://conf.reactjs.org/fonts/Optimistic_Display_W_Bd.woff2 --output public/fonts/Optimistic_Display_W_Bd.woff2'
"curl https://conf.reactjs.org/fonts/Optimistic_Display_W_Bd.woff2 --output public/fonts/Optimistic_Display_W_Bd.woff2"
);

View File

@@ -5,33 +5,33 @@
* LICENSE file in the root directory of this source tree.
*/
const defaultTheme = require('tailwindcss/defaultTheme');
const colors = require('./colors');
const defaultTheme = require("tailwindcss/defaultTheme");
const colors = require("./colors");
module.exports = {
content: [
'./app/**/*.{js,ts,jsx,tsx}',
'./pages/**/*.{js,ts,jsx,tsx}',
'./components/**/*.{js,ts,jsx,tsx}',
'./lib/forgetMonacoDiagnostics.ts',
"./app/**/*.{js,ts,jsx,tsx}",
"./pages/**/*.{js,ts,jsx,tsx}",
"./components/**/*.{js,ts,jsx,tsx}",
"./lib/forgetMonacoDiagnostics.ts",
],
theme: {
extend: {
colors,
width: {
toast: 'min(900px, 100vw - 40px)',
'toast-body': 'calc(100% - 60px)',
'toast-title': 'calc(100% - 40px)',
toast: "min(900px, 100vw - 40px)",
"toast-body": "calc(100% - 60px)",
"toast-title": "calc(100% - 40px)",
},
height: {
content: 'calc(100vh - 45px)',
monaco: 'calc(100vh - 93px)',
monaco_small: 'calc(100vh - 129px)',
content: "calc(100vh - 45px)",
monaco: "calc(100vh - 93px)",
monaco_small: "calc(100vh - 129px)",
},
fontFamily: {
sans: [
'Optimistic Display',
'-apple-system',
"Optimistic Display",
"-apple-system",
...defaultTheme.fontFamily.sans,
],
},

View File

@@ -12,9 +12,11 @@
},
"repository": {
"type": "git",
"url": "git+https://github.com/facebook/react.git"
"url": "git+https://github.com/facebook/react-forget.git"
},
"scripts": {
"bundle:meta": "scripts/bundle-meta.sh",
"bundle:oss": "scripts/bundle-oss.sh",
"copyright": "node scripts/copyright.js",
"hash": "scripts/hash.sh",
"start": "yarn workspace playground run start",
@@ -22,6 +24,8 @@
"build": "yarn workspaces run build",
"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",
"prettier:write": "prettier --write . --log-level=warn",
"prettier:ci": "prettier --check . --log-level=warn",
"snap": "yarn workspace babel-plugin-react-compiler run snap",
"snap:build": "yarn workspace snap run build",
"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",
@@ -38,8 +42,7 @@
"concurrently": "^7.4.0",
"folder-hash": "^4.0.4",
"ora": "5.4.1",
"prettier": "^3.3.3",
"prettier-plugin-hermes-parser": "^0.23.0",
"prettier": "^3.2.5",
"prompt-promise": "^1.0.3",
"rollup": "^4.13.2",
"rollup-plugin-banner2": "^1.2.3",

View File

@@ -7,7 +7,7 @@
/** @type {import('jest').Config} */
const config = {
projects: ['<rootDir>/scripts/jest/*.config.js'],
projects: ["<rootDir>/scripts/jest/*.config.js"],
};
module.exports = config;

View File

@@ -1,6 +1,6 @@
{
"name": "babel-plugin-react-compiler",
"version": "0.0.0-experimental-334f00b-20240725",
"version": "0.0.0-experimental-938cd9a-20240601",
"description": "Babel plugin for React Compiler.",
"main": "dist/index.js",
"license": "MIT",
@@ -63,10 +63,5 @@
"@babel/core": "7.2.0",
"@babel/generator": "7.2.0",
"@babel/traverse": "7.7.4"
},
"repository": {
"type": "git",
"url": "git+https://github.com/facebook/react.git",
"directory": "compiler/packages/babel-plugin-react-compiler"
}
}

View File

@@ -5,29 +5,29 @@
* LICENSE file in the root directory of this source tree.
*/
import typescript from '@rollup/plugin-typescript';
import {nodeResolve} from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import json from '@rollup/plugin-json';
import path from 'path';
import process from 'process';
import terser from '@rollup/plugin-terser';
import prettier from 'rollup-plugin-prettier';
import banner2 from 'rollup-plugin-banner2';
import typescript from "@rollup/plugin-typescript";
import { nodeResolve } from "@rollup/plugin-node-resolve";
import commonjs from "@rollup/plugin-commonjs";
import json from "@rollup/plugin-json";
import path from "path";
import process from "process";
import terser from "@rollup/plugin-terser";
import prettier from "rollup-plugin-prettier";
import banner2 from "rollup-plugin-banner2";
const NO_INLINE = new Set(['@babel/types']);
const NO_INLINE = new Set(["@babel/types"]);
const DEV_ROLLUP_CONFIG = {
input: 'src/index.ts',
input: "src/index.ts",
output: {
file: 'dist/index.js',
format: 'cjs',
file: "dist/index.js",
format: "cjs",
sourcemap: false,
exports: 'named',
exports: "named",
},
plugins: [
typescript({
tsconfig: './tsconfig.json',
tsconfig: "./tsconfig.json",
compilerOptions: {
noEmit: true,
},
@@ -35,8 +35,8 @@ const DEV_ROLLUP_CONFIG = {
json(),
nodeResolve({
preferBuiltins: true,
resolveOnly: module => NO_INLINE.has(module) === false,
rootDir: path.join(process.cwd(), '..'),
resolveOnly: (module) => NO_INLINE.has(module) === false,
rootDir: path.join(process.cwd(), ".."),
}),
commonjs(),
terser({

View File

@@ -5,15 +5,15 @@
* LICENSE file in the root directory of this source tree.
*/
import type * as BabelCore from '@babel/core';
import {NodePath} from '@babel/core';
import * as t from '@babel/types';
import type * as BabelCore from "@babel/core";
import { NodePath } from "@babel/core";
import * as t from "@babel/types";
export default function AnnotateReactCodeBabelPlugin(
_babel: typeof BabelCore,
_babel: typeof BabelCore
): BabelCore.PluginObj {
return {
name: 'annotate-react-code',
name: "annotate-react-code",
visitor: {
Program(prog): void {
annotate(prog);
@@ -56,23 +56,23 @@ function buildTypeOfReactForget(): t.Statement {
// typeof globalThis[Symbol.for("react_forget")]
return t.expressionStatement(
t.unaryExpression(
'typeof',
"typeof",
t.memberExpression(
t.identifier('globalThis'),
t.identifier("globalThis"),
t.callExpression(
t.memberExpression(
t.identifier('Symbol'),
t.identifier('for'),
false,
t.identifier("Symbol"),
t.identifier("for"),
false,
false
),
[t.stringLiteral('react_forget')],
[t.stringLiteral("react_forget")]
),
true,
false,
false
),
true,
),
true
)
);
}
@@ -89,9 +89,9 @@ type BabelFn =
| NodePath<t.ArrowFunctionExpression>;
export function isComponentDeclaration(
node: t.FunctionDeclaration,
node: t.FunctionDeclaration
): node is ComponentDeclaration {
return Object.prototype.hasOwnProperty.call(node, '__componentDeclaration');
return Object.prototype.hasOwnProperty.call(node, "__componentDeclaration");
}
/*
@@ -101,7 +101,7 @@ export function isComponentDeclaration(
function isComponentOrHookLike(
node: NodePath<
t.FunctionDeclaration | t.ArrowFunctionExpression | t.FunctionExpression
>,
>
): boolean {
const functionName = getFunctionName(node);
// Check if the name is component or hook like:
@@ -114,7 +114,7 @@ function isComponentOrHookLike(
* helpers are _usually_ named with lowercase, but some code may
* violate this rule
*/
node.get('params').length <= 1
node.get("params").length <= 1
);
} else if (functionName !== null && isHook(functionName)) {
// Hooks have hook invocations or JSX, but can take any # of arguments
@@ -151,11 +151,11 @@ function isHook(path: NodePath<t.Expression | t.PrivateName>): boolean {
} else if (
path.isMemberExpression() &&
!path.node.computed &&
isHook(path.get('property'))
isHook(path.get("property"))
) {
const obj = path.get('object').node;
const obj = path.get("object").node;
const isPascalCaseNameSpace = /^[A-Z].*/;
return obj.type === 'Identifier' && isPascalCaseNameSpace.test(obj.name);
return obj.type === "Identifier" && isPascalCaseNameSpace.test(obj.name);
} else {
return false;
}
@@ -177,8 +177,8 @@ function isComponentName(path: NodePath<t.Expression>): boolean {
function isForwardRefCallback(path: NodePath<t.Expression>): boolean {
return !!(
path.parentPath.isCallExpression() &&
path.parentPath.get('callee').isExpression() &&
isReactAPI(path.parentPath.get('callee'), 'forwardRef')
path.parentPath.get("callee").isExpression() &&
isReactAPI(path.parentPath.get("callee"), "forwardRef")
);
}
@@ -190,22 +190,22 @@ function isForwardRefCallback(path: NodePath<t.Expression>): boolean {
function isMemoCallback(path: NodePath<t.Expression>): boolean {
return (
path.parentPath.isCallExpression() &&
path.parentPath.get('callee').isExpression() &&
isReactAPI(path.parentPath.get('callee'), 'memo')
path.parentPath.get("callee").isExpression() &&
isReactAPI(path.parentPath.get("callee"), "memo")
);
}
function isReactAPI(
path: NodePath<t.Expression | t.PrivateName | t.V8IntrinsicIdentifier>,
functionName: string,
functionName: string
): boolean {
const node = path.node;
return (
(node.type === 'Identifier' && node.name === functionName) ||
(node.type === 'MemberExpression' &&
node.object.type === 'Identifier' &&
node.object.name === 'React' &&
node.property.type === 'Identifier' &&
(node.type === "Identifier" && node.name === functionName) ||
(node.type === "MemberExpression" &&
node.object.type === "Identifier" &&
node.object.name === "React" &&
node.property.type === "Identifier" &&
node.property.name === functionName)
);
}
@@ -218,7 +218,7 @@ function callsHooksOrCreatesJsx(node: NodePath<t.Node>): boolean {
createsJsx = true;
},
CallExpression(call) {
const callee = call.get('callee');
const callee = call.get("callee");
if (callee.isExpression() && isHook(callee)) {
invokesHooks = true;
}
@@ -239,10 +239,10 @@ function callsHooksOrCreatesJsx(node: NodePath<t.Node>): boolean {
function getFunctionName(
path: NodePath<
t.FunctionDeclaration | t.ArrowFunctionExpression | t.FunctionExpression
>,
>
): NodePath<t.Expression> | null {
if (path.isFunctionDeclaration()) {
const id = path.get('id');
const id = path.get("id");
if (id.isIdentifier()) {
return id;
}
@@ -250,31 +250,31 @@ function getFunctionName(
}
let id: NodePath<t.LVal | t.Expression | t.PrivateName> | null = null;
const parent = path.parentPath;
if (parent.isVariableDeclarator() && parent.get('init').node === path.node) {
if (parent.isVariableDeclarator() && parent.get("init").node === path.node) {
// const useHook = () => {};
id = parent.get('id');
id = parent.get("id");
} else if (
parent.isAssignmentExpression() &&
parent.get('right').node === path.node &&
parent.get('operator') === '='
parent.get("right").node === path.node &&
parent.get("operator") === "="
) {
// useHook = () => {};
id = parent.get('left');
id = parent.get("left");
} else if (
parent.isProperty() &&
parent.get('value').node === path.node &&
!parent.get('computed') &&
parent.get('key').isLVal()
parent.get("value").node === path.node &&
!parent.get("computed") &&
parent.get("key").isLVal()
) {
/*
* {useHook: () => {}}
* {useHook() {}}
*/
id = parent.get('key');
id = parent.get("key");
} else if (
parent.isAssignmentPattern() &&
parent.get('right').node === path.node &&
!parent.get('computed')
parent.get("right").node === path.node &&
!parent.get("computed")
) {
/*
* const {useHook = () => {}} = {};
@@ -283,7 +283,7 @@ function getFunctionName(
* Kinda clowny, but we'd said we'd follow spec convention for
* `IsAnonymousFunctionDefinition()` usage.
*/
id = parent.get('left');
id = parent.get("left");
}
if (id !== null && (id.isIdentifier() || id.isMemberExpression())) {
return id;

View File

@@ -5,27 +5,27 @@
* LICENSE file in the root directory of this source tree.
*/
'use strict';
"use strict";
const {tests} = require('./eslint-plugin-react-hooks-test-cases');
const { tests } = require("./eslint-plugin-react-hooks-test-cases");
const {
runBabelPluginReactCompiler,
} = require('../dist/Babel/RunReactCompilerBabelPlugin');
const fs = require('fs');
const path = require('path');
const prettier = require('prettier');
const prettierConfigPath = require.resolve('../.prettierrc');
const process = require('process');
const {createHash} = require('crypto');
const {create} = require('domain');
} = require("../dist/Babel/RunReactCompilerBabelPlugin");
const fs = require("fs");
const path = require("path");
const prettier = require("prettier");
const prettierConfigPath = require.resolve("../.prettierrc");
const process = require("process");
const { createHash } = require("crypto");
const { create } = require("domain");
const FIXTURES_DIR = path.join(
process.cwd(),
'src',
'__tests__',
'fixtures',
'compiler',
'rules-of-hooks'
"src",
"__tests__",
"fixtures",
"compiler",
"rules-of-hooks"
);
const PRETTIER_OPTIONS = prettier.resolveConfig.sync(FIXTURES_DIR, {
@@ -34,10 +34,10 @@ const PRETTIER_OPTIONS = prettier.resolveConfig.sync(FIXTURES_DIR, {
const fixtures = [];
for (const test of tests.valid) {
fixtures.push({code: test.code, valid: true});
fixtures.push({ code: test.code, valid: true });
}
for (const test of tests.invalid) {
fixtures.push({code: test.code, valid: false});
fixtures.push({ code: test.code, valid: false });
}
for (const fixture of fixtures) {
@@ -47,8 +47,8 @@ for (const fixture of fixtures) {
// Does the fixture pass with hooks validation disabled? if not skip it
runBabelPluginReactCompiler(
fixture.code,
'rules-of-hooks.js',
'typescript',
"rules-of-hooks.js",
"typescript",
{
environment: {
validateHooksUsage: false,
@@ -59,8 +59,8 @@ for (const fixture of fixtures) {
try {
runBabelPluginReactCompiler(
fixture.code,
'rules-of-hooks.js',
'typescript',
"rules-of-hooks.js",
"typescript",
{
environment: {
validateHooksUsage: true,
@@ -74,7 +74,7 @@ for (const fixture of fixtures) {
error = e;
}
let code = fixture.code;
let prefix = '';
let prefix = "";
if (error !== null) {
prefix = `todo.bail.`;
code = `// @skip\n// Unsupported input\n${code}`;
@@ -92,11 +92,11 @@ for (const fixture of fixtures) {
code = `// @skip\n// Failed but should have passed\n${code}`;
}
const formatted = prettier.format(code, PRETTIER_OPTIONS);
const hmac = createHash('sha256');
hmac.update(formatted, 'utf8');
const hmac = createHash("sha256");
hmac.update(formatted, "utf8");
let name = `${prefix}rules-of-hooks-${hmac
.digest('hex')
.digest("hex")
.substring(0, 12)}.js`;
const fixturePath = path.join(FIXTURES_DIR, name);
fs.writeFileSync(fixturePath, formatted, 'utf8');
fs.writeFileSync(fixturePath, formatted, "utf8");
}

View File

@@ -5,7 +5,7 @@
* LICENSE file in the root directory of this source tree.
*/
'use strict';
"use strict";
// NOTE: Extracted from https://github.com/facebook/react/blob/main/packages/eslint-plugin-react-hooks/__tests__/ESLintRulesOfHooks-test.js
@@ -13,9 +13,9 @@
* A string template tag that removes padding from the left side of multi-line strings
*/
function normalizeIndent(strings) {
const codeLines = strings[0].split('\n');
const codeLines = strings[0].split("\n");
const leftPadding = codeLines[1].match(/\s+/)[0];
return codeLines.map(line => line.slice(leftPadding.length)).join('\n');
return codeLines.map((line) => line.slice(leftPadding.length)).join("\n");
}
module.exports.tests = {

View File

@@ -5,6 +5,6 @@
* LICENSE file in the root directory of this source tree.
*/
const makeE2EConfig = require('../jest/makeE2EConfig');
const makeE2EConfig = require("../jest/makeE2EConfig");
module.exports = makeE2EConfig('e2e no forget', false);
module.exports = makeE2EConfig("e2e no forget", false);

View File

@@ -5,9 +5,9 @@
* LICENSE file in the root directory of this source tree.
*/
const makeE2EConfig = require('../jest/makeE2EConfig');
const makeE2EConfig = require("../jest/makeE2EConfig");
const config = makeE2EConfig('e2e with forget', true);
config.setupFilesAfterEnv = ['<rootDir>/../scripts/jest/setupEnvE2E.js'];
const config = makeE2EConfig("e2e with forget", true);
config.setupFilesAfterEnv = ["<rootDir>/../scripts/jest/setupEnvE2E.js"];
module.exports = config;

View File

@@ -6,10 +6,10 @@
*/
module.exports = {
displayName: 'main',
preset: 'ts-jest',
rootDir: '../../src',
testPathIgnorePatterns: ['e2e', 'TestDriver', 'test-utils', 'fixtures'],
displayName: "main",
preset: "ts-jest",
rootDir: "../../src",
testPathIgnorePatterns: ["e2e", "TestDriver", "test-utils", "fixtures"],
globals: {
__DEV__: true,
},

View File

@@ -8,27 +8,27 @@
module.exports = function makeE2EConfig(displayName, useForget) {
return {
displayName,
testEnvironment: 'jsdom',
rootDir: '../../src',
testMatch: ['**/*.e2e.(js|tsx)'],
testEnvironment: "jsdom",
rootDir: "../../src",
testMatch: ["**/*.e2e.(js|tsx)"],
modulePathIgnorePatterns: [
// ignore snapshots from the opposite forget configuration
useForget ? '.*\\.no-forget\\.snap$' : '.*\\.with-forget\\.snap$',
useForget ? ".*\\.no-forget\\.snap$" : ".*\\.with-forget\\.snap$",
// ignore snapshots from the main project
'.*\\.ts\\.snap$',
".*\\.ts\\.snap$",
],
globals: {
__FORGET__: useForget,
},
snapshotResolver: useForget
? '<rootDir>/../scripts/jest/snapshot-resolver-with-forget.js'
: '<rootDir>/../scripts/jest/snapshot-resolver-no-forget.js',
? "<rootDir>/../scripts/jest/snapshot-resolver-with-forget.js"
: "<rootDir>/../scripts/jest/snapshot-resolver-no-forget.js",
transform: {
'\\.[tj]sx?$': useForget
? '<rootDir>/../scripts/jest/transform-with-forget'
: '<rootDir>/../scripts/jest/transform-no-forget',
"\\.[tj]sx?$": useForget
? "<rootDir>/../scripts/jest/transform-with-forget"
: "<rootDir>/../scripts/jest/transform-no-forget",
},
transformIgnorePatterns: ['/node_modules/'],
transformIgnorePatterns: ["/node_modules/"],
};
};

View File

@@ -6,7 +6,7 @@
*/
module.exports = function makeSnapshotResolver(useForget) {
const modeExtension = useForget ? '.with-forget' : '.no-forget';
const modeExtension = useForget ? ".with-forget" : ".no-forget";
return {
resolveSnapshotPath: (testPath, snapshotExtension) =>
testPath + modeExtension + snapshotExtension,
@@ -17,6 +17,6 @@ module.exports = function makeSnapshotResolver(useForget) {
-modeExtension.length - snapshotExtension.length
),
testPathForConsistencyCheck: 'some/__tests__/example.test.js',
testPathForConsistencyCheck: "some/__tests__/example.test.js",
};
};

View File

@@ -5,19 +5,19 @@
* LICENSE file in the root directory of this source tree.
*/
import {jsx} from '@babel/plugin-syntax-jsx';
import babelJest from 'babel-jest';
import {compile} from 'babel-plugin-react-compiler';
import {execSync} from 'child_process';
import { jsx } from "@babel/plugin-syntax-jsx";
import babelJest from "babel-jest";
import { compile } from "babel-plugin-react-compiler";
import { execSync } from "child_process";
import type {NodePath, Visitor} from '@babel/traverse';
import type {CallExpression, FunctionDeclaration} from '@babel/types';
import * as t from '@babel/types';
import type { NodePath, Visitor } from "@babel/traverse";
import type { CallExpression, FunctionDeclaration } from "@babel/types";
import * as t from "@babel/types";
import {
EnvironmentConfig,
validateEnvironmentConfig,
} from 'babel-plugin-react-compiler';
import {basename} from 'path';
} from "babel-plugin-react-compiler";
import { basename } from "path";
/**
* -- IMPORTANT --
@@ -28,16 +28,15 @@ import {basename} from 'path';
const e2eTransformerCacheKey = 1;
const forgetOptions: EnvironmentConfig = validateEnvironmentConfig({
enableAssumeHooksFollowRulesOfReact: true,
enableFunctionOutlining: false,
});
const debugMode = process.env['DEBUG_FORGET_COMPILER'] != null;
const debugMode = process.env["DEBUG_FORGET_COMPILER"] != null;
module.exports = (useForget: boolean) => {
function createTransformer() {
return babelJest.createTransformer({
passPerPreset: true,
presets: [
'@babel/preset-typescript',
"@babel/preset-typescript",
{
plugins: [
useForget
@@ -49,36 +48,36 @@ module.exports = (useForget: boolean) => {
* (see https://github.com/jestjs/jest/blob/v29.6.2/packages/babel-jest/src/index.ts#L84)
*/
compilerCacheKey: execSync(
'yarn --silent --cwd ../.. hash packages/babel-plugin-react-compiler/dist',
"yarn --silent --cwd ../.. hash packages/babel-plugin-react-compiler/dist"
).toString(),
transformOptionsCacheKey: forgetOptions,
e2eTransformerCacheKey,
},
]
: '@babel/plugin-syntax-jsx',
: "@babel/plugin-syntax-jsx",
],
},
'@babel/preset-react',
"@babel/preset-react",
{
plugins: [
[
function BabelPluginRewriteRequirePath(): {visitor: Visitor} {
function BabelPluginRewriteRequirePath(): { visitor: Visitor } {
return {
visitor: {
CallExpression(path: NodePath<CallExpression>): void {
const {callee} = path.node;
const { callee } = path.node;
if (
callee.type === 'Identifier' &&
callee.name === 'require'
callee.type === "Identifier" &&
callee.name === "require"
) {
const arg = path.node.arguments[0];
if (arg.type === 'StringLiteral') {
if (arg.type === "StringLiteral") {
/*
* The compiler adds requires of "React", which is expected to be a wrapper
* around the "react" package. For tests, we just rewrite the require.
*/
if (arg.value === 'React') {
arg.value = 'react';
if (arg.value === "React") {
arg.value = "react";
}
}
}
@@ -87,7 +86,7 @@ module.exports = (useForget: boolean) => {
};
},
],
'@babel/plugin-transform-modules-commonjs',
"@babel/plugin-transform-modules-commonjs",
],
},
],
@@ -125,7 +124,7 @@ function isReactComponentLike(fn: NodePath<FunctionDeclaration>): boolean {
fn.traverse({
DirectiveLiteral(path) {
if (path.node.value === 'use no forget') {
if (path.node.value === "use no forget") {
hasNoUseForgetDirective = true;
}
},
@@ -140,7 +139,7 @@ function isReactComponentLike(fn: NodePath<FunctionDeclaration>): boolean {
CallExpression(path) {
// Is there hook usage?
if (
path.node.callee.type === 'Identifier' &&
path.node.callee.type === "Identifier" &&
!/^use[A-Z0-9]/.test(path.node.callee.name)
) {
isReactComponent = true;
@@ -170,7 +169,7 @@ function ReactForgetFunctionTransform() {
const filename = basename(state.file.opts.filename);
if (fn.node.loc && fn.node.id) {
console.log(
` Compiling ${filename}:${fn.node.loc.start.line}:${fn.node.loc.start.column} ${fn.node.id.name}`,
` Compiling ${filename}:${fn.node.loc.start.line}:${fn.node.loc.start.column} ${fn.node.id.name}`
);
} else {
console.log(` Compiling ${filename} ${fn.node.id?.name}`);
@@ -180,11 +179,11 @@ function ReactForgetFunctionTransform() {
const compiled = compile(
fn,
forgetOptions,
'Other',
'_c',
null,
"Other",
"_c",
null,
null,
null
);
compiledFns.add(compiled);
@@ -193,14 +192,14 @@ function ReactForgetFunctionTransform() {
compiled.params,
compiled.body,
compiled.generator,
compiled.async,
compiled.async
);
fn.replaceWith(fun);
fn.skip();
},
};
return {
name: 'react-forget-e2e',
name: "react-forget-e2e",
inherits: jsx,
visitor,
};

View File

@@ -5,7 +5,7 @@
* LICENSE file in the root directory of this source tree.
*/
const ReactCompilerRuntime = require('react/compiler-runtime');
const ReactCompilerRuntime = require("react/compiler-runtime");
/*
* Our e2e babel transform currently only compiles functions, not programs.

View File

@@ -5,6 +5,6 @@
* LICENSE file in the root directory of this source tree.
*/
const makeSnapshotResolver = require('./makeSnapshotResolver');
const makeSnapshotResolver = require("./makeSnapshotResolver");
module.exports = makeSnapshotResolver(false);

View File

@@ -5,6 +5,6 @@
* LICENSE file in the root directory of this source tree.
*/
const makeSnapshotResolver = require('./makeSnapshotResolver');
const makeSnapshotResolver = require("./makeSnapshotResolver");
module.exports = makeSnapshotResolver(true);

View File

@@ -5,4 +5,4 @@
* LICENSE file in the root directory of this source tree.
*/
module.exports = require('./makeTransform')(false);
module.exports = require("./makeTransform")(false);

View File

@@ -5,4 +5,4 @@
* LICENSE file in the root directory of this source tree.
*/
module.exports = require('./makeTransform')(true);
module.exports = require("./makeTransform")(true);

View File

@@ -5,12 +5,12 @@
* LICENSE file in the root directory of this source tree.
*/
import type * as BabelCore from '@babel/core';
import {compileProgram, parsePluginOptions} from '../Entrypoint';
import type * as BabelCore from "@babel/core";
import { compileProgram, parsePluginOptions } from "../Entrypoint";
import {
injectReanimatedFlag,
pipelineUsesReanimatedPlugin,
} from '../Entrypoint/Reanimated';
} from "../Entrypoint/Reanimated";
/*
* The React Forget Babel Plugin
@@ -18,10 +18,10 @@ import {
* @returns
*/
export default function BabelPluginReactCompiler(
_babel: typeof BabelCore,
_babel: typeof BabelCore
): BabelCore.PluginObj {
return {
name: 'react-forget',
name: "react-forget",
visitor: {
/*
* Note: Babel does some "smart" merging of visitors across plugins, so even if A is inserted
@@ -30,16 +30,13 @@ export default function BabelPluginReactCompiler(
*/
Program(prog, pass): void {
let opts = parsePluginOptions(pass.opts);
const isDev =
(typeof __DEV__ !== 'undefined' && __DEV__ === true) ||
process.env['NODE_ENV'] === 'development';
if (
opts.enableReanimatedCheck === true &&
pipelineUsesReanimatedPlugin(pass.file.opts.plugins)
) {
opts = injectReanimatedFlag(opts);
}
if (isDev) {
if (process.env["NODE_ENV"] === "development") {
opts = {
...opts,
environment: {

View File

@@ -5,25 +5,25 @@
* LICENSE file in the root directory of this source tree.
*/
import type * as BabelCore from '@babel/core';
import {transformFromAstSync} from '@babel/core';
import * as BabelParser from '@babel/parser';
import invariant from 'invariant';
import type {PluginOptions} from '../Entrypoint';
import BabelPluginReactCompiler from './BabelPlugin';
import type * as BabelCore from "@babel/core";
import { transformFromAstSync } from "@babel/core";
import * as BabelParser from "@babel/parser";
import invariant from "invariant";
import type { PluginOptions } from "../Entrypoint";
import BabelPluginReactCompiler from "./BabelPlugin";
export const DEFAULT_PLUGINS = ['babel-plugin-fbt', 'babel-plugin-fbt-runtime'];
export const DEFAULT_PLUGINS = ["babel-plugin-fbt", "babel-plugin-fbt-runtime"];
export function runBabelPluginReactCompiler(
text: string,
file: string,
language: 'flow' | 'typescript',
language: "flow" | "typescript",
options: Partial<PluginOptions> | null,
includeAst: boolean = false,
includeAst: boolean = false
): BabelCore.BabelFileResult {
const ast = BabelParser.parse(text, {
sourceFilename: file,
plugins: [language, 'jsx'],
sourceType: 'module',
plugins: [language, "jsx"],
sourceType: "module",
});
const result = transformFromAstSync(ast, text, {
ast: includeAst,
@@ -32,16 +32,16 @@ export function runBabelPluginReactCompiler(
retainLines: true,
plugins: [
[BabelPluginReactCompiler, options],
'babel-plugin-fbt',
'babel-plugin-fbt-runtime',
"babel-plugin-fbt",
"babel-plugin-fbt-runtime",
],
sourceType: 'module',
sourceType: "module",
configFile: false,
babelrc: false,
});
invariant(
result?.code != null,
`Expected BabelPluginReactForget to codegen successfully, got: ${result}`,
`Expected BabelPluginReactForget to codegen successfully, got: ${result}`
);
return result;
}

View File

@@ -5,37 +5,37 @@
* LICENSE file in the root directory of this source tree.
*/
import type {SourceLocation} from './HIR';
import {assertExhaustive} from './Utils/utils';
import type { SourceLocation } from "./HIR";
import { assertExhaustive } from "./Utils/utils";
export enum ErrorSeverity {
/**
* Invalid JS syntax, or valid syntax that is semantically invalid which may indicate some
* misunderstanding on the users part.
*/
InvalidJS = 'InvalidJS',
InvalidJS = "InvalidJS",
/**
* Code that breaks the rules of React.
*/
InvalidReact = 'InvalidReact',
InvalidReact = "InvalidReact",
/**
* Incorrect configuration of the compiler.
*/
InvalidConfig = 'InvalidConfig',
InvalidConfig = "InvalidConfig",
/**
* Code that can reasonably occur and that doesn't break any rules, but is unsafe to preserve
* memoization.
*/
CannotPreserveMemoization = 'CannotPreserveMemoization',
CannotPreserveMemoization = "CannotPreserveMemoization",
/**
* Unhandled syntax that we don't support yet.
*/
Todo = 'Todo',
Todo = "Todo",
/**
* An unexpected internal error in the compiler that indicates critical issues that can panic
* the compiler.
*/
Invariant = 'Invariant',
Invariant = "Invariant",
}
export enum CompilerSuggestionOperation {
@@ -79,19 +79,19 @@ export class CompilerErrorDetail {
this.options = options;
}
get reason(): CompilerErrorDetailOptions['reason'] {
get reason(): CompilerErrorDetailOptions["reason"] {
return this.options.reason;
}
get description(): CompilerErrorDetailOptions['description'] {
get description(): CompilerErrorDetailOptions["description"] {
return this.options.description;
}
get severity(): CompilerErrorDetailOptions['severity'] {
get severity(): CompilerErrorDetailOptions["severity"] {
return this.options.severity;
}
get loc(): CompilerErrorDetailOptions['loc'] {
get loc(): CompilerErrorDetailOptions["loc"] {
return this.options.loc;
}
get suggestions(): CompilerErrorDetailOptions['suggestions'] {
get suggestions(): CompilerErrorDetailOptions["suggestions"] {
return this.options.suggestions;
}
@@ -100,10 +100,10 @@ export class CompilerErrorDetail {
if (this.description != null) {
buffer.push(`. ${this.description}`);
}
if (this.loc != null && typeof this.loc !== 'symbol') {
if (this.loc != null && typeof this.loc !== "symbol") {
buffer.push(` (${this.loc.start.line}:${this.loc.end.line})`);
}
return buffer.join('');
return buffer.join("");
}
toString(): string {
@@ -116,7 +116,7 @@ export class CompilerError extends Error {
static invariant(
condition: unknown,
options: Omit<CompilerErrorDetailOptions, 'severity'>,
options: Omit<CompilerErrorDetailOptions, "severity">
): asserts condition {
if (!condition) {
const errors = new CompilerError();
@@ -124,57 +124,57 @@ export class CompilerError extends Error {
new CompilerErrorDetail({
...options,
severity: ErrorSeverity.Invariant,
}),
})
);
throw errors;
}
}
static throwTodo(
options: Omit<CompilerErrorDetailOptions, 'severity'>,
options: Omit<CompilerErrorDetailOptions, "severity">
): never {
const errors = new CompilerError();
errors.pushErrorDetail(
new CompilerErrorDetail({...options, severity: ErrorSeverity.Todo}),
new CompilerErrorDetail({ ...options, severity: ErrorSeverity.Todo })
);
throw errors;
}
static throwInvalidJS(
options: Omit<CompilerErrorDetailOptions, 'severity'>,
options: Omit<CompilerErrorDetailOptions, "severity">
): never {
const errors = new CompilerError();
errors.pushErrorDetail(
new CompilerErrorDetail({
...options,
severity: ErrorSeverity.InvalidJS,
}),
})
);
throw errors;
}
static throwInvalidReact(
options: Omit<CompilerErrorDetailOptions, 'severity'>,
options: Omit<CompilerErrorDetailOptions, "severity">
): never {
const errors = new CompilerError();
errors.pushErrorDetail(
new CompilerErrorDetail({
...options,
severity: ErrorSeverity.InvalidReact,
}),
})
);
throw errors;
}
static throwInvalidConfig(
options: Omit<CompilerErrorDetailOptions, 'severity'>,
options: Omit<CompilerErrorDetailOptions, "severity">
): never {
const errors = new CompilerError();
errors.pushErrorDetail(
new CompilerErrorDetail({
...options,
severity: ErrorSeverity.InvalidConfig,
}),
})
);
throw errors;
}
@@ -187,7 +187,7 @@ export class CompilerError extends Error {
constructor(...args: Array<any>) {
super(...args);
this.name = 'ReactCompilerError';
this.name = "ReactCompilerError";
}
override get message(): string {
@@ -197,7 +197,7 @@ export class CompilerError extends Error {
override set message(_message: string) {}
override toString(): string {
return this.details.map(detail => detail.toString()).join('\n\n');
return this.details.map((detail) => detail.toString()).join("\n\n");
}
push(options: CompilerErrorDetailOptions): CompilerErrorDetail {
@@ -206,7 +206,7 @@ export class CompilerError extends Error {
description: options.description ?? null,
severity: options.severity,
suggestions: options.suggestions,
loc: typeof options.loc === 'symbol' ? null : options.loc,
loc: typeof options.loc === "symbol" ? null : options.loc,
});
return this.pushErrorDetail(detail);
}
@@ -226,7 +226,7 @@ export class CompilerError extends Error {
* but otherwise continue compiling the rest of the app.
*/
isCritical(): boolean {
return this.details.some(detail => {
return this.details.some((detail) => {
switch (detail.severity) {
case ErrorSeverity.Invariant:
case ErrorSeverity.InvalidJS:
@@ -237,7 +237,7 @@ export class CompilerError extends Error {
case ErrorSeverity.Todo:
return false;
default:
assertExhaustive(detail.severity, 'Unhandled error severity');
assertExhaustive(detail.severity, "Unhandled error severity");
}
});
}

View File

@@ -5,9 +5,9 @@
* LICENSE file in the root directory of this source tree.
*/
import {NodePath} from '@babel/core';
import * as t from '@babel/types';
import {PluginOptions} from './Options';
import { NodePath } from "@babel/core";
import * as t from "@babel/types";
import { PluginOptions } from "./Options";
export function insertGatedFunctionDeclaration(
fnPath: NodePath<
@@ -17,14 +17,15 @@ export function insertGatedFunctionDeclaration(
| t.FunctionDeclaration
| t.ArrowFunctionExpression
| t.FunctionExpression,
gating: NonNullable<PluginOptions['gating']>,
): void {
gating: NonNullable<PluginOptions["gating"]>
): NodePath<t.ConditionalExpression | t.VariableDeclaration> {
const gatingExpression = t.conditionalExpression(
t.callExpression(t.identifier(gating.importSpecifierName), []),
buildFunctionExpression(compiled),
buildFunctionExpression(fnPath.node),
buildFunctionExpression(fnPath.node)
);
let compiledFn;
/*
* Convert function declarations to named variables *unless* this is an
* `export default function ...` since `export default const ...` is
@@ -32,50 +33,33 @@ export function insertGatedFunctionDeclaration(
* conditional expression
*/
if (
fnPath.parentPath.node.type !== 'ExportDefaultDeclaration' &&
fnPath.node.type === 'FunctionDeclaration' &&
fnPath.parentPath.node.type !== "ExportDefaultDeclaration" &&
fnPath.node.type === "FunctionDeclaration" &&
fnPath.node.id != null
) {
fnPath.replaceWith(
t.variableDeclaration('const', [
compiledFn = fnPath.replaceWith(
t.variableDeclaration("const", [
t.variableDeclarator(fnPath.node.id, gatingExpression),
]),
);
} else if (
fnPath.parentPath.node.type === 'ExportDefaultDeclaration' &&
fnPath.node.type !== 'ArrowFunctionExpression' &&
fnPath.node.id != null
) {
fnPath.insertAfter(
t.exportDefaultDeclaration(t.identifier(fnPath.node.id.name)),
);
fnPath.parentPath.replaceWith(
t.variableDeclaration('const', [
t.variableDeclarator(
t.identifier(fnPath.node.id.name),
gatingExpression,
),
]),
);
])
)[0];
} else {
fnPath.replaceWith(gatingExpression);
compiledFn = fnPath.replaceWith(gatingExpression)[0];
}
return compiledFn;
}
function buildFunctionExpression(
node:
| t.FunctionDeclaration
| t.ArrowFunctionExpression
| t.FunctionExpression,
node: t.FunctionDeclaration | t.ArrowFunctionExpression | t.FunctionExpression
): t.ArrowFunctionExpression | t.FunctionExpression {
if (
node.type === 'ArrowFunctionExpression' ||
node.type === 'FunctionExpression'
node.type === "ArrowFunctionExpression" ||
node.type === "FunctionExpression"
) {
return node;
} else {
const fn: t.FunctionExpression = {
type: 'FunctionExpression',
type: "FunctionExpression",
async: node.async,
generator: node.generator,
loc: node.loc ?? null,

View File

@@ -5,19 +5,19 @@
* LICENSE file in the root directory of this source tree.
*/
import {NodePath} from '@babel/core';
import * as t from '@babel/types';
import {CompilerError} from '../CompilerError';
import {ExternalFunction, GeneratedSource} from '../HIR';
import {getOrInsertDefault} from '../Utils/utils';
import { NodePath } from "@babel/core";
import * as t from "@babel/types";
import { CompilerError } from "../CompilerError";
import { ExternalFunction, GeneratedSource } from "../HIR";
import { getOrInsertDefault } from "../Utils/utils";
export function addImportsToProgram(
path: NodePath<t.Program>,
importList: Array<ExternalFunction>,
importList: Array<ExternalFunction>
): void {
const identifiers: Set<string> = new Set();
const sortedImports: Map<string, Array<string>> = new Map();
for (const {importSpecifierName, source} of importList) {
for (const { importSpecifierName, source } of importList) {
/*
* Codegen currently does not rename import specifiers, so we do additional
* validation here
@@ -35,28 +35,28 @@ export function addImportsToProgram(
description: null,
loc: GeneratedSource,
suggestions: null,
},
}
);
identifiers.add(importSpecifierName);
const importSpecifierNameList = getOrInsertDefault(
sortedImports,
source,
[],
[]
);
importSpecifierNameList.push(importSpecifierName);
}
const stmts: Array<t.ImportDeclaration> = [];
for (const [source, importSpecifierNameList] of sortedImports) {
const importSpecifiers = importSpecifierNameList.map(name => {
const importSpecifiers = importSpecifierNameList.map((name) => {
const id = t.identifier(name);
return t.importSpecifier(id, id);
});
stmts.push(t.importDeclaration(importSpecifiers, t.stringLiteral(source)));
}
path.unshiftContainer('body', stmts);
path.unshiftContainer("body", stmts);
}
/*
@@ -65,21 +65,21 @@ export function addImportsToProgram(
*/
function isNonNamespacedImport(
importDeclPath: NodePath<t.ImportDeclaration>,
moduleName: string,
moduleName: string
): boolean {
return (
importDeclPath.get('source').node.value === moduleName &&
importDeclPath.get("source").node.value === moduleName &&
importDeclPath
.get('specifiers')
.every(specifier => specifier.isImportSpecifier()) &&
importDeclPath.node.importKind !== 'type' &&
importDeclPath.node.importKind !== 'typeof'
.get("specifiers")
.every((specifier) => specifier.isImportSpecifier()) &&
importDeclPath.node.importKind !== "type" &&
importDeclPath.node.importKind !== "typeof"
);
}
function hasExistingNonNamespacedImportOfModule(
program: NodePath<t.Program>,
moduleName: string,
moduleName: string
): boolean {
let hasExistingImport = false;
program.traverse({
@@ -100,7 +100,7 @@ function hasExistingNonNamespacedImportOfModule(
function addMemoCacheFunctionSpecifierToExistingImport(
program: NodePath<t.Program>,
moduleName: string,
identifierName: string,
identifierName: string
): boolean {
let didInsertUseMemoCache = false;
program.traverse({
@@ -110,8 +110,8 @@ function addMemoCacheFunctionSpecifierToExistingImport(
isNonNamespacedImport(importDeclPath, moduleName)
) {
importDeclPath.pushContainer(
'specifiers',
t.importSpecifier(t.identifier(identifierName), t.identifier('c')),
"specifiers",
t.importSpecifier(t.identifier(identifierName), t.identifier("c"))
);
didInsertUseMemoCache = true;
}
@@ -123,7 +123,7 @@ function addMemoCacheFunctionSpecifierToExistingImport(
export function updateMemoCacheFunctionImport(
program: NodePath<t.Program>,
moduleName: string,
useMemoCacheIdentifier: string,
useMemoCacheIdentifier: string
): void {
/*
* If there isn't already an import of * as React, insert it so useMemoCache doesn't
@@ -131,25 +131,25 @@ export function updateMemoCacheFunctionImport(
*/
const hasExistingImport = hasExistingNonNamespacedImportOfModule(
program,
moduleName,
moduleName
);
if (hasExistingImport) {
const didUpdateImport = addMemoCacheFunctionSpecifierToExistingImport(
program,
moduleName,
useMemoCacheIdentifier,
useMemoCacheIdentifier
);
if (!didUpdateImport) {
throw new Error(
`Expected an ImportDeclaration of \`${moduleName}\` in order to update ImportSpecifiers with useMemoCache`,
`Expected an ImportDeclaration of \`${moduleName}\` in order to update ImportSpecifiers with useMemoCache`
);
}
} else {
addMemoCacheFunctionImportDeclaration(
program,
moduleName,
useMemoCacheIdentifier,
useMemoCacheIdentifier
);
}
}
@@ -157,13 +157,13 @@ export function updateMemoCacheFunctionImport(
function addMemoCacheFunctionImportDeclaration(
program: NodePath<t.Program>,
moduleName: string,
localName: string,
localName: string
): void {
program.unshiftContainer(
'body',
"body",
t.importDeclaration(
[t.importSpecifier(t.identifier(localName), t.identifier('c'))],
t.stringLiteral(moduleName),
),
[t.importSpecifier(t.identifier(localName), t.identifier("c"))],
t.stringLiteral(moduleName)
)
);
}

View File

@@ -5,11 +5,11 @@
* LICENSE file in the root directory of this source tree.
*/
import * as t from '@babel/types';
import {z} from 'zod';
import {CompilerErrorDetailOptions} from '../CompilerError';
import {ExternalFunction, PartialEnvironmentConfig} from '../HIR/Environment';
import {hasOwnProperty} from '../Utils/utils';
import * as t from "@babel/types";
import { z } from "zod";
import { CompilerErrorDetailOptions } from "../CompilerError";
import { ExternalFunction, PartialEnvironmentConfig } from "../HIR/Environment";
import { hasOwnProperty } from "../Utils/utils";
const PanicThresholdOptionsSchema = z.enum([
/*
@@ -18,15 +18,15 @@ const PanicThresholdOptionsSchema = z.enum([
* If Forget is invoked through `BabelPluginReactCompiler`, this will at the least
* skip Forget compilation for the rest of current file.
*/
'all_errors',
"all_errors",
/*
* Panic by throwing an exception only on critical or unrecognized errors.
* For all other errors, skip the erroring function without inserting
* a Forget-compiled version (i.e. same behavior as noEmit).
*/
'critical_errors',
"critical_errors",
// Never panic by throwing an exception.
'none',
"none",
]);
export type PanicThresholdOptions = z.infer<typeof PanicThresholdOptionsSchema>;
@@ -130,13 +130,11 @@ const CompilationModeSchema = z.enum([
* false positives, since compilation has a greater impact than linting.
* This is the default mode
*/
'infer',
// Compile only components using Flow component syntax and hooks using hook syntax.
'syntax',
"infer",
// Compile only functions which are explicitly annotated with "use forget"
'annotation',
"annotation",
// Compile all top-level functions
'all',
"all",
]);
export type CompilationMode = z.infer<typeof CompilationModeSchema>;
@@ -156,27 +154,24 @@ export type CompilationMode = z.infer<typeof CompilationModeSchema>;
*/
export type LoggerEvent =
| {
kind: 'CompileError';
kind: "CompileError";
fnLoc: t.SourceLocation | null;
detail: CompilerErrorDetailOptions;
}
| {
kind: 'CompileDiagnostic';
kind: "CompileDiagnostic";
fnLoc: t.SourceLocation | null;
detail: Omit<Omit<CompilerErrorDetailOptions, 'severity'>, 'suggestions'>;
detail: Omit<Omit<CompilerErrorDetailOptions, "severity">, "suggestions">;
}
| {
kind: 'CompileSuccess';
kind: "CompileSuccess";
fnLoc: t.SourceLocation | null;
fnName: string | null;
memoSlots: number;
memoBlocks: number;
memoValues: number;
prunedMemoBlocks: number;
prunedMemoValues: number;
}
| {
kind: 'PipelineError';
kind: "PipelineError";
fnLoc: t.SourceLocation | null;
data: string;
};
@@ -186,8 +181,8 @@ export type Logger = {
};
export const defaultOptions: PluginOptions = {
compilationMode: 'infer',
panicThreshold: 'none',
compilationMode: "infer",
panicThreshold: "none",
environment: {},
logger: null,
gating: null,
@@ -196,19 +191,19 @@ export const defaultOptions: PluginOptions = {
eslintSuppressionRules: null,
flowSuppressions: false,
ignoreUseNoForget: false,
sources: filename => {
return filename.indexOf('node_modules') === -1;
sources: (filename) => {
return filename.indexOf("node_modules") === -1;
},
enableReanimatedCheck: true,
} as const;
export function parsePluginOptions(obj: unknown): PluginOptions {
if (obj == null || typeof obj !== 'object') {
if (obj == null || typeof obj !== "object") {
return defaultOptions;
}
const parsedOptions = Object.create(null);
for (let [key, value] of Object.entries(obj)) {
if (typeof value === 'string') {
if (typeof value === "string") {
// normalize string configs to be case insensitive
value = value.toLowerCase();
}
@@ -216,7 +211,7 @@ export function parsePluginOptions(obj: unknown): PluginOptions {
parsedOptions[key] = value;
}
}
return {...defaultOptions, ...parsedOptions};
return { ...defaultOptions, ...parsedOptions };
}
function isCompilerFlag(s: string): s is keyof PluginOptions {

View File

@@ -5,15 +5,14 @@
* LICENSE file in the root directory of this source tree.
*/
import {NodePath} from '@babel/traverse';
import * as t from '@babel/types';
import prettyFormat from 'pretty-format';
import {Logger} from '.';
import { NodePath } from "@babel/traverse";
import * as t from "@babel/types";
import prettyFormat from "pretty-format";
import { Logger } from ".";
import {
HIRFunction,
ReactiveFunction,
assertConsistentIdentifiers,
assertTerminalPredsExist,
assertTerminalSuccessorsExist,
assertValidBlockNesting,
assertValidMutableRanges,
@@ -22,13 +21,13 @@ import {
mergeConsecutiveBlocks,
mergeOverlappingReactiveScopesHIR,
pruneUnusedLabelsHIR,
} from '../HIR';
} from "../HIR";
import {
Environment,
EnvironmentConfig,
ReactFunctionType,
} from '../HIR/Environment';
import {findContextIdentifiers} from '../HIR/FindContextIdentifiers';
} from "../HIR/Environment";
import { findContextIdentifiers } from "../HIR/FindContextIdentifiers";
import {
analyseFunctions,
dropManualMemoization,
@@ -36,13 +35,12 @@ import {
inferReactivePlaces,
inferReferenceEffects,
inlineImmediatelyInvokedFunctionExpressions,
} from '../Inference';
} from "../Inference";
import {
constantPropagation,
deadCodeElimination,
pruneMaybeThrows,
} from '../Optimization';
import {instructionReordering} from '../Optimization/InstructionReordering';
} from "../Optimization";
import {
CodegenFunction,
alignObjectMethodScopes,
@@ -69,23 +67,20 @@ import {
pruneUnusedLabels,
pruneUnusedScopes,
renameVariables,
} from '../ReactiveScopes';
import {alignMethodCallScopes} from '../ReactiveScopes/AlignMethodCallScopes';
import {alignReactiveScopesToBlockScopesHIR} from '../ReactiveScopes/AlignReactiveScopesToBlockScopesHIR';
import {flattenReactiveLoopsHIR} from '../ReactiveScopes/FlattenReactiveLoopsHIR';
import {flattenScopesWithHooksOrUseHIR} from '../ReactiveScopes/FlattenScopesWithHooksOrUseHIR';
import {pruneAlwaysInvalidatingScopes} from '../ReactiveScopes/PruneAlwaysInvalidatingScopes';
import pruneInitializationDependencies from '../ReactiveScopes/PruneInitializationDependencies';
import {stabilizeBlockIds} from '../ReactiveScopes/StabilizeBlockIds';
import {eliminateRedundantPhi, enterSSA, leaveSSA} from '../SSA';
import {inferTypes} from '../TypeInference';
} from "../ReactiveScopes";
import { alignMethodCallScopes } from "../ReactiveScopes/AlignMethodCallScopes";
import { alignReactiveScopesToBlockScopesHIR } from "../ReactiveScopes/AlignReactiveScopesToBlockScopesHIR";
import { pruneAlwaysInvalidatingScopes } from "../ReactiveScopes/PruneAlwaysInvalidatingScopes";
import { stabilizeBlockIds } from "../ReactiveScopes/StabilizeBlockIds";
import { eliminateRedundantPhi, enterSSA, leaveSSA } from "../SSA";
import { inferTypes } from "../TypeInference";
import {
logCodegenFunction,
logDebug,
logHIRFunction,
logReactiveFunction,
} from '../Utils/logger';
import {assertExhaustive} from '../Utils/utils';
} from "../Utils/logger";
import { assertExhaustive } from "../Utils/utils";
import {
validateContextVariableLValues,
validateHooksUsage,
@@ -95,15 +90,14 @@ import {
validateNoSetStateInRender,
validatePreservedManualMemoization,
validateUseMemo,
} from '../Validation';
import {validateLocalsNotReassignedAfterRender} from '../Validation/ValidateLocalsNotReassignedAfterRender';
import {outlineFunctions} from '../Optimization/OutlineFunctions';
} from "../Validation";
import pruneInitializationDependencies from "../ReactiveScopes/PruneInitializationDependencies";
export type CompilerPipelineValue =
| {kind: 'ast'; name: string; value: CodegenFunction}
| {kind: 'hir'; name: string; value: HIRFunction}
| {kind: 'reactive'; name: string; value: ReactiveFunction}
| {kind: 'debug'; name: string; value: string};
| { kind: "ast"; name: string; value: CodegenFunction }
| { kind: "hir"; name: string; value: HIRFunction }
| { kind: "reactive"; name: string; value: ReactiveFunction }
| { kind: "debug"; name: string; value: string };
export function* run(
func: NodePath<
@@ -114,22 +108,21 @@ export function* run(
useMemoCacheIdentifier: string,
logger: Logger | null,
filename: string | null,
code: string | null,
code: string | null
): Generator<CompilerPipelineValue, CodegenFunction> {
const contextIdentifiers = findContextIdentifiers(func);
const env = new Environment(
func.scope,
fnType,
config,
contextIdentifiers,
logger,
filename,
code,
useMemoCacheIdentifier,
useMemoCacheIdentifier
);
yield {
kind: 'debug',
name: 'EnvironmentConfig',
kind: "debug",
name: "EnvironmentConfig",
value: prettyFormat(env.config),
};
const ast = yield* runWithEnvironment(func, env);
@@ -144,13 +137,13 @@ function* runWithEnvironment(
func: NodePath<
t.FunctionDeclaration | t.ArrowFunctionExpression | t.FunctionExpression
>,
env: Environment,
env: Environment
): Generator<CompilerPipelineValue, CodegenFunction> {
const hir = lower(func, env).unwrap();
yield log({kind: 'hir', name: 'HIR', value: hir});
yield log({ kind: "hir", name: "HIR", value: hir });
pruneMaybeThrows(hir);
yield log({kind: 'hir', name: 'PruneMaybeThrows', value: hir});
yield log({ kind: "hir", name: "PruneMaybeThrows", value: hir });
validateContextVariableLValues(hir);
validateUseMemo(hir);
@@ -161,35 +154,35 @@ function* runWithEnvironment(
!env.config.enableChangeDetectionForDebugging
) {
dropManualMemoization(hir);
yield log({kind: 'hir', name: 'DropManualMemoization', value: hir});
yield log({ kind: "hir", name: "DropManualMemoization", value: hir });
}
inlineImmediatelyInvokedFunctionExpressions(hir);
yield log({
kind: 'hir',
name: 'InlineImmediatelyInvokedFunctionExpressions',
kind: "hir",
name: "InlineImmediatelyInvokedFunctionExpressions",
value: hir,
});
mergeConsecutiveBlocks(hir);
yield log({kind: 'hir', name: 'MergeConsecutiveBlocks', value: hir});
yield log({ kind: "hir", name: "MergeConsecutiveBlocks", value: hir });
assertConsistentIdentifiers(hir);
assertTerminalSuccessorsExist(hir);
enterSSA(hir);
yield log({kind: 'hir', name: 'SSA', value: hir});
yield log({ kind: "hir", name: "SSA", value: hir });
eliminateRedundantPhi(hir);
yield log({kind: 'hir', name: 'EliminateRedundantPhi', value: hir});
yield log({ kind: "hir", name: "EliminateRedundantPhi", value: hir });
assertConsistentIdentifiers(hir);
constantPropagation(hir);
yield log({kind: 'hir', name: 'ConstantPropagation', value: hir});
yield log({ kind: "hir", name: "ConstantPropagation", value: hir });
inferTypes(hir);
yield log({kind: 'hir', name: 'InferTypes', value: hir});
yield log({ kind: "hir", name: "InferTypes", value: hir });
if (env.config.validateHooksUsage) {
validateHooksUsage(hir);
@@ -200,27 +193,20 @@ function* runWithEnvironment(
}
analyseFunctions(hir);
yield log({kind: 'hir', name: 'AnalyseFunctions', value: hir});
yield log({ kind: "hir", name: "AnalyseFunctions", value: hir });
inferReferenceEffects(hir);
yield log({kind: 'hir', name: 'InferReferenceEffects', value: hir});
validateLocalsNotReassignedAfterRender(hir);
yield log({ kind: "hir", name: "InferReferenceEffects", value: hir });
// Note: Has to come after infer reference effects because "dead" code may still affect inference
deadCodeElimination(hir);
yield log({kind: 'hir', name: 'DeadCodeElimination', value: hir});
if (env.config.enableInstructionReordering) {
instructionReordering(hir);
yield log({kind: 'hir', name: 'InstructionReordering', value: hir});
}
yield log({ kind: "hir", name: "DeadCodeElimination", value: hir });
pruneMaybeThrows(hir);
yield log({kind: 'hir', name: 'PruneMaybeThrows', value: hir});
yield log({ kind: "hir", name: "PruneMaybeThrows", value: hir });
inferMutableRanges(hir);
yield log({kind: 'hir', name: 'InferMutableRanges', value: hir});
yield log({ kind: "hir", name: "InferMutableRanges", value: hir });
if (env.config.assertValidMutableRanges) {
assertValidMutableRanges(hir);
@@ -235,93 +221,72 @@ function* runWithEnvironment(
}
inferReactivePlaces(hir);
yield log({kind: 'hir', name: 'InferReactivePlaces', value: hir});
yield log({ kind: "hir", name: "InferReactivePlaces", value: hir });
leaveSSA(hir);
yield log({kind: 'hir', name: 'LeaveSSA', value: hir});
yield log({ kind: "hir", name: "LeaveSSA", value: hir });
inferReactiveScopeVariables(hir);
yield log({kind: 'hir', name: 'InferReactiveScopeVariables', value: hir});
if (env.config.enableFunctionOutlining) {
outlineFunctions(hir);
yield log({kind: 'hir', name: 'OutlineFunctions', value: hir});
}
yield log({ kind: "hir", name: "InferReactiveScopeVariables", value: hir });
alignMethodCallScopes(hir);
yield log({
kind: 'hir',
name: 'AlignMethodCallScopes',
kind: "hir",
name: "AlignMethodCallScopes",
value: hir,
});
alignObjectMethodScopes(hir);
yield log({
kind: 'hir',
name: 'AlignObjectMethodScopes',
kind: "hir",
name: "AlignObjectMethodScopes",
value: hir,
});
const fbtOperands = memoizeFbtOperandsInSameScope(hir);
memoizeFbtOperandsInSameScope(hir);
yield log({
kind: 'hir',
name: 'MemoizeFbtAndMacroOperandsInSameScope',
kind: "hir",
name: "MemoizeFbtOperandsInSameScope",
value: hir,
});
if (env.config.enableReactiveScopesInHIR) {
pruneUnusedLabelsHIR(hir);
yield log({
kind: 'hir',
name: 'PruneUnusedLabelsHIR',
kind: "hir",
name: "PruneUnusedLabelsHIR",
value: hir,
});
alignReactiveScopesToBlockScopesHIR(hir);
yield log({
kind: 'hir',
name: 'AlignReactiveScopesToBlockScopesHIR',
kind: "hir",
name: "AlignReactiveScopesToBlockScopesHIR",
value: hir,
});
mergeOverlappingReactiveScopesHIR(hir);
yield log({
kind: 'hir',
name: 'MergeOverlappingReactiveScopesHIR',
kind: "hir",
name: "MergeOverlappingReactiveScopesHIR",
value: hir,
});
assertValidBlockNesting(hir);
buildReactiveScopeTerminalsHIR(hir);
yield log({
kind: 'hir',
name: 'BuildReactiveScopeTerminalsHIR',
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);
yield log({
kind: 'reactive',
name: 'BuildReactiveFunction',
kind: "reactive",
name: "BuildReactiveFunction",
value: reactiveFunction,
});
@@ -329,147 +294,147 @@ function* runWithEnvironment(
pruneUnusedLabels(reactiveFunction);
yield log({
kind: 'reactive',
name: 'PruneUnusedLabels',
kind: "reactive",
name: "PruneUnusedLabels",
value: reactiveFunction,
});
if (!env.config.enableReactiveScopesInHIR) {
alignReactiveScopesToBlockScopes(reactiveFunction);
yield log({
kind: 'reactive',
name: 'AlignReactiveScopesToBlockScopes',
kind: "reactive",
name: "AlignReactiveScopesToBlockScopes",
value: reactiveFunction,
});
mergeOverlappingReactiveScopes(reactiveFunction);
yield log({
kind: 'reactive',
name: 'MergeOverlappingReactiveScopes',
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',
kind: "reactive",
name: "BuildReactiveBlocks",
value: reactiveFunction,
});
}
flattenReactiveLoops(reactiveFunction);
yield log({
kind: "reactive",
name: "FlattenReactiveLoops",
value: reactiveFunction,
});
assertScopeInstructionsWithinScopes(reactiveFunction);
flattenScopesWithHooksOrUse(reactiveFunction);
yield log({
kind: "reactive",
name: "FlattenScopesWithHooks",
value: reactiveFunction,
});
propagateScopeDependencies(reactiveFunction);
yield log({
kind: 'reactive',
name: 'PropagateScopeDependencies',
kind: "reactive",
name: "PropagateScopeDependencies",
value: reactiveFunction,
});
pruneNonEscapingScopes(reactiveFunction);
yield log({
kind: 'reactive',
name: 'PruneNonEscapingScopes',
kind: "reactive",
name: "PruneNonEscapingScopes",
value: reactiveFunction,
});
pruneNonReactiveDependencies(reactiveFunction);
yield log({
kind: 'reactive',
name: 'PruneNonReactiveDependencies',
kind: "reactive",
name: "PruneNonReactiveDependencies",
value: reactiveFunction,
});
pruneUnusedScopes(reactiveFunction);
yield log({
kind: 'reactive',
name: 'PruneUnusedScopes',
kind: "reactive",
name: "PruneUnusedScopes",
value: reactiveFunction,
});
mergeReactiveScopesThatInvalidateTogether(reactiveFunction);
yield log({
kind: 'reactive',
name: 'MergeReactiveScopesThatInvalidateTogether',
kind: "reactive",
name: "MergeReactiveScopesThatInvalidateTogether",
value: reactiveFunction,
});
pruneAlwaysInvalidatingScopes(reactiveFunction);
yield log({
kind: 'reactive',
name: 'PruneAlwaysInvalidatingScopes',
kind: "reactive",
name: "PruneAlwaysInvalidatingScopes",
value: reactiveFunction,
});
if (env.config.enableChangeDetectionForDebugging != null) {
pruneInitializationDependencies(reactiveFunction);
yield log({
kind: 'reactive',
name: 'PruneInitializationDependencies',
kind: "reactive",
name: "PruneInitializationDependencies",
value: reactiveFunction,
});
}
propagateEarlyReturns(reactiveFunction);
yield log({
kind: 'reactive',
name: 'PropagateEarlyReturns',
kind: "reactive",
name: "PropagateEarlyReturns",
value: reactiveFunction,
});
promoteUsedTemporaries(reactiveFunction);
yield log({
kind: 'reactive',
name: 'PromoteUsedTemporaries',
kind: "reactive",
name: "PromoteUsedTemporaries",
value: reactiveFunction,
});
pruneUnusedLValues(reactiveFunction);
yield log({
kind: 'reactive',
name: 'PruneUnusedLValues',
kind: "reactive",
name: "PruneUnusedLValues",
value: reactiveFunction,
});
extractScopeDeclarationsFromDestructuring(reactiveFunction);
yield log({
kind: 'reactive',
name: 'ExtractScopeDeclarationsFromDestructuring',
kind: "reactive",
name: "ExtractScopeDeclarationsFromDestructuring",
value: reactiveFunction,
});
stabilizeBlockIds(reactiveFunction);
yield log({
kind: 'reactive',
name: 'StabilizeBlockIds',
kind: "reactive",
name: "StabilizeBlockIds",
value: reactiveFunction,
});
const uniqueIdentifiers = renameVariables(reactiveFunction);
yield log({
kind: 'reactive',
name: 'RenameVariables',
kind: "reactive",
name: "RenameVariables",
value: reactiveFunction,
});
pruneHoistedContexts(reactiveFunction);
yield log({
kind: 'reactive',
name: 'PruneHoistedContexts',
kind: "reactive",
name: "PruneHoistedContexts",
value: reactiveFunction,
});
@@ -484,14 +449,8 @@ function* runWithEnvironment(
validatePreservedManualMemoization(reactiveFunction);
}
const ast = codegenFunction(reactiveFunction, {
uniqueIdentifiers,
fbtOperands,
}).unwrap();
yield log({kind: 'ast', name: 'Codegen', value: ast});
for (const outlined of ast.outlined) {
yield log({kind: 'ast', name: 'Codegen (outlined)', value: outlined.fn});
}
const ast = codegenFunction(reactiveFunction, uniqueIdentifiers).unwrap();
yield log({ kind: "ast", name: "Codegen", value: ast });
/**
* This flag should be only set for unit / fixture tests to check
@@ -499,7 +458,7 @@ function* runWithEnvironment(
* thrown by babel functions or other unexpected exceptions).
*/
if (env.config.throwUnknownException__testonly) {
throw new Error('unexpected error');
throw new Error("unexpected error");
}
return ast;
@@ -514,7 +473,7 @@ export function compileFn(
useMemoCacheIdentifier: string,
logger: Logger | null,
filename: string | null,
code: string | null,
code: string | null
): CodegenFunction {
let generator = run(
func,
@@ -523,7 +482,7 @@ export function compileFn(
useMemoCacheIdentifier,
logger,
filename,
code,
code
);
while (true) {
const next = generator.next();
@@ -535,24 +494,24 @@ export function compileFn(
export function log(value: CompilerPipelineValue): CompilerPipelineValue {
switch (value.kind) {
case 'ast': {
case "ast": {
logCodegenFunction(value.name, value.value);
break;
}
case 'hir': {
case "hir": {
logHIRFunction(value.name, value.value);
break;
}
case 'reactive': {
case "reactive": {
logReactiveFunction(value.name, value.value);
break;
}
case 'debug': {
case "debug": {
logDebug(value.name, value.value);
break;
}
default: {
assertExhaustive(value, 'Unexpected compilation kind');
assertExhaustive(value, "Unexpected compilation kind");
}
}
return value;

View File

@@ -1,13 +1,13 @@
import type * as BabelCore from '@babel/core';
import {hasOwnProperty} from '../Utils/utils';
import {PluginOptions} from './Options';
import type * as BabelCore from "@babel/core";
import { hasOwnProperty } from "../Utils/utils";
import { PluginOptions } from "./Options";
function hasModule(name: string): boolean {
try {
return !!require.resolve(name);
} catch (error: any) {
if (
error.code === 'MODULE_NOT_FOUND' &&
error.code === "MODULE_NOT_FOUND" &&
error.message.indexOf(name) !== -1
) {
return false;
@@ -24,22 +24,22 @@ function hasModule(name: string): boolean {
* See https://github.com/expo/expo/blob/e4b8d86442482c7316365a6b7ec1141eec73409d/packages/babel-preset-expo/src/index.ts#L300-L301
*/
export function pipelineUsesReanimatedPlugin(
plugins: Array<BabelCore.PluginItem> | null | undefined,
plugins: Array<BabelCore.PluginItem> | null | undefined
): boolean {
if (Array.isArray(plugins)) {
for (const plugin of plugins) {
if (hasOwnProperty(plugin, 'key')) {
if (hasOwnProperty(plugin, "key")) {
const key = (plugin as any).key; // already checked
if (
typeof key === 'string' &&
key.indexOf('react-native-reanimated') !== -1
typeof key === "string" &&
key.indexOf("react-native-reanimated") !== -1
) {
return true;
}
}
}
}
return hasModule('react-native-reanimated');
return hasModule("react-native-reanimated");
}
export function injectReanimatedFlag(options: PluginOptions): PluginOptions {

View File

@@ -5,15 +5,15 @@
* LICENSE file in the root directory of this source tree.
*/
import {NodePath} from '@babel/core';
import * as t from '@babel/types';
import { NodePath } from "@babel/core";
import * as t from "@babel/types";
import {
CompilerError,
CompilerErrorDetail,
CompilerSuggestionOperation,
ErrorSeverity,
} from '../CompilerError';
import {assertExhaustive} from '../Utils/utils';
} from "../CompilerError";
import { assertExhaustive } from "../Utils/utils";
/**
* Captures the start and end range of a pair of eslint-disable ... eslint-enable comments. In the
@@ -29,7 +29,7 @@ export type SuppressionRange = {
source: SuppressionSource;
};
type SuppressionSource = 'Eslint' | 'Flow';
type SuppressionSource = "Eslint" | "Flow";
/**
* An suppression affects a function if:
@@ -38,7 +38,7 @@ type SuppressionSource = 'Eslint' | 'Flow';
*/
export function filterSuppressionsThatAffectFunction(
suppressionRanges: Array<SuppressionRange>,
fn: NodePath<t.Function>,
fn: NodePath<t.Function>
): Array<SuppressionRange> {
const suppressionsInScope: Array<SuppressionRange> = [];
const fnNode = fn.node;
@@ -78,21 +78,21 @@ export function filterSuppressionsThatAffectFunction(
export function findProgramSuppressions(
programComments: Array<t.Comment>,
ruleNames: Array<string>,
flowSuppressions: boolean,
flowSuppressions: boolean
): Array<SuppressionRange> {
const suppressionRanges: Array<SuppressionRange> = [];
let disableComment: t.Comment | null = null;
let enableComment: t.Comment | null = null;
let source: SuppressionSource | null = null;
const rulePattern = `(${ruleNames.join('|')})`;
const rulePattern = `(${ruleNames.join("|")})`;
const disableNextLinePattern = new RegExp(
`eslint-disable-next-line ${rulePattern}`,
`eslint-disable-next-line ${rulePattern}`
);
const disablePattern = new RegExp(`eslint-disable ${rulePattern}`);
const enablePattern = new RegExp(`eslint-enable ${rulePattern}`);
const flowSuppressionPattern = new RegExp(
'\\$(FlowFixMe\\w*|FlowExpectedError|FlowIssue)\\[react\\-rule',
"\\$(FlowFixMe\\w*|FlowExpectedError|FlowIssue)\\[react\\-rule"
);
for (const comment of programComments) {
@@ -110,7 +110,7 @@ export function findProgramSuppressions(
) {
disableComment = comment;
enableComment = comment;
source = 'Eslint';
source = "Eslint";
}
if (
@@ -120,15 +120,15 @@ export function findProgramSuppressions(
) {
disableComment = comment;
enableComment = comment;
source = 'Flow';
source = "Flow";
}
if (disablePattern.test(comment.value)) {
disableComment = comment;
source = 'Eslint';
source = "Eslint";
}
if (enablePattern.test(comment.value) && source === 'Eslint') {
if (enablePattern.test(comment.value) && source === "Eslint") {
enableComment = comment;
}
@@ -147,7 +147,7 @@ export function findProgramSuppressions(
}
export function suppressionsToCompilerError(
suppressionRanges: Array<SuppressionRange>,
suppressionRanges: Array<SuppressionRange>
): CompilerError | null {
if (suppressionRanges.length === 0) {
return null;
@@ -162,21 +162,21 @@ export function suppressionsToCompilerError(
}
let reason, suggestion;
switch (suppressionRange.source) {
case 'Eslint':
case "Eslint":
reason =
'React Compiler has skipped optimizing this component because one or more React ESLint rules were disabled';
"React Compiler has skipped optimizing this component because one or more React ESLint rules were disabled";
suggestion =
'Remove the ESLint suppression and address the React error';
"Remove the ESLint suppression and address the React error";
break;
case 'Flow':
case "Flow":
reason =
'React Compiler has skipped optimizing this component because one or more React rule violations were reported by Flow';
suggestion = 'Remove the Flow suppression and address the React error';
"React Compiler has skipped optimizing this component because one or more React rule violations were reported by Flow";
suggestion = "Remove the Flow suppression and address the React error";
break;
default:
assertExhaustive(
suppressionRange.source,
'Unhandled suppression source',
"Unhandled suppression source"
);
}
error.pushErrorDetail(
@@ -195,7 +195,7 @@ export function suppressionsToCompilerError(
op: CompilerSuggestionOperation.Remove,
},
],
}),
})
);
}
return error;

View File

@@ -5,9 +5,9 @@
* LICENSE file in the root directory of this source tree.
*/
export * from './Gating';
export * from './Imports';
export * from './Options';
export * from './Pipeline';
export * from './Program';
export * from './Suppression';
export * from "./Gating";
export * from "./Imports";
export * from "./Options";
export * from "./Pipeline";
export * from "./Program";
export * from "./Suppression";

View File

@@ -5,20 +5,20 @@
* LICENSE file in the root directory of this source tree.
*/
import {CompilerError} from '../CompilerError';
import { CompilerError } from "../CompilerError";
import {
GeneratedSource,
HIRFunction,
Identifier,
IdentifierId,
SourceLocation,
} from './HIR';
import {printPlace} from './PrintHIR';
} from "./HIR";
import { printPlace } from "./PrintHIR";
import {
eachInstructionLValue,
eachInstructionValueOperand,
eachTerminalOperand,
} from './visitors';
} from "./visitors";
/*
* Validation pass to check that there is a 1:1 mapping between Identifier objects and IdentifierIds,
@@ -44,7 +44,7 @@ export function assertConsistentIdentifiers(fn: HIRFunction): void {
CompilerError.invariant(!assignments.has(instr.lvalue.identifier.id), {
reason: `Expected lvalues to be assigned exactly once`,
description: `Found duplicate assignment of '${printPlace(
instr.lvalue,
instr.lvalue
)}'`,
loc: instr.lvalue.loc,
suggestions: null,
@@ -68,7 +68,7 @@ type Identifiers = Map<IdentifierId, Identifier>;
function validate(
identifiers: Identifiers,
identifier: Identifier,
loc: SourceLocation | null = null,
loc: SourceLocation | null = null
): void {
const previous = identifiers.get(identifier.id);
if (previous === undefined) {

View File

@@ -1,48 +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 {GeneratedSource, HIRFunction} from './HIR';
import {printTerminal} from './PrintHIR';
import {eachTerminalSuccessor, mapTerminalSuccessors} from './visitors';
export function assertTerminalSuccessorsExist(fn: HIRFunction): void {
for (const [, block] of fn.body.blocks) {
mapTerminalSuccessors(block.terminal, successor => {
CompilerError.invariant(fn.body.blocks.has(successor), {
reason: `Terminal successor references unknown block`,
description: `Block bb${successor} does not exist for terminal '${printTerminal(
block.terminal,
)}'`,
loc: (block.terminal as any).loc ?? GeneratedSource,
suggestions: null,
});
return successor;
});
}
}
export function assertTerminalPredsExist(fn: HIRFunction): void {
for (const [, block] of fn.body.blocks) {
for (const pred of block.preds) {
const predBlock = fn.body.blocks.get(pred);
CompilerError.invariant(predBlock != null, {
reason: 'Expected predecessor block to exist',
description: `Block ${block.id} references non-existent ${pred}`,
loc: GeneratedSource,
});
CompilerError.invariant(
[...eachTerminalSuccessor(predBlock.terminal)].includes(block.id),
{
reason: 'Terminal successor does not reference correct predecessor',
description: `Block bb${block.id} has bb${predBlock.id} as a predecessor, but bb${predBlock.id}'s successors do not include bb${block.id}`,
loc: GeneratedSource,
},
);
}
}
}

View File

@@ -0,0 +1,27 @@
/**
* 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 { GeneratedSource, HIRFunction } from "./HIR";
import { printTerminal } from "./PrintHIR";
import { mapTerminalSuccessors } from "./visitors";
export function assertTerminalSuccessorsExist(fn: HIRFunction): void {
for (const [, block] of fn.body.blocks) {
mapTerminalSuccessors(block.terminal, (successor) => {
CompilerError.invariant(fn.body.blocks.has(successor), {
reason: `Terminal successor references unknown block`,
description: `Block bb${successor} does not exist for terminal '${printTerminal(
block.terminal
)}'`,
loc: (block.terminal as any).loc ?? GeneratedSource,
suggestions: null,
});
return successor;
});
}
}

View File

@@ -1,4 +1,4 @@
import {CompilerError} from '..';
import { CompilerError } from "..";
import {
BlockId,
GeneratedSource,
@@ -7,13 +7,13 @@ import {
Place,
ReactiveScope,
ScopeId,
} from './HIR';
} from "./HIR";
import {
eachInstructionLValue,
eachInstructionOperand,
eachTerminalOperand,
terminalFallthrough,
} from './visitors';
} from "./visitors";
/**
* This pass asserts that program blocks and scopes properly form a tree hierarchy
@@ -41,11 +41,11 @@ import {
*/
type Block =
| ({
kind: 'ProgramBlockSubtree';
kind: "ProgramBlockSubtree";
id: BlockId;
} & MutableRange)
| ({
kind: 'Scope';
kind: "Scope";
id: ScopeId;
} & MutableRange);
@@ -96,7 +96,7 @@ export function getScopes(fn: HIRFunction): Set<ReactiveScope> {
*/
export function rangePreOrderComparator(
a: MutableRange,
b: MutableRange,
b: MutableRange
): number {
const startDiff = a.start - b.start;
if (startDiff !== 0) return startDiff;
@@ -108,7 +108,7 @@ export function recursivelyTraverseItems<T, TContext>(
getRange: (val: T) => MutableRange,
context: TContext,
enter: (val: T, context: TContext) => void,
exit: (val: T, context: TContext) => void,
exit: (val: T, context: TContext) => void
): void {
items.sort((a, b) => rangePreOrderComparator(getRange(a), getRange(b)));
let activeItems: Array<T> = [];
@@ -122,7 +122,7 @@ export function recursivelyTraverseItems<T, TContext>(
const disjoint = currRange.start >= maybeParentRange.end;
const nested = currRange.end <= maybeParentRange.end;
CompilerError.invariant(disjoint || nested, {
reason: 'Invalid nesting in program blocks or scopes',
reason: "Invalid nesting in program blocks or scopes",
description: `Items overlap but are not nested: ${maybeParentRange.start}:${maybeParentRange.end}(${currRange.start}:${currRange.end})`,
loc: GeneratedSource,
});
@@ -148,8 +148,8 @@ const no_op: () => void = () => {};
export function assertValidBlockNesting(fn: HIRFunction): void {
const scopes = getScopes(fn);
const blocks: Array<Block> = [...scopes].map(scope => ({
kind: 'Scope',
const blocks: Array<Block> = [...scopes].map((scope) => ({
kind: "Scope",
id: scope.id,
...scope.range,
})) as Array<Block>;
@@ -159,7 +159,7 @@ export function assertValidBlockNesting(fn: HIRFunction): void {
const fallthrough = fn.body.blocks.get(fallthroughId)!;
const end = fallthrough.instructions[0]?.id ?? fallthrough.terminal.id;
blocks.push({
kind: 'ProgramBlockSubtree',
kind: "ProgramBlockSubtree",
id: block.id,
start: block.terminal.id,
end,
@@ -167,5 +167,5 @@ export function assertValidBlockNesting(fn: HIRFunction): void {
}
}
recursivelyTraverseItems(blocks, block => block, null, no_op, no_op);
recursivelyTraverseItems(blocks, (block) => block, null, no_op, no_op);
}

View File

@@ -5,13 +5,13 @@
* LICENSE file in the root directory of this source tree.
*/
import invariant from 'invariant';
import {HIRFunction, Identifier, MutableRange} from './HIR';
import invariant from "invariant";
import { HIRFunction, Identifier, MutableRange } from "./HIR";
import {
eachInstructionLValue,
eachInstructionOperand,
eachTerminalOperand,
} from './visitors';
} from "./visitors";
/*
* Checks that all mutable ranges in the function are well-formed, with
@@ -49,8 +49,8 @@ function validateMutableRange(mutableRange: MutableRange): void {
invariant(
(mutableRange.start === 0 && mutableRange.end === 0) ||
mutableRange.end > mutableRange.start,
'Identifier scope mutableRange was invalid: [%s:%s]',
"Identifier scope mutableRange was invalid: [%s:%s]",
mutableRange.start,
mutableRange.end,
mutableRange.end
);
}

View File

@@ -1,6 +1,6 @@
import {CompilerError} from '../CompilerError';
import {getScopes, recursivelyTraverseItems} from './AssertValidBlockNesting';
import {Environment} from './Environment';
import { CompilerError } from "../CompilerError";
import { getScopes, recursivelyTraverseItems } from "./AssertValidBlockNesting";
import { Environment } from "./Environment";
import {
BasicBlock,
BlockId,
@@ -12,12 +12,7 @@ import {
ReactiveScope,
ReactiveScopeTerminal,
ScopeId,
} from './HIR';
import {
markInstructionIds,
markPredecessors,
reversePostorderBlocks,
} from './HIRBuilder';
} from "./HIR";
/**
* This pass assumes that all program blocks are properly nested with respect to fallthroughs
@@ -77,14 +72,14 @@ export function buildReactiveScopeTerminalsHIR(fn: HIRFunction): void {
const queuedRewrites: Array<TerminalRewriteInfo> = [];
recursivelyTraverseItems(
[...getScopes(fn)],
scope => scope.range,
(scope) => scope.range,
{
fallthroughs: new Map(),
rewrites: queuedRewrites,
env: fn.env,
},
pushStartScopeTerminal,
pushEndScopeTerminal,
pushEndScopeTerminal
);
/**
@@ -147,9 +142,16 @@ export function buildReactiveScopeTerminalsHIR(fn: HIRFunction): void {
/**
* Step 3:
* Repoint phis when they refer to a rewritten block.
* Repoint preds and phis when they refer to a rewritten block.
*/
for (const [, block] of originalBlocks) {
for (const pred of block.preds) {
const newId = rewrittenFinalBlocks.get(pred);
if (newId != null) {
block.preds.delete(pred);
block.preds.add(newId);
}
}
for (const phi of block.phis) {
for (const [originalId, value] of phi.operands) {
const newId = rewrittenFinalBlocks.get(originalId);
@@ -160,48 +162,18 @@ export function buildReactiveScopeTerminalsHIR(fn: HIRFunction): void {
}
}
}
/**
* Step 4:
* Fixup the HIR to restore RPO, ensure correct predecessors, and
* renumber instructions. Note that the renumbering instructions
* invalidates scope and identifier ranges, so we fix them in the
* next step.
*/
reversePostorderBlocks(fn.body);
markPredecessors(fn.body);
markInstructionIds(fn.body);
/**
* Step 5:
* Fix scope and identifier ranges to account for renumbered instructions
*/
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 =
| {
kind: 'StartScope';
kind: "StartScope";
blockId: BlockId;
fallthroughId: BlockId;
instrId: InstructionId;
scope: ReactiveScope;
}
| {
kind: 'EndScope';
kind: "EndScope";
instrId: InstructionId;
fallthroughId: BlockId;
};
@@ -218,12 +190,12 @@ type ScopeTraversalContext = {
function pushStartScopeTerminal(
scope: ReactiveScope,
context: ScopeTraversalContext,
context: ScopeTraversalContext
): void {
const blockId = context.env.nextBlockId;
const fallthroughId = context.env.nextBlockId;
context.rewrites.push({
kind: 'StartScope',
kind: "StartScope",
blockId,
fallthroughId,
instrId: scope.range.start,
@@ -234,15 +206,15 @@ function pushStartScopeTerminal(
function pushEndScopeTerminal(
scope: ReactiveScope,
context: ScopeTraversalContext,
context: ScopeTraversalContext
): void {
const fallthroughId = context.fallthroughs.get(scope.id);
CompilerError.invariant(fallthroughId != null, {
reason: 'Expected scope to exist',
reason: "Expected scope to exist",
loc: GeneratedSource,
});
context.rewrites.push({
kind: 'EndScope',
kind: "EndScope",
fallthroughId,
instrId: scope.range.end,
});
@@ -276,13 +248,13 @@ type RewriteContext = {
function handleRewrite(
terminalInfo: TerminalRewriteInfo,
idx: number,
context: RewriteContext,
context: RewriteContext
): void {
// TODO make consistent instruction IDs instead of reusing
const terminal: ReactiveScopeTerminal | GotoTerminal =
terminalInfo.kind === 'StartScope'
terminalInfo.kind === "StartScope"
? {
kind: 'scope',
kind: "scope",
fallthrough: terminalInfo.fallthroughId,
block: terminalInfo.blockId,
scope: terminalInfo.scope,
@@ -290,7 +262,7 @@ function handleRewrite(
loc: GeneratedSource,
}
: {
kind: 'goto',
kind: "goto",
variant: GotoVariant.Break,
block: terminalInfo.fallthroughId,
id: terminalInfo.instrId,
@@ -309,7 +281,7 @@ function handleRewrite(
});
context.nextPreds = new Set([currBlockId]);
context.nextBlockId =
terminalInfo.kind === 'StartScope'
terminalInfo.kind === "StartScope"
? terminalInfo.blockId
: terminalInfo.fallthroughId;
context.instrSliceIdx = idx;

View File

@@ -5,8 +5,8 @@
* LICENSE file in the root directory of this source tree.
*/
import {BlockId, HIRFunction, computePostDominatorTree} from '.';
import {CompilerError} from '..';
import { BlockId, HIRFunction, computePostDominatorTree } from ".";
import { CompilerError } from "..";
export function computeUnconditionalBlocks(fn: HIRFunction): Set<BlockId> {
// Construct the set of blocks that is always reachable from the entry block.
@@ -23,7 +23,7 @@ export function computeUnconditionalBlocks(fn: HIRFunction): Set<BlockId> {
while (current !== null && current !== exit) {
CompilerError.invariant(!unconditionalBlocks.has(current), {
reason:
'Internal error: non-terminating loop in ComputeUnconditionalBlocks',
"Internal error: non-terminating loop in ComputeUnconditionalBlocks",
loc: null,
suggestions: null,
});

View File

@@ -5,10 +5,10 @@
* LICENSE file in the root directory of this source tree.
*/
import prettyFormat from 'pretty-format';
import {CompilerError} from '../CompilerError';
import {BlockId, HIRFunction} from './HIR';
import {eachTerminalSuccessor} from './visitors';
import prettyFormat from "pretty-format";
import { CompilerError } from "../CompilerError";
import { BlockId, HIRFunction } from "./HIR";
import { eachTerminalSuccessor } from "./visitors";
/*
* Computes the dominator tree of the given function. The returned `Dominator` stores the immediate
@@ -34,7 +34,7 @@ export function computeDominatorTree(fn: HIRFunction): Dominator<BlockId> {
*/
export function computePostDominatorTree(
fn: HIRFunction,
options: {includeThrowsAsExitNode: boolean},
options: { includeThrowsAsExitNode: boolean }
): PostDominator<BlockId> {
const graph = buildReverseGraph(fn, options.includeThrowsAsExitNode);
const nodes = computeImmediateDominators(graph);
@@ -87,7 +87,7 @@ export class Dominator<T> {
get(id: T): T | null {
const dominator = this.#nodes.get(id);
CompilerError.invariant(dominator !== undefined, {
reason: 'Unknown node',
reason: "Unknown node",
description: null,
loc: null,
suggestions: null,
@@ -128,7 +128,7 @@ export class PostDominator<T> {
get(id: T): T | null {
const dominator = this.#nodes.get(id);
CompilerError.invariant(dominator !== undefined, {
reason: 'Unknown node',
reason: "Unknown node",
description: null,
loc: null,
suggestions: null,
@@ -217,7 +217,7 @@ function intersect<T>(a: T, b: T, graph: Graph<T>, nodes: Map<T, T>): T {
// Turns the HIRFunction into a simplified internal form that is shared for dominator/post-dominator computation
function buildGraph(fn: HIRFunction): Graph<BlockId> {
const graph: Graph<BlockId> = {entry: fn.body.entry, nodes: new Map()};
const graph: Graph<BlockId> = { entry: fn.body.entry, nodes: new Map() };
let index = 0;
for (const [id, block] of fn.body.blocks) {
graph.nodes.set(id, {
@@ -237,7 +237,7 @@ function buildGraph(fn: HIRFunction): Graph<BlockId> {
*/
function buildReverseGraph(
fn: HIRFunction,
includeThrowsAsExitNode: boolean,
includeThrowsAsExitNode: boolean
): Graph<BlockId> {
const nodes: Map<BlockId, Node<BlockId>> = new Map();
const exitId = fn.env.nextBlockId;
@@ -256,10 +256,10 @@ function buildReverseGraph(
preds: new Set(eachTerminalSuccessor(block.terminal)),
succs: new Set(block.preds),
};
if (block.terminal.kind === 'return') {
if (block.terminal.kind === "return") {
node.preds.add(exitId);
exit.succs.add(id);
} else if (block.terminal.kind === 'throw' && includeThrowsAsExitNode) {
} else if (block.terminal.kind === "throw" && includeThrowsAsExitNode) {
node.preds.add(exitId);
exit.succs.add(id);
}
@@ -282,7 +282,7 @@ function buildReverseGraph(
}
visit(exitId);
const rpo: Graph<BlockId> = {entry: exitId, nodes: new Map()};
const rpo: Graph<BlockId> = { entry: exitId, nodes: new Map() };
let index = 0;
for (const id of postorder.reverse()) {
const node = nodes.get(id)!;

View File

@@ -5,37 +5,34 @@
* LICENSE file in the root directory of this source tree.
*/
import * as t from '@babel/types';
import {ZodError, z} from 'zod';
import {fromZodError} from 'zod-validation-error';
import {CompilerError} from '../CompilerError';
import {Logger} from '../Entrypoint';
import {Err, Ok, Result} from '../Utils/Result';
import * as t from "@babel/types";
import { ZodError, z } from "zod";
import { fromZodError } from "zod-validation-error";
import { CompilerError } from "../CompilerError";
import { Logger } from "../Entrypoint";
import { Err, Ok, Result } from "../Utils/Result";
import {
DEFAULT_GLOBALS,
DEFAULT_SHAPES,
Global,
GlobalRegistry,
installReAnimatedTypes,
} from './Globals';
} from "./Globals";
import {
BlockId,
BuiltInType,
Effect,
FunctionType,
HIRFunction,
IdentifierId,
NonLocalBinding,
PolyType,
ScopeId,
Type,
ValidatedIdentifier,
ValueKind,
makeBlockId,
makeIdentifierId,
makeIdentifierName,
makeScopeId,
} from './HIR';
} from "./HIR";
import {
BuiltInMixedReadonlyId,
DefaultMutatingHook,
@@ -43,8 +40,7 @@ import {
FunctionSignature,
ShapeRegistry,
addHook,
} from './ObjectShape';
import {Scope as BabelScope} from '@babel/traverse';
} from "./ObjectShape";
export const ExternalFunctionSchema = z.object({
// Source for the imported module that exports the `importSpecifierName` functions
@@ -61,8 +57,8 @@ export const InstrumentationSchema = z
globalGating: z.string().nullish(),
})
.refine(
opts => opts.gating != null || opts.globalGating != null,
'Expected at least one of gating or globalGating',
(opts) => opts.gating != null || opts.globalGating != null,
"Expected at least one of gating or globalGating"
);
export type ExternalFunction = z.infer<typeof ExternalFunctionSchema>;
@@ -123,18 +119,6 @@ export type Hook = z.infer<typeof HookSchema>;
const EnvironmentConfigSchema = z.object({
customHooks: z.map(z.string(), HookSchema).optional().default(new Map()),
/**
* 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
* "function" from its argument.
*
* For example, Meta has some APIs such as `featureflag("name-of-feature-flag")` which
* are rewritten by a plugin. Assigning `featureflag` to a temporary would break the
* plugin since it looks specifically for the name of the function being invoked, not
* following aliases.
*/
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.
* This is intended to support hot module reloading (HMR), where the same runtime component
@@ -281,18 +265,6 @@ const EnvironmentConfigSchema = z.object({
enableEmitHookGuards: ExternalFunctionSchema.nullish(),
/**
* Enable instruction reordering. See InstructionReordering.ts for the details
* of the approach.
*/
enableInstructionReordering: z.boolean().default(false),
/**
* Enables function outlinining, where anonymous functions that do not close over
* local variables can be extracted into top-level helper functions.
*/
enableFunctionOutlining: z.boolean().default(true),
/*
* Enables instrumentation codegen. This emits a dev-mode only call to an
* instrumentation function, for components and hooks that Forget compiles.
@@ -417,23 +389,6 @@ const EnvironmentConfigSchema = z.object({
* and identifiers have been changed.
*/
hookPattern: z.string().nullable().default(null),
/**
* If enabled, this will treat objects named as `ref` or if their names end with the substring `Ref`,
* and contain a property named `current`, as React refs.
*
* ```
* const ref = useMyRef();
* const myRef = useMyRef2();
* useEffect(() => {
* ref.current = ...;
* myRef.current = ...;
* })
* ```
*
* Here the variables `ref` and `myRef` will be typed as Refs.
*/
enableTreatRefLikeIdentifiersAsRefs: z.boolean().nullable().default(false),
});
export type EnvironmentConfig = z.infer<typeof EnvironmentConfigSchema>;
@@ -443,34 +398,23 @@ export function parseConfigPragma(pragma: string): EnvironmentConfig {
// Get the defaults to programmatically check for boolean properties
const defaultConfig = EnvironmentConfigSchema.parse({});
for (const token of pragma.split(' ')) {
if (!token.startsWith('@')) {
for (const token of pragma.split(" ")) {
if (!token.startsWith("@")) {
continue;
}
const keyVal = token.slice(1);
let [key, val]: any = keyVal.split(':');
let [key, val]: any = keyVal.split(":");
if (key === 'validateNoCapitalizedCalls') {
if (key === "validateNoCapitalizedCalls") {
maybeConfig[key] = [];
continue;
}
if (
key === 'enableChangeDetectionForDebugging' &&
(val === undefined || val === 'true')
) {
maybeConfig[key] = {
source: 'react-compiler-runtime',
importSpecifierName: '$structuralCheck',
};
continue;
}
if (typeof defaultConfig[key as keyof EnvironmentConfig] !== 'boolean') {
if (typeof defaultConfig[key as keyof EnvironmentConfig] !== "boolean") {
// skip parsing non-boolean properties
continue;
}
if (val === undefined || val === 'true') {
if (val === undefined || val === "true") {
val = true;
} else {
val = false;
@@ -483,7 +427,7 @@ export function parseConfigPragma(pragma: string): EnvironmentConfig {
return config.data;
}
CompilerError.invariant(false, {
reason: 'Internal error, could not parse config from pragma string',
reason: "Internal error, could not parse config from pragma string",
description: `${fromZodError(config.error)}`,
loc: null,
suggestions: null,
@@ -492,18 +436,18 @@ export function parseConfigPragma(pragma: string): EnvironmentConfig {
export type PartialEnvironmentConfig = Partial<EnvironmentConfig>;
export type ReactFunctionType = 'Component' | 'Hook' | 'Other';
export type ReactFunctionType = "Component" | "Hook" | "Other";
export function printFunctionType(type: ReactFunctionType): string {
switch (type) {
case 'Component': {
return 'component';
case "Component": {
return "component";
}
case 'Hook': {
return 'hook';
case "Hook": {
return "hook";
}
default: {
return 'function';
return "function";
}
}
}
@@ -514,11 +458,6 @@ export class Environment {
#nextIdentifer: number = 0;
#nextBlock: number = 0;
#nextScope: number = 0;
#scope: BabelScope;
#outlinedFunctions: Array<{
fn: HIRFunction;
type: ReactFunctionType | null;
}> = [];
logger: Logger | null;
filename: string | null;
code: string | null;
@@ -530,16 +469,14 @@ export class Environment {
#hoistedIdentifiers: Set<t.Identifier>;
constructor(
scope: BabelScope,
fnType: ReactFunctionType,
config: EnvironmentConfig,
contextIdentifiers: Set<t.Identifier>,
logger: Logger | null,
filename: string | null,
code: string | null,
useMemoCacheIdentifier: string,
useMemoCacheIdentifier: string
) {
this.#scope = scope;
this.fnType = fnType;
this.config = config;
this.filename = filename;
@@ -574,13 +511,13 @@ export class Environment {
positionalParams: [],
restParam: hook.effectKind,
returnType: hook.transitiveMixedData
? {kind: 'Object', shapeId: BuiltInMixedReadonlyId}
: {kind: 'Poly'},
? { kind: "Object", shapeId: BuiltInMixedReadonlyId }
: { kind: "Poly" },
returnValueKind: hook.valueKind,
calleeEffect: Effect.Read,
hookKind: 'Custom',
hookKind: "Custom",
noAlias: hook.noAlias,
}),
})
);
}
@@ -612,30 +549,12 @@ export class Environment {
return this.#hoistedIdentifiers.has(node);
}
generateGloballyUniqueIdentifierName(
name: string | null,
): ValidatedIdentifier {
const identifierNode = this.#scope.generateUidIdentifier(name ?? undefined);
return makeIdentifierName(identifierNode.name);
}
outlineFunction(fn: HIRFunction, type: ReactFunctionType | null): void {
this.#outlinedFunctions.push({fn, type});
}
getOutlinedFunctions(): Array<{
fn: HIRFunction;
type: ReactFunctionType | null;
}> {
return this.#outlinedFunctions;
}
getGlobalDeclaration(binding: NonLocalBinding): Global | null {
if (this.config.hookPattern != null) {
const match = new RegExp(this.config.hookPattern).exec(binding.name);
if (
match != null &&
typeof match[1] === 'string' &&
typeof match[1] === "string" &&
isHookName(match[1])
) {
const resolvedName = match[1];
@@ -644,17 +563,17 @@ export class Environment {
}
switch (binding.kind) {
case 'ModuleLocal': {
case "ModuleLocal": {
// don't resolve module locals
return isHookName(binding.name) ? this.#getCustomHookType() : null;
}
case 'Global': {
case "Global": {
return (
this.#globals.get(binding.name) ??
(isHookName(binding.name) ? this.#getCustomHookType() : null)
);
}
case 'ImportSpecifier': {
case "ImportSpecifier": {
if (this.#isKnownReactModule(binding.module)) {
/**
* For `import {imported as name} from "..."` form, we use the `imported`
@@ -681,8 +600,8 @@ export class Environment {
: null;
}
}
case 'ImportDefault':
case 'ImportNamespace': {
case "ImportDefault":
case "ImportNamespace": {
if (this.#isKnownReactModule(binding.module)) {
// only resolve imports to modules we know about
return (
@@ -698,19 +617,19 @@ export class Environment {
#isKnownReactModule(moduleName: string): boolean {
return (
moduleName.toLowerCase() === 'react' ||
moduleName.toLowerCase() === 'react-dom' ||
moduleName.toLowerCase() === "react" ||
moduleName.toLowerCase() === "react-dom" ||
(this.config.enableSharedRuntime__testonly &&
moduleName === 'shared-runtime')
moduleName === "shared-runtime")
);
}
getPropertyType(
receiver: Type,
property: string,
property: string
): BuiltInType | PolyType | null {
let shapeId = null;
if (receiver.kind === 'Object' || receiver.kind === 'Function') {
if (receiver.kind === "Object" || receiver.kind === "Function") {
shapeId = receiver.shapeId;
}
if (shapeId !== null) {
@@ -726,7 +645,7 @@ export class Environment {
suggestions: null,
});
let value =
shape.properties.get(property) ?? shape.properties.get('*') ?? null;
shape.properties.get(property) ?? shape.properties.get("*") ?? null;
if (value === null && isHookName(property)) {
value = this.#getCustomHookType();
}
@@ -739,7 +658,7 @@ export class Environment {
}
getFunctionSignature(type: FunctionType): FunctionSignature | null {
const {shapeId} = type;
const { shapeId } = type;
if (shapeId !== null) {
const shape = this.#shapes.get(shapeId);
CompilerError.invariant(shape !== undefined, {
@@ -773,7 +692,7 @@ export function isHookName(name: string): boolean {
}
export function parseEnvironmentConfig(
partialConfig: PartialEnvironmentConfig,
partialConfig: PartialEnvironmentConfig
): Result<EnvironmentConfig, ZodError<PartialEnvironmentConfig>> {
const config = EnvironmentConfigSchema.safeParse(partialConfig);
if (config.success) {
@@ -784,7 +703,7 @@ export function parseEnvironmentConfig(
}
export function validateEnvironmentConfig(
partialConfig: PartialEnvironmentConfig,
partialConfig: PartialEnvironmentConfig
): EnvironmentConfig {
const config = EnvironmentConfigSchema.safeParse(partialConfig);
if (config.success) {
@@ -793,7 +712,7 @@ export function validateEnvironmentConfig(
CompilerError.throwInvalidConfig({
reason:
'Could not validate environment config. Update React Compiler config to fix the error',
"Could not validate environment config. Update React Compiler config to fix the error",
description: `${fromZodError(config.error)}`,
loc: null,
suggestions: null,
@@ -801,10 +720,10 @@ export function validateEnvironmentConfig(
}
export function tryParseExternalFunction(
maybeExternalFunction: any,
maybeExternalFunction: any
): ExternalFunction {
const externalFunction = ExternalFunctionSchema.safeParse(
maybeExternalFunction,
maybeExternalFunction
);
if (externalFunction.success) {
return externalFunction.data;
@@ -812,7 +731,7 @@ export function tryParseExternalFunction(
CompilerError.throwInvalidConfig({
reason:
'Could not parse external function. Update React Compiler config to fix the error',
"Could not parse external function. Update React Compiler config to fix the error",
description: `${fromZodError(externalFunction.error)}`,
loc: null,
suggestions: null,

View File

@@ -5,11 +5,11 @@
* LICENSE file in the root directory of this source tree.
*/
import type {NodePath} from '@babel/traverse';
import type * as t from '@babel/types';
import {CompilerError} from '../CompilerError';
import {getOrInsertDefault} from '../Utils/utils';
import {GeneratedSource} from './HIR';
import type { NodePath } from "@babel/traverse";
import type * as t from "@babel/types";
import { CompilerError } from "../CompilerError";
import { getOrInsertDefault } from "../Utils/utils";
import { GeneratedSource } from "./HIR";
type IdentifierInfo = {
reassigned: boolean;
@@ -35,7 +35,7 @@ type FindContextIdentifierState = {
const withFunctionScope = {
enter: function (
path: BabelFunction,
state: FindContextIdentifierState,
state: FindContextIdentifierState
): void {
state.currentFn.push(path);
},
@@ -45,7 +45,7 @@ const withFunctionScope = {
};
export function findContextIdentifiers(
func: NodePath<t.Function>,
func: NodePath<t.Function>
): Set<t.Identifier> {
const state: FindContextIdentifierState = {
currentFn: [],
@@ -60,17 +60,17 @@ export function findContextIdentifiers(
ObjectMethod: withFunctionScope,
AssignmentExpression(
path: NodePath<t.AssignmentExpression>,
state: FindContextIdentifierState,
state: FindContextIdentifierState
): void {
const left = path.get('left');
const left = path.get("left");
const currentFn = state.currentFn.at(-1) ?? null;
handleAssignment(currentFn, state.identifiers, left);
},
UpdateExpression(
path: NodePath<t.UpdateExpression>,
state: FindContextIdentifierState,
state: FindContextIdentifierState
): void {
const argument = path.get('argument');
const argument = path.get("argument");
const currentFn = state.currentFn.at(-1) ?? null;
if (argument.isLVal()) {
handleAssignment(currentFn, state.identifiers, argument);
@@ -78,7 +78,7 @@ export function findContextIdentifiers(
},
Identifier(
path: NodePath<t.Identifier>,
state: FindContextIdentifierState,
state: FindContextIdentifierState
): void {
const currentFn = state.currentFn.at(-1) ?? null;
if (path.isReferencedIdentifier()) {
@@ -86,7 +86,7 @@ export function findContextIdentifiers(
}
},
},
state,
state
);
const result = new Set<t.Identifier>();
@@ -103,7 +103,7 @@ export function findContextIdentifiers(
function handleIdentifier(
currentFn: BabelFunction | null,
identifiers: Map<t.Identifier, IdentifierInfo>,
path: NodePath<t.Identifier>,
path: NodePath<t.Identifier>
): void {
const name = path.node.name;
const binding = path.scope.getBinding(name);
@@ -126,7 +126,7 @@ function handleIdentifier(
function handleAssignment(
currentFn: BabelFunction | null,
identifiers: Map<t.Identifier, IdentifierInfo>,
lvalPath: NodePath<t.LVal>,
lvalPath: NodePath<t.LVal>
): void {
/*
* Find all reassignments to identifiers declared outside of currentFn
@@ -134,7 +134,7 @@ function handleAssignment(
*/
const lvalNode = lvalPath.node;
switch (lvalNode.type) {
case 'Identifier': {
case "Identifier": {
const path = lvalPath as NodePath<t.Identifier>;
const name = path.node.name;
const binding = path.scope.getBinding(name);
@@ -155,20 +155,20 @@ function handleAssignment(
}
break;
}
case 'ArrayPattern': {
case "ArrayPattern": {
const path = lvalPath as NodePath<t.ArrayPattern>;
for (const element of path.get('elements')) {
for (const element of path.get("elements")) {
if (nonNull(element)) {
handleAssignment(currentFn, identifiers, element);
}
}
break;
}
case 'ObjectPattern': {
case "ObjectPattern": {
const path = lvalPath as NodePath<t.ObjectPattern>;
for (const property of path.get('properties')) {
for (const property of path.get("properties")) {
if (property.isObjectProperty()) {
const valuePath = property.get('value');
const valuePath = property.get("value");
CompilerError.invariant(valuePath.isLVal(), {
reason: `[FindContextIdentifiers] Expected object property value to be an LVal, got: ${valuePath.type}`,
description: null,
@@ -188,18 +188,18 @@ function handleAssignment(
}
break;
}
case 'AssignmentPattern': {
case "AssignmentPattern": {
const path = lvalPath as NodePath<t.AssignmentPattern>;
const left = path.get('left');
const left = path.get("left");
handleAssignment(currentFn, identifiers, left);
break;
}
case 'RestElement': {
case "RestElement": {
const path = lvalPath as NodePath<t.RestElement>;
handleAssignment(currentFn, identifiers, path.get('argument'));
handleAssignment(currentFn, identifiers, path.get("argument"));
break;
}
case 'MemberExpression': {
case "MemberExpression": {
// Interior mutability (not a reassign)
break;
}
@@ -215,7 +215,7 @@ function handleAssignment(
}
function nonNull<T extends NonNullable<t.Node>>(
t: NodePath<T | null>,
t: NodePath<T | null>
): t is NodePath<T> {
return t.node != null;
}

View File

@@ -5,24 +5,22 @@
* LICENSE file in the root directory of this source tree.
*/
import {Effect, ValueKind, ValueReason} from './HIR';
import { Effect, ValueKind, ValueReason } from "./HIR";
import {
BUILTIN_SHAPES,
BuiltInArrayId,
BuiltInUseActionStateId,
BuiltInUseEffectHookId,
BuiltInUseInsertionEffectHookId,
BuiltInUseLayoutEffectHookId,
BuiltInUseOperatorId,
BuiltInUseReducerId,
BuiltInUseRefId,
BuiltInUseStateId,
ShapeRegistry,
addFunction,
addHook,
addObject,
} from './ObjectShape';
import {BuiltInType, PolyType} from './Types';
} from "./ObjectShape";
import { BuiltInType, PolyType } from "./Types";
/*
* This file exports types and defaults for JavaScript global objects.
@@ -37,59 +35,59 @@ export const DEFAULT_SHAPES: ShapeRegistry = new Map(BUILTIN_SHAPES);
// Hack until we add ObjectShapes for all globals
const UNTYPED_GLOBALS: Set<string> = new Set([
'String',
'Object',
'Function',
'Number',
'RegExp',
'Date',
'Error',
'Function',
'TypeError',
'RangeError',
'ReferenceError',
'SyntaxError',
'URIError',
'EvalError',
'Boolean',
'DataView',
'Float32Array',
'Float64Array',
'Int8Array',
'Int16Array',
'Int32Array',
'Map',
'Set',
'WeakMap',
'Uint8Array',
'Uint8ClampedArray',
'Uint16Array',
'Uint32Array',
'ArrayBuffer',
'JSON',
'parseFloat',
'parseInt',
'console',
'isNaN',
'eval',
'isFinite',
'encodeURI',
'decodeURI',
'encodeURIComponent',
'decodeURIComponent',
"String",
"Object",
"Function",
"Number",
"RegExp",
"Date",
"Error",
"Function",
"TypeError",
"RangeError",
"ReferenceError",
"SyntaxError",
"URIError",
"EvalError",
"Boolean",
"DataView",
"Float32Array",
"Float64Array",
"Int8Array",
"Int16Array",
"Int32Array",
"Map",
"Set",
"WeakMap",
"Uint8Array",
"Uint8ClampedArray",
"Uint16Array",
"Uint32Array",
"ArrayBuffer",
"JSON",
"parseFloat",
"parseInt",
"console",
"isNaN",
"eval",
"isFinite",
"encodeURI",
"decodeURI",
"encodeURIComponent",
"decodeURIComponent",
]);
const TYPED_GLOBALS: Array<[string, BuiltInType]> = [
[
'Array',
addObject(DEFAULT_SHAPES, 'Array', [
"Array",
addObject(DEFAULT_SHAPES, "Array", [
[
'isArray',
"isArray",
// Array.isArray(value)
addFunction(DEFAULT_SHAPES, [], {
positionalParams: [Effect.Read],
restParam: null,
returnType: {kind: 'Primitive'},
returnType: { kind: "Primitive" },
calleeEffect: Effect.Read,
returnValueKind: ValueKind.Primitive,
}),
@@ -106,12 +104,12 @@ const TYPED_GLOBALS: Array<[string, BuiltInType]> = [
* function)
*/
[
'of',
"of",
// Array.of(element0, ..., elementN)
addFunction(DEFAULT_SHAPES, [], {
positionalParams: [],
restParam: Effect.Read,
returnType: {kind: 'Object', shapeId: BuiltInArrayId},
returnType: { kind: "Object", shapeId: BuiltInArrayId },
calleeEffect: Effect.Read,
returnValueKind: ValueKind.Mutable,
}),
@@ -119,85 +117,85 @@ const TYPED_GLOBALS: Array<[string, BuiltInType]> = [
]),
],
[
'Math',
addObject(DEFAULT_SHAPES, 'Math', [
"Math",
addObject(DEFAULT_SHAPES, "Math", [
// Static properties (TODO)
['PI', {kind: 'Primitive'}],
["PI", { kind: "Primitive" }],
// Static methods (TODO)
[
'max',
"max",
// Math.max(value0, ..., valueN)
addFunction(DEFAULT_SHAPES, [], {
positionalParams: [],
restParam: Effect.Read,
returnType: {kind: 'Primitive'},
returnType: { kind: "Primitive" },
calleeEffect: Effect.Read,
returnValueKind: ValueKind.Primitive,
}),
],
]),
],
['Infinity', {kind: 'Primitive'}],
['NaN', {kind: 'Primitive'}],
["Infinity", { kind: "Primitive" }],
["NaN", { kind: "Primitive" }],
[
'console',
addObject(DEFAULT_SHAPES, 'console', [
"console",
addObject(DEFAULT_SHAPES, "console", [
[
'error',
"error",
addFunction(DEFAULT_SHAPES, [], {
positionalParams: [],
restParam: Effect.Read,
returnType: {kind: 'Primitive'},
returnType: { kind: "Primitive" },
calleeEffect: Effect.Read,
returnValueKind: ValueKind.Primitive,
}),
],
[
'info',
"info",
addFunction(DEFAULT_SHAPES, [], {
positionalParams: [],
restParam: Effect.Read,
returnType: {kind: 'Primitive'},
returnType: { kind: "Primitive" },
calleeEffect: Effect.Read,
returnValueKind: ValueKind.Primitive,
}),
],
[
'log',
"log",
addFunction(DEFAULT_SHAPES, [], {
positionalParams: [],
restParam: Effect.Read,
returnType: {kind: 'Primitive'},
returnType: { kind: "Primitive" },
calleeEffect: Effect.Read,
returnValueKind: ValueKind.Primitive,
}),
],
[
'table',
"table",
addFunction(DEFAULT_SHAPES, [], {
positionalParams: [],
restParam: Effect.Read,
returnType: {kind: 'Primitive'},
returnType: { kind: "Primitive" },
calleeEffect: Effect.Read,
returnValueKind: ValueKind.Primitive,
}),
],
[
'trace',
"trace",
addFunction(DEFAULT_SHAPES, [], {
positionalParams: [],
restParam: Effect.Read,
returnType: {kind: 'Primitive'},
returnType: { kind: "Primitive" },
calleeEffect: Effect.Read,
returnValueKind: ValueKind.Primitive,
}),
],
[
'warn',
"warn",
addFunction(DEFAULT_SHAPES, [], {
positionalParams: [],
restParam: Effect.Read,
returnType: {kind: 'Primitive'},
returnType: { kind: "Primitive" },
calleeEffect: Effect.Read,
returnValueKind: ValueKind.Primitive,
}),
@@ -205,31 +203,31 @@ const TYPED_GLOBALS: Array<[string, BuiltInType]> = [
]),
],
[
'Boolean',
"Boolean",
addFunction(DEFAULT_SHAPES, [], {
positionalParams: [],
restParam: Effect.Read,
returnType: {kind: 'Primitive'},
returnType: { kind: "Primitive" },
calleeEffect: Effect.Read,
returnValueKind: ValueKind.Primitive,
}),
],
[
'Number',
"Number",
addFunction(DEFAULT_SHAPES, [], {
positionalParams: [],
restParam: Effect.Read,
returnType: {kind: 'Primitive'},
returnType: { kind: "Primitive" },
calleeEffect: Effect.Read,
returnValueKind: ValueKind.Primitive,
}),
],
[
'String',
"String",
addFunction(DEFAULT_SHAPES, [], {
positionalParams: [],
restParam: Effect.Read,
returnType: {kind: 'Primitive'},
returnType: { kind: "Primitive" },
calleeEffect: Effect.Read,
returnValueKind: ValueKind.Primitive,
}),
@@ -244,179 +242,155 @@ const TYPED_GLOBALS: Array<[string, BuiltInType]> = [
*/
const REACT_APIS: Array<[string, BuiltInType]> = [
[
'useContext',
"useContext",
addHook(DEFAULT_SHAPES, {
positionalParams: [],
restParam: Effect.Read,
returnType: {kind: 'Poly'},
returnType: { kind: "Poly" },
calleeEffect: Effect.Read,
hookKind: 'useContext',
hookKind: "useContext",
returnValueKind: ValueKind.Frozen,
returnValueReason: ValueReason.Context,
}),
],
[
'useState',
"useState",
addHook(DEFAULT_SHAPES, {
positionalParams: [],
restParam: Effect.Freeze,
returnType: {kind: 'Object', shapeId: BuiltInUseStateId},
returnType: { kind: "Object", shapeId: BuiltInUseStateId },
calleeEffect: Effect.Read,
hookKind: 'useState',
hookKind: "useState",
returnValueKind: ValueKind.Frozen,
returnValueReason: ValueReason.State,
}),
],
[
'useActionState',
addHook(DEFAULT_SHAPES, {
positionalParams: [],
restParam: Effect.Freeze,
returnType: {kind: 'Object', shapeId: BuiltInUseActionStateId},
calleeEffect: Effect.Read,
hookKind: 'useActionState',
returnValueKind: ValueKind.Frozen,
returnValueReason: ValueReason.State,
}),
],
[
'useReducer',
addHook(DEFAULT_SHAPES, {
positionalParams: [],
restParam: Effect.Freeze,
returnType: {kind: 'Object', shapeId: BuiltInUseReducerId},
calleeEffect: Effect.Read,
hookKind: 'useReducer',
returnValueKind: ValueKind.Frozen,
returnValueReason: ValueReason.ReducerState,
}),
],
[
'useRef',
"useRef",
addHook(DEFAULT_SHAPES, {
positionalParams: [],
restParam: Effect.Capture,
returnType: {kind: 'Object', shapeId: BuiltInUseRefId},
returnType: { kind: "Object", shapeId: BuiltInUseRefId },
calleeEffect: Effect.Read,
hookKind: 'useRef',
hookKind: "useRef",
returnValueKind: ValueKind.Mutable,
}),
],
[
'useMemo',
"useMemo",
addHook(DEFAULT_SHAPES, {
positionalParams: [],
restParam: Effect.Freeze,
returnType: {kind: 'Poly'},
returnType: { kind: "Poly" },
calleeEffect: Effect.Read,
hookKind: 'useMemo',
hookKind: "useMemo",
returnValueKind: ValueKind.Frozen,
}),
],
[
'useCallback',
"useCallback",
addHook(DEFAULT_SHAPES, {
positionalParams: [],
restParam: Effect.Freeze,
returnType: {kind: 'Poly'},
returnType: { kind: "Poly" },
calleeEffect: Effect.Read,
hookKind: 'useCallback',
hookKind: "useCallback",
returnValueKind: ValueKind.Frozen,
}),
],
[
'useEffect',
"useEffect",
addHook(
DEFAULT_SHAPES,
{
positionalParams: [],
restParam: Effect.Freeze,
returnType: {kind: 'Primitive'},
returnType: { kind: "Primitive" },
calleeEffect: Effect.Read,
hookKind: 'useEffect',
hookKind: "useEffect",
returnValueKind: ValueKind.Frozen,
},
BuiltInUseEffectHookId,
BuiltInUseEffectHookId
),
],
[
'useLayoutEffect',
"useLayoutEffect",
addHook(
DEFAULT_SHAPES,
{
positionalParams: [],
restParam: Effect.Freeze,
returnType: {kind: 'Poly'},
returnType: { kind: "Poly" },
calleeEffect: Effect.Read,
hookKind: 'useLayoutEffect',
hookKind: "useLayoutEffect",
returnValueKind: ValueKind.Frozen,
},
BuiltInUseLayoutEffectHookId,
BuiltInUseLayoutEffectHookId
),
],
[
'useInsertionEffect',
"useInsertionEffect",
addHook(
DEFAULT_SHAPES,
{
positionalParams: [],
restParam: Effect.Freeze,
returnType: {kind: 'Poly'},
returnType: { kind: "Poly" },
calleeEffect: Effect.Read,
hookKind: 'useInsertionEffect',
hookKind: "useLayoutEffect",
returnValueKind: ValueKind.Frozen,
},
BuiltInUseInsertionEffectHookId,
BuiltInUseInsertionEffectHookId
),
],
[
'use',
"use",
addFunction(
DEFAULT_SHAPES,
[],
{
positionalParams: [],
restParam: Effect.Freeze,
returnType: {kind: 'Poly'},
returnType: { kind: "Poly" },
calleeEffect: Effect.Read,
returnValueKind: ValueKind.Frozen,
},
BuiltInUseOperatorId,
BuiltInUseOperatorId
),
],
];
TYPED_GLOBALS.push(
[
'React',
"React",
addObject(DEFAULT_SHAPES, null, [
...REACT_APIS,
[
'createElement',
"createElement",
addFunction(DEFAULT_SHAPES, [], {
positionalParams: [],
restParam: Effect.Freeze,
returnType: {kind: 'Poly'},
returnType: { kind: "Poly" },
calleeEffect: Effect.Read,
returnValueKind: ValueKind.Frozen,
}),
],
[
'cloneElement',
"cloneElement",
addFunction(DEFAULT_SHAPES, [], {
positionalParams: [],
restParam: Effect.Freeze,
returnType: {kind: 'Poly'},
returnType: { kind: "Poly" },
calleeEffect: Effect.Read,
returnValueKind: ValueKind.Frozen,
}),
],
[
'createRef',
"createRef",
addFunction(DEFAULT_SHAPES, [], {
positionalParams: [],
restParam: Effect.Capture, // createRef takes no paramters
returnType: {kind: 'Object', shapeId: BuiltInUseRefId},
returnType: { kind: "Object", shapeId: BuiltInUseRefId },
calleeEffect: Effect.Read,
returnValueKind: ValueKind.Mutable,
}),
@@ -424,15 +398,15 @@ TYPED_GLOBALS.push(
]),
],
[
'_jsx',
"_jsx",
addFunction(DEFAULT_SHAPES, [], {
positionalParams: [],
restParam: Effect.Freeze,
returnType: {kind: 'Poly'},
returnType: { kind: "Poly" },
calleeEffect: Effect.Read,
returnValueKind: ValueKind.Frozen,
}),
],
]
);
export type Global = BuiltInType | PolyType;
@@ -442,7 +416,7 @@ export const DEFAULT_GLOBALS: GlobalRegistry = new Map(REACT_APIS);
// Hack until we add ObjectShapes for all globals
for (const name of UNTYPED_GLOBALS) {
DEFAULT_GLOBALS.set(name, {
kind: 'Poly',
kind: "Poly",
});
}
@@ -452,22 +426,22 @@ for (const [name, type_] of TYPED_GLOBALS) {
// Recursive global type
DEFAULT_GLOBALS.set(
'globalThis',
addObject(DEFAULT_SHAPES, 'globalThis', TYPED_GLOBALS),
"globalThis",
addObject(DEFAULT_SHAPES, "globalThis", TYPED_GLOBALS)
);
export function installReAnimatedTypes(
globals: GlobalRegistry,
registry: ShapeRegistry,
registry: ShapeRegistry
): void {
// hooks that freeze args and return frozen value
const frozenHooks = [
'useFrameCallback',
'useAnimatedStyle',
'useAnimatedProps',
'useAnimatedScrollHandler',
'useAnimatedReaction',
'useWorkletCallback',
"useFrameCallback",
"useAnimatedStyle",
"useAnimatedProps",
"useAnimatedScrollHandler",
"useAnimatedReaction",
"useWorkletCallback",
];
for (const hook of frozenHooks) {
globals.set(
@@ -475,12 +449,12 @@ export function installReAnimatedTypes(
addHook(registry, {
positionalParams: [],
restParam: Effect.Freeze,
returnType: {kind: 'Poly'},
returnType: { kind: "Poly" },
returnValueKind: ValueKind.Frozen,
noAlias: true,
calleeEffect: Effect.Read,
hookKind: 'Custom',
}),
hookKind: "Custom",
})
);
}
@@ -488,31 +462,31 @@ export function installReAnimatedTypes(
* hooks that return a mutable value. ideally these should be modelled as a
* ref, but this works for now.
*/
const mutableHooks = ['useSharedValue', 'useDerivedValue'];
const mutableHooks = ["useSharedValue", "useDerivedValue"];
for (const hook of mutableHooks) {
globals.set(
hook,
addHook(registry, {
positionalParams: [],
restParam: Effect.Freeze,
returnType: {kind: 'Poly'},
returnType: { kind: "Poly" },
returnValueKind: ValueKind.Mutable,
noAlias: true,
calleeEffect: Effect.Read,
hookKind: 'Custom',
}),
hookKind: "Custom",
})
);
}
// functions that return mutable value
const funcs = [
'withTiming',
'withSpring',
'createAnimatedPropAdapter',
'withDecay',
'withRepeat',
'runOnUI',
'executeOnUIRuntimeSync',
"withTiming",
"withSpring",
"createAnimatedPropAdapter",
"withDecay",
"withRepeat",
"runOnUI",
"executeOnUIRuntimeSync",
];
for (const fn of funcs) {
globals.set(
@@ -520,11 +494,11 @@ export function installReAnimatedTypes(
addFunction(registry, [], {
positionalParams: [],
restParam: Effect.Read,
returnType: {kind: 'Poly'},
returnType: { kind: "Poly" },
calleeEffect: Effect.Read,
returnValueKind: ValueKind.Mutable,
noAlias: true,
}),
})
);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -5,10 +5,10 @@
* LICENSE file in the root directory of this source tree.
*/
import {Binding, NodePath} from '@babel/traverse';
import * as t from '@babel/types';
import {CompilerError} from '../CompilerError';
import {Environment} from './Environment';
import { Binding, NodePath } from "@babel/traverse";
import * as t from "@babel/types";
import { CompilerError } from "../CompilerError";
import { Environment } from "./Environment";
import {
BasicBlock,
BlockId,
@@ -28,13 +28,13 @@ import {
makeIdentifierName,
makeInstructionId,
makeType,
} from './HIR';
import {printInstruction} from './PrintHIR';
} from "./HIR";
import { printInstruction } from "./PrintHIR";
import {
eachTerminalSuccessor,
mapTerminalSuccessors,
terminalFallthrough,
} from './visitors';
} from "./visitors";
/*
* *******************************************************************************************
@@ -54,31 +54,31 @@ export type WipBlock = {
type Scope = LoopScope | LabelScope | SwitchScope;
type LoopScope = {
kind: 'loop';
kind: "loop";
label: string | null;
continueBlock: BlockId;
breakBlock: BlockId;
};
type SwitchScope = {
kind: 'switch';
kind: "switch";
breakBlock: BlockId;
label: string | null;
};
type LabelScope = {
kind: 'label';
kind: "label";
label: string;
breakBlock: BlockId;
};
function newBlock(id: BlockId, kind: BlockKind): WipBlock {
return {id, kind, instructions: []};
return { id, kind, instructions: [] };
}
export type Bindings = Map<
string,
{node: t.Identifier; identifier: Identifier}
{ node: t.Identifier; identifier: Identifier }
>;
/*
@@ -90,13 +90,13 @@ export type ExceptionsMode =
* Mode used for code not covered by explicit exception handling, any
* errors are assumed to be thrown out of the function
*/
| {kind: 'ThrowExceptions'}
| { kind: "ThrowExceptions" }
/*
* Mode used for code that *is* covered by explicit exception handling
* (ie try/catch), which requires modeling the possibility of control
* flow to the exception handler.
*/
| {kind: 'CatchExceptions'; handler: BlockId};
| { kind: "CatchExceptions"; handler: BlockId };
// Helper class for constructing a CFG
export default class HIRBuilder {
@@ -110,11 +110,6 @@ export default class HIRBuilder {
#exceptionHandlerStack: Array<BlockId> = [];
parentFunction: NodePath<t.Function>;
errors: CompilerError = new CompilerError();
/**
* Traversal context: counts the number of `fbt` tag parents
* of the current babel node.
*/
fbtDepth: number = 0;
get nextIdentifierId(): IdentifierId {
return this.#env.nextIdentifierId;
@@ -136,14 +131,14 @@ export default class HIRBuilder {
env: Environment,
parentFunction: NodePath<t.Function>, // the outermost function being compiled
bindings: Bindings | null = null,
context: Array<t.Identifier> | null = null,
context: Array<t.Identifier> | null = null
) {
this.#env = env;
this.#bindings = bindings ?? new Map();
this.parentFunction = parentFunction;
this.#context = context ?? [];
this.#entry = makeBlockId(env.nextBlockId);
this.#current = newBlock(this.#entry, 'block');
this.#current = newBlock(this.#entry, "block");
}
currentBlockKind(): BlockKind {
@@ -158,13 +153,13 @@ export default class HIRBuilder {
const continuationBlock = this.reserve(this.currentBlockKind());
this.terminateWithContinuation(
{
kind: 'maybe-throw',
kind: "maybe-throw",
continuation: continuationBlock.id,
handler: exceptionHandler,
id: makeInstructionId(0),
loc: instruction.loc,
},
continuationBlock,
continuationBlock
);
}
}
@@ -185,7 +180,7 @@ export default class HIRBuilder {
return {
id,
name: null,
mutableRange: {start: makeInstructionId(0), end: makeInstructionId(0)},
mutableRange: { start: makeInstructionId(0), end: makeInstructionId(0) },
scope: null,
type: makeType(),
loc,
@@ -193,7 +188,7 @@ export default class HIRBuilder {
}
#resolveBabelBinding(
path: NodePath<t.Identifier | t.JSXIdentifier>,
path: NodePath<t.Identifier | t.JSXIdentifier>
): Binding | null {
const originalName = path.node.name;
const binding = path.scope.getBinding(originalName);
@@ -234,12 +229,12 @@ export default class HIRBuilder {
* ```
*/
resolveIdentifier(
path: NodePath<t.Identifier | t.JSXIdentifier>,
path: NodePath<t.Identifier | t.JSXIdentifier>
): VariableBinding {
const originalName = path.node.name;
const babelBinding = this.#resolveBabelBinding(path);
if (babelBinding == null) {
return {kind: 'Global', name: originalName};
return { kind: "Global", name: originalName };
}
// Check if the binding is from module scope
@@ -251,7 +246,7 @@ export default class HIRBuilder {
const importDeclaration =
path.parentPath as NodePath<t.ImportDeclaration>;
return {
kind: 'ImportDefault',
kind: "ImportDefault",
name: originalName,
module: importDeclaration.node.source.value,
};
@@ -259,11 +254,11 @@ export default class HIRBuilder {
const importDeclaration =
path.parentPath as NodePath<t.ImportDeclaration>;
return {
kind: 'ImportSpecifier',
kind: "ImportSpecifier",
name: originalName,
module: importDeclaration.node.source.value,
imported:
path.node.imported.type === 'Identifier'
path.node.imported.type === "Identifier"
? path.node.imported.name
: path.node.imported.value,
};
@@ -271,13 +266,13 @@ export default class HIRBuilder {
const importDeclaration =
path.parentPath as NodePath<t.ImportDeclaration>;
return {
kind: 'ImportNamespace',
kind: "ImportNamespace",
name: originalName,
module: importDeclaration.node.source.value,
};
} else {
return {
kind: 'ModuleLocal',
kind: "ModuleLocal",
name: originalName,
};
}
@@ -288,7 +283,7 @@ export default class HIRBuilder {
babelBinding.scope.rename(originalName, resolvedBinding.name.value);
}
return {
kind: 'Identifier',
kind: "Identifier",
identifier: resolvedBinding,
bindingKind: babelBinding.kind,
};
@@ -299,7 +294,7 @@ export default class HIRBuilder {
if (binding) {
// Check if the binding is from module scope, if so return null
const outerBinding = this.parentFunction.scope.parent.getBinding(
path.node.name,
path.node.name
);
if (binding === outerBinding) {
return false;
@@ -311,12 +306,6 @@ export default class HIRBuilder {
}
resolveBinding(node: t.Identifier): Identifier {
if (node.name === 'fbt') {
CompilerError.throwTodo({
reason: 'Support local variables named "fbt"',
loc: node.loc ?? null,
});
}
const originalName = node.name;
let name = originalName;
let index = 0;
@@ -335,7 +324,7 @@ export default class HIRBuilder {
type: makeType(),
loc: node.loc ?? GeneratedSource,
};
this.#bindings.set(name, {node, identifier});
this.#bindings.set(name, { node, identifier });
return identifier;
} else if (mapping.node === node) {
return mapping.identifier;
@@ -356,7 +345,7 @@ export default class HIRBuilder {
if (
!rpoBlocks.has(id) &&
block.instructions.some(
instr => instr.value.kind === 'FunctionExpression',
(instr) => instr.value.kind === "FunctionExpression"
)
) {
CompilerError.throwTodo({
@@ -380,7 +369,7 @@ export default class HIRBuilder {
// Terminate the current block w the given terminal, and start a new block
terminate(terminal: Terminal, nextBlockKind: BlockKind | null): void {
const {id: blockId, kind, instructions} = this.#current;
const { id: blockId, kind, instructions } = this.#current;
this.#completed.set(blockId, {
kind,
id: blockId,
@@ -400,7 +389,7 @@ export default class HIRBuilder {
* reserved block as the new current block
*/
terminateWithContinuation(terminal: Terminal, continuation: WipBlock): void {
const {id: blockId, kind, instructions} = this.#current;
const { id: blockId, kind, instructions } = this.#current;
this.#completed.set(blockId, {
kind: kind,
id: blockId,
@@ -423,7 +412,7 @@ export default class HIRBuilder {
// Save a previously reserved block as completed
complete(block: WipBlock, terminal: Terminal): void {
const {id: blockId, kind, instructions} = block;
const { id: blockId, kind, instructions } = block;
this.#completed.set(blockId, {
kind,
id: blockId,
@@ -442,7 +431,7 @@ export default class HIRBuilder {
const current = this.#current;
this.#current = wip;
const terminal = fn();
const {id: blockId, kind, instructions} = this.#current;
const { id: blockId, kind, instructions } = this.#current;
this.#completed.set(blockId, {
kind,
id: blockId,
@@ -470,7 +459,7 @@ export default class HIRBuilder {
label<T>(label: string, breakBlock: BlockId, fn: () => T): T {
this.#scopes.push({
kind: 'label',
kind: "label",
breakBlock,
label,
});
@@ -478,22 +467,22 @@ export default class HIRBuilder {
const last = this.#scopes.pop();
CompilerError.invariant(
last != null &&
last.kind === 'label' &&
last.kind === "label" &&
last.label === label &&
last.breakBlock === breakBlock,
{
reason: 'Mismatched label',
reason: "Mismatched label",
description: null,
loc: null,
suggestions: null,
},
}
);
return value;
}
switch<T>(label: string | null, breakBlock: BlockId, fn: () => T): T {
this.#scopes.push({
kind: 'switch',
kind: "switch",
breakBlock,
label,
});
@@ -501,15 +490,15 @@ export default class HIRBuilder {
const last = this.#scopes.pop();
CompilerError.invariant(
last != null &&
last.kind === 'switch' &&
last.kind === "switch" &&
last.label === label &&
last.breakBlock === breakBlock,
{
reason: 'Mismatched label',
reason: "Mismatched label",
description: null,
loc: null,
suggestions: null,
},
}
);
return value;
}
@@ -524,10 +513,10 @@ export default class HIRBuilder {
continueBlock: BlockId,
// block following the loop. "break" jumps here.
breakBlock: BlockId,
fn: () => T,
fn: () => T
): T {
this.#scopes.push({
kind: 'loop',
kind: "loop",
label,
continueBlock,
breakBlock,
@@ -536,16 +525,16 @@ export default class HIRBuilder {
const last = this.#scopes.pop();
CompilerError.invariant(
last != null &&
last.kind === 'loop' &&
last.kind === "loop" &&
last.label === label &&
last.continueBlock === continueBlock &&
last.breakBlock === breakBlock,
{
reason: 'Mismatched loops',
reason: "Mismatched loops",
description: null,
loc: null,
suggestions: null,
},
}
);
return value;
}
@@ -559,14 +548,14 @@ export default class HIRBuilder {
const scope = this.#scopes[ii];
if (
(label === null &&
(scope.kind === 'loop' || scope.kind === 'switch')) ||
(scope.kind === "loop" || scope.kind === "switch")) ||
label === scope.label
) {
return scope.breakBlock;
}
}
CompilerError.invariant(false, {
reason: 'Expected a loop or switch to be in scope',
reason: "Expected a loop or switch to be in scope",
description: null,
loc: null,
suggestions: null,
@@ -581,13 +570,13 @@ export default class HIRBuilder {
lookupContinue(label: string | null): BlockId {
for (let ii = this.#scopes.length - 1; ii >= 0; ii--) {
const scope = this.#scopes[ii];
if (scope.kind === 'loop') {
if (scope.kind === "loop") {
if (label === null || label === scope.label) {
return scope.continueBlock;
}
} else if (label !== null && scope.label === label) {
CompilerError.invariant(false, {
reason: 'Continue may only refer to a labeled loop',
reason: "Continue may only refer to a labeled loop",
description: null,
loc: null,
suggestions: null,
@@ -595,7 +584,7 @@ export default class HIRBuilder {
}
}
CompilerError.invariant(false, {
reason: 'Expected a loop to be in scope',
reason: "Expected a loop to be in scope",
description: null,
loc: null,
suggestions: null,
@@ -644,7 +633,7 @@ function _shrink(func: HIR): void {
}
reachable.add(blockId);
const block = func.blocks.get(blockId)!;
block.terminal = mapTerminalSuccessors(block.terminal, prevTarget => {
block.terminal = mapTerminalSuccessors(block.terminal, (prevTarget) => {
const target = resolveBlockTarget(prevTarget);
queue.push(target);
return target;
@@ -660,7 +649,7 @@ function _shrink(func: HIR): void {
export function removeUnreachableForUpdates(fn: HIR): void {
for (const [, block] of fn.blocks) {
if (
block.terminal.kind === 'for' &&
block.terminal.kind === "for" &&
block.terminal.update !== null &&
!fn.blocks.has(block.terminal.update)
) {
@@ -681,10 +670,10 @@ export function removeDeadDoWhileStatements(func: HIR): void {
* MergeConsecutiveBlocks figures out how to merge as appropriate.
*/
for (const [_, block] of func.blocks) {
if (block.terminal.kind === 'do-while') {
if (block.terminal.kind === "do-while") {
if (!visited.has(block.terminal.test)) {
block.terminal = {
kind: 'goto',
kind: "goto",
block: block.terminal.loop,
variant: GotoVariant.Break,
id: block.terminal.id,
@@ -711,7 +700,7 @@ export function reversePostorderBlocks(func: HIR): void {
* may be in the output: blocks will be removed in the case of unreachable code in
* the input.
*/
function getReversePostorderedBlocks(func: HIR): HIR['blocks'] {
function getReversePostorderedBlocks(func: HIR): HIR["blocks"] {
const visited: Set<BlockId> = new Set();
const used: Set<BlockId> = new Set();
const usedFallthroughs: Set<BlockId> = new Set();
@@ -783,7 +772,7 @@ function getReversePostorderedBlocks(func: HIR): HIR['blocks'] {
...block,
instructions: [],
terminal: {
kind: 'unreachable',
kind: "unreachable",
id: block.terminal.id,
loc: block.terminal.loc,
},
@@ -824,7 +813,7 @@ export function markPredecessors(func: HIR): void {
return;
}
CompilerError.invariant(block != null, {
reason: 'unexpected missing block',
reason: "unexpected missing block",
description: `block ${blockId}`,
loc: GeneratedSource,
});
@@ -837,7 +826,7 @@ export function markPredecessors(func: HIR): void {
}
visited.add(blockId);
const {terminal} = block;
const { terminal } = block;
for (const successor of eachTerminalSuccessor(terminal)) {
visit(successor, block);
@@ -852,7 +841,7 @@ export function markPredecessors(func: HIR): void {
*/
function getTargetIfIndirection(block: BasicBlock): number | null {
return block.instructions.length === 0 &&
block.terminal.kind === 'goto' &&
block.terminal.kind === "goto" &&
block.terminal.variant === GotoVariant.Break
? block.terminal.block
: null;
@@ -865,14 +854,14 @@ function getTargetIfIndirection(block: BasicBlock): number | null {
export function removeUnnecessaryTryCatch(fn: HIR): void {
for (const [, block] of fn.blocks) {
if (
block.terminal.kind === 'try' &&
block.terminal.kind === "try" &&
!fn.blocks.has(block.terminal.handler)
) {
const handlerId = block.terminal.handler;
const fallthroughId = block.terminal.fallthrough;
const fallthrough = fn.blocks.get(fallthroughId);
block.terminal = {
kind: 'goto',
kind: "goto",
block: block.terminal.block,
id: makeInstructionId(0),
loc: block.terminal.loc,
@@ -893,13 +882,13 @@ export function removeUnnecessaryTryCatch(fn: HIR): void {
export function createTemporaryPlace(
env: Environment,
loc: SourceLocation,
loc: SourceLocation
): Place {
return {
kind: 'Identifier',
kind: "Identifier",
identifier: {
id: env.nextIdentifierId,
mutableRange: {start: makeInstructionId(0), end: makeInstructionId(0)},
mutableRange: { start: makeInstructionId(0), end: makeInstructionId(0) },
name: null,
scope: null,
type: makeType(),

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