Compare commits

..

1 Commits

Author SHA1 Message Date
Mofei Zhang
2b817b78bc [compiler] Playground qol: shared compilation option directives with tests
- Adds @compilationMode(all|infer|syntax|annotation) and @panicMode(none) directives. This is now shared with our test infra
- Playground still defaults to `infer` mode while tests default to `all` mode
- See added fixture tests
2025-01-09 12:29:03 -05:00
3900 changed files with 271026 additions and 189195 deletions

View File

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

View File

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

View File

@@ -446,7 +446,10 @@ module.exports = {
},
},
{
files: ['scripts/eslint-rules/*.js'],
files: [
'scripts/eslint-rules/*.js',
'packages/eslint-plugin-react-hooks/src/*.js',
],
plugins: ['eslint-plugin'],
rules: {
'eslint-plugin/prefer-object-rule': ERROR,
@@ -468,14 +471,13 @@ module.exports = {
files: ['packages/react-server-dom-webpack/**/*.js'],
globals: {
__webpack_chunk_load__: 'readonly',
__webpack_get_script_filename__: 'readonly',
__webpack_require__: 'readonly',
},
},
{
files: ['packages/react-server-dom-turbopack/**/*.js'],
globals: {
__turbopack_load_by_url__: 'readonly',
__turbopack_load__: 'readonly',
__turbopack_require__: 'readonly',
},
},
@@ -494,21 +496,16 @@ module.exports = {
{
files: [
'packages/react-devtools-extensions/**/*.js',
'packages/react-devtools-shared/src/devtools/views/**/*.js',
'packages/react-devtools-shared/src/hook.js',
'packages/react-devtools-shared/src/backend/console.js',
'packages/react-devtools-shared/src/backend/fiber/renderer.js',
'packages/react-devtools-shared/src/backend/shared/DevToolsComponentStackFrame.js',
'packages/react-devtools-shared/src/frontend/utils/withPermissionsCheck.js',
],
globals: {
__IS_CHROME__: 'readonly',
__IS_FIREFOX__: 'readonly',
__IS_EDGE__: 'readonly',
__IS_NATIVE__: 'readonly',
__IS_INTERNAL_MCP_BUILD__: 'readonly',
__IS_INTERNAL_VERSION__: 'readonly',
chrome: 'readonly',
},
},
{
@@ -517,34 +514,6 @@ module.exports = {
__IS_INTERNAL_VERSION__: 'readonly',
},
},
{
files: ['packages/react-devtools-*/**/*.js'],
excludedFiles: '**/__tests__/**/*.js',
plugins: ['eslint-plugin-react-hooks-published'],
rules: {
'react-hooks-published/rules-of-hooks': ERROR,
},
},
{
files: ['packages/eslint-plugin-react-hooks/src/**/*'],
extends: ['plugin:@typescript-eslint/recommended'],
parser: '@typescript-eslint/parser',
plugins: ['@typescript-eslint', 'eslint-plugin'],
rules: {
'@typescript-eslint/no-explicit-any': OFF,
'@typescript-eslint/no-non-null-assertion': OFF,
'@typescript-eslint/array-type': [ERROR, {default: 'generic'}],
'es/no-optional-chaining': OFF,
'eslint-plugin/prefer-object-rule': ERROR,
'eslint-plugin/require-meta-fixable': [
ERROR,
{catchNoFixerButFixableProperty: true},
],
'eslint-plugin/require-meta-has-suggestions': ERROR,
},
},
],
env: {
@@ -555,10 +524,13 @@ module.exports = {
},
globals: {
$Call: 'readonly',
$ElementType: 'readonly',
$Flow$ModuleRef: 'readonly',
$FlowFixMe: 'readonly',
$Keys: 'readonly',
$NonMaybeType: 'readonly',
$PropertyType: 'readonly',
$ReadOnly: 'readonly',
$ReadOnlyArray: 'readonly',
$ArrayBufferView: 'readonly',
@@ -567,13 +539,11 @@ module.exports = {
ConsoleTask: 'readonly', // TOOD: Figure out what the official name of this will be.
ReturnType: 'readonly',
AnimationFrameID: 'readonly',
WeakRef: 'readonly',
// For Flow type annotation. Only `BigInt` is valid at runtime.
bigint: 'readonly',
BigInt: 'readonly',
BigInt64Array: 'readonly',
BigUint64Array: 'readonly',
CacheType: 'readonly',
Class: 'readonly',
ClientRect: 'readonly',
CopyInspectedElementPath: 'readonly',
@@ -585,19 +555,15 @@ module.exports = {
$AsyncIterator: 'readonly',
Iterator: 'readonly',
AsyncIterator: 'readonly',
IntervalID: 'readonly',
IteratorResult: 'readonly',
JSONValue: 'readonly',
JSResourceReference: 'readonly',
mixin$Animatable: 'readonly',
MouseEventHandler: 'readonly',
NavigateEvent: 'readonly',
PerformanceMeasureOptions: 'readonly',
PropagationPhases: 'readonly',
PropertyDescriptor: 'readonly',
PropertyDescriptorMap: 'readonly',
Proxy$traps: 'readonly',
React$AbstractComponent: 'readonly',
React$Component: 'readonly',
React$ComponentType: 'readonly',
React$Config: 'readonly',
React$Context: 'readonly',
React$Element: 'readonly',
@@ -618,21 +584,11 @@ module.exports = {
symbol: 'readonly',
SyntheticEvent: 'readonly',
SyntheticMouseEvent: 'readonly',
SyntheticPointerEvent: 'readonly',
Thenable: 'readonly',
TimeoutID: 'readonly',
WheelEventHandler: 'readonly',
FinalizationRegistry: 'readonly',
Exclude: 'readonly',
Omit: 'readonly',
Keyframe: 'readonly',
PropertyIndexedKeyframes: 'readonly',
KeyframeAnimationOptions: 'readonly',
GetAnimationsOptions: 'readonly',
ScrollTimeline: 'readonly',
EventListenerOptionsOrUseCapture: 'readonly',
FocusOptions: 'readonly',
OptionalEffectTiming: 'readonly',
spyOnDev: 'readonly',
spyOnDevAndProd: 'readonly',
@@ -650,6 +606,5 @@ module.exports = {
AsyncLocalStorage: 'readonly',
async_hooks: 'readonly',
globalThis: 'readonly',
navigation: 'readonly',
},
};

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

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

View File

@@ -1,49 +0,0 @@
name: (Compiler) Discord Notify
on:
pull_request_target:
types: [opened, ready_for_review]
paths:
- compiler/**
- .github/workflows/compiler_**.yml
permissions: {}
jobs:
check_access:
if: ${{ github.event.pull_request.draft == false }}
runs-on: ubuntu-latest
outputs:
is_member_or_collaborator: ${{ steps.check_is_member_or_collaborator.outputs.is_member_or_collaborator }}
steps:
- run: echo ${{ github.event.pull_request.author_association }}
- name: Check is member or collaborator
id: check_is_member_or_collaborator
if: ${{ github.event.pull_request.author_association == 'MEMBER' || github.event.pull_request.author_association == 'COLLABORATOR' }}
run: echo "is_member_or_collaborator=true" >> "$GITHUB_OUTPUT"
check_maintainer:
if: ${{ needs.check_access.outputs.is_member_or_collaborator == 'true' || needs.check_access.outputs.is_member_or_collaborator == true }}
needs: [check_access]
uses: facebook/react/.github/workflows/shared_check_maintainer.yml@main
permissions:
# Used by check_maintainer
contents: read
with:
actor: ${{ github.event.pull_request.user.login }}
notify:
if: ${{ needs.check_maintainer.outputs.is_core_team == 'true' }}
needs: check_maintainer
runs-on: ubuntu-latest
steps:
- name: Discord Webhook Action
uses: tsickert/discord-webhook@86dc739f3f165f16dadc5666051c367efa1692f4
with:
webhook-url: ${{ secrets.COMPILER_DISCORD_WEBHOOK_URL }}
embed-author-name: ${{ github.event.pull_request.user.login }}
embed-author-url: ${{ github.event.pull_request.user.html_url }}
embed-author-icon-url: ${{ github.event.pull_request.user.avatar_url }}
embed-title: '#${{ github.event.number }} (+${{github.event.pull_request.additions}} -${{github.event.pull_request.deletions}}): ${{ github.event.pull_request.title }}'
embed-description: ${{ github.event.pull_request.body }}
embed-url: ${{ github.event.pull_request.html_url }}

View File

@@ -8,8 +8,6 @@ on:
- compiler/**
- .github/workflows/compiler_playground.yml
permissions: {}
concurrency:
group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.event.pull_request.number || github.run_id }}
cancel-in-progress: true
@@ -38,25 +36,10 @@ jobs:
uses: actions/cache@v4
id: node_modules
with:
path: |
**/node_modules
key: compiler-and-playground-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('compiler/**/yarn.lock') }}
path: "**/node_modules"
key: compiler-node_modules-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('compiler/**/yarn.lock') }}
- run: yarn install --frozen-lockfile
if: steps.node_modules.outputs.cache-hit != 'true'
working-directory: compiler
- run: yarn install --frozen-lockfile
if: steps.node_modules.outputs.cache-hit != 'true'
- name: Check Playwright version
id: playwright_version
run: echo "playwright_version=$(npm ls @playwright/test | grep @playwright | sed 's/.*@//' | head -1)" >> "$GITHUB_OUTPUT"
- name: Cache Playwright Browsers for version ${{ steps.playwright_version.outputs.playwright_version }}
id: cache_playwright_browsers
uses: actions/cache@v4
with:
path: ~/.cache/ms-playwright
key: playwright-browsers-v6-${{ runner.arch }}-${{ runner.os }}-${{ steps.playwright_version.outputs.playwright_version }}
- run: npx playwright install --with-deps chromium
if: steps.cache_playwright_browsers.outputs.cache-hit != 'true'
- run: CI=true yarn test
- run: ls -R test-results
if: '!cancelled()'
@@ -66,4 +49,3 @@ jobs:
with:
name: test-results
path: compiler/apps/playground/test-results
if-no-files-found: ignore

View File

@@ -16,22 +16,15 @@ on:
version_name:
required: true
type: string
tag_version:
required: false
type: string
dry_run:
required: false
type: boolean
secrets:
NPM_TOKEN:
required: true
permissions: {}
env:
TZ: /usr/share/zoneinfo/America/Los_Angeles
# https://github.com/actions/cache/blob/main/tips-and-workarounds.md#cache-segment-restore-timeout
SEGMENT_DOWNLOAD_TIMEOUT_MINS: 1
GH_TOKEN: ${{ github.token }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
defaults:
@@ -53,18 +46,10 @@ jobs:
uses: actions/cache@v4
id: node_modules
with:
path: |
**/node_modules
key: compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('compiler/yarn.lock') }}
path: "**/node_modules"
key: compiler-node_modules-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('compiler/**/yarn.lock') }}
- run: yarn install --frozen-lockfile
if: steps.node_modules.outputs.cache-hit != 'true'
- if: inputs.dry_run == true
name: Publish packages to npm (dry run)
- name: Publish packages to npm
run: |
cp ./scripts/release/ci-npmrc ~/.npmrc
scripts/release/publish.js --frfr --debug --ci --versionName=${{ inputs.version_name }} --tag=${{ inputs.dist_tag }} ${{ inputs.tag_version && format('--tagVersion={0}', inputs.tag_version) || '' }}
- if: inputs.dry_run != true
name: Publish packages to npm
run: |
cp ./scripts/release/ci-npmrc ~/.npmrc
scripts/release/publish.js --frfr --ci --versionName=${{ inputs.version_name }} --tag=${{ inputs.dist_tag }} ${{ inputs.tag_version && format('--tagVersion={0}', inputs.tag_version) || '' }}
scripts/release/publish.js --frfr --ci --versionName=${{ inputs.version_name }} --tag ${{ inputs.dist_tag }}

View File

@@ -14,14 +14,6 @@ on:
version_name:
required: true
type: string
tag_version:
required: false
type: string
dry_run:
required: false
type: boolean
permissions: {}
env:
TZ: /usr/share/zoneinfo/America/Los_Angeles
@@ -35,7 +27,5 @@ jobs:
release_channel: ${{ inputs.release_channel }}
dist_tag: ${{ inputs.dist_tag }}
version_name: ${{ inputs.version_name }}
tag_version: ${{ inputs.tag_version }}
dry_run: ${{ inputs.dry_run }}
secrets:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}

View File

@@ -5,8 +5,6 @@ on:
# At 10 minutes past 16:00 on Mon, Tue, Wed, Thu, and Fri
- cron: 10 16 * * 1,2,3,4,5
permissions: {}
env:
TZ: /usr/share/zoneinfo/America/Los_Angeles
@@ -19,6 +17,5 @@ jobs:
release_channel: experimental
dist_tag: experimental
version_name: '0.0.0'
dry_run: false
secrets:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}

View File

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

77
.github/workflows/compiler_rust.yml vendored Normal file
View File

@@ -0,0 +1,77 @@
name: (Compiler) Rust
on:
push:
branches: ["main"]
paths:
- .github/workflows/**
- compiler/crates/**
- compiler/Cargo.*
- compiler/*.toml
pull_request:
paths:
- .github/workflows/**
- compiler/crates/**
- compiler/Cargo.*
- compiler/*.toml
concurrency:
group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.event.pull_request.number || github.run_id }}
cancel-in-progress: true
env:
CARGO_TERM_COLOR: always
RUSTFLAGS: -Dwarnings
TZ: /usr/share/zoneinfo/America/Los_Angeles
defaults:
run:
working-directory: compiler
jobs:
test:
name: Rust Test (${{ matrix.target.os }})
strategy:
matrix:
target:
- target: ubuntu-latest
os: ubuntu-latest
# TODO: run on more platforms
# - target: macos-latest
# os: macos-latest
# - target: windows-latest
# os: windows-latest
runs-on: ${{ matrix.target.os }}
steps:
- uses: actions/checkout@v4
- uses: Swatinem/rust-cache@v2
- name: cargo test
run: cargo test --manifest-path=Cargo.toml --locked ${{ matrix.target.features && '--features' }} ${{ matrix.target.features }}
lint:
name: Rust Lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions-rs/toolchain@v1
with:
# NOTE: use `rustup run <toolchain> <command>` in commands below
# with this exact same toolchain value
toolchain: nightly-2023-08-01
override: true
components: rustfmt, clippy
- uses: Swatinem/rust-cache@v2
- name: rustfmt
run: grep -r --include "*.rs" --files-without-match "@generated" crates | xargs rustup run nightly-2023-08-01 rustfmt --check --config="skip_children=true"
# - name: cargo clippy
# run: rustup run nightly-2023-08-01 cargo clippy -- -Dclippy::correctness
build:
name: Rust Build
runs-on: ubuntu-latest
# TODO: build on more platforms, deploy, etc
steps:
- uses: actions/checkout@v4
- uses: Swatinem/rust-cache@v2
- name: cargo build
run: cargo build --release

View File

@@ -8,8 +8,6 @@ on:
- compiler/**
- .github/workflows/compiler_typescript.yml
permissions: {}
concurrency:
group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.event.pull_request.number || github.run_id }}
cancel-in-progress: true
@@ -47,13 +45,10 @@ jobs:
cache-dependency-path: compiler/yarn.lock
- name: Restore cached node_modules
uses: actions/cache@v4
id: node_modules
with:
path: |
**/node_modules
key: compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('compiler/yarn.lock') }}
path: "**/node_modules"
key: compiler-node_modules-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('compiler/**/yarn.lock') }}
- run: yarn install --frozen-lockfile
if: steps.node_modules.outputs.cache-hit != 'true'
- run: yarn workspace babel-plugin-react-compiler lint
# Hardcoded to improve parallelism
@@ -71,19 +66,17 @@ jobs:
uses: actions/cache@v4
id: node_modules
with:
path: |
**/node_modules
key: compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('compiler/yarn.lock') }}
path: "**/node_modules"
key: compiler-node_modules-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('compiler/**/yarn.lock') }}
- run: yarn install --frozen-lockfile
if: steps.node_modules.outputs.cache-hit != 'true'
- run: yarn workspace babel-plugin-react-compiler jest
test:
name: Test ${{ matrix.workspace_name }}
needs: discover_yarn_workspaces
runs-on: ubuntu-latest
continue-on-error: true
strategy:
fail-fast: false
matrix:
workspace_name: ${{ fromJSON(needs.discover_yarn_workspaces.outputs.matrix) }}
steps:
@@ -97,12 +90,7 @@ jobs:
uses: actions/cache@v4
id: node_modules
with:
path: |
**/node_modules
key: compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('compiler/yarn.lock') }}
path: "**/node_modules"
key: compiler-node_modules-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('compiler/**/yarn.lock') }}
- run: yarn install --frozen-lockfile
if: steps.node_modules.outputs.cache-hit != 'true'
- run: xvfb-run -a yarn workspace ${{ matrix.workspace_name }} test
if: runner.os == 'Linux' && matrix.workspace_name == 'react-forgive'
- run: yarn workspace ${{ matrix.workspace_name }} test
if: matrix.workspace_name != 'react-forgive'

View File

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

View File

@@ -9,8 +9,6 @@ on:
required: false
type: string
permissions: {}
env:
TZ: /usr/share/zoneinfo/America/Los_Angeles
# https://github.com/actions/cache/blob/main/tips-and-workarounds.md#cache-segment-restore-timeout
@@ -20,9 +18,6 @@ jobs:
download_build:
name: Download base build
runs-on: ubuntu-latest
permissions:
# We use github.token to download the build artifact from a previous runtime_build_and_test.yml run
actions: read
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
@@ -34,15 +29,13 @@ jobs:
uses: actions/cache@v4
id: node_modules
with:
path: |
**/node_modules
key: runtime-release-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'scripts/release/yarn.lock') }}
path: "**/node_modules"
key: runtime-release-node_modules-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'scripts/release/yarn.lock') }}
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
if: steps.node_modules.outputs.cache-hit != 'true'
- run: yarn --cwd scripts/release install --frozen-lockfile
if: steps.node_modules.outputs.cache-hit != 'true'
- run: yarn install --frozen-lockfile
working-directory: scripts/release
- name: Download react-devtools artifacts for base revision
run: |
git fetch origin main
@@ -54,7 +47,6 @@ jobs:
with:
name: build
path: build
if-no-files-found: error
build_devtools_and_process_artifacts:
name: Build DevTools and process artifacts
@@ -71,13 +63,11 @@ jobs:
uses: actions/cache@v4
id: node_modules
with:
path: |
**/node_modules
key: runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
path: "**/node_modules"
key: runtime-node_modules-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
if: steps.node_modules.outputs.cache-hit != 'true'
- name: Restore archived build
uses: actions/download-artifact@v4
with:
@@ -92,28 +82,24 @@ jobs:
uses: actions/upload-artifact@v4
with:
name: react-devtools
path: build/devtools
if-no-files-found: error
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
if-no-files-found: error
- name: Archive firefox extension
uses: actions/upload-artifact@v4
with:
name: react-devtools-firefox-extension
path: build/devtools/firefox-extension.zip
if-no-files-found: error
run_devtools_tests_for_versions:
name: Run DevTools tests for versions
needs: build_devtools_and_process_artifacts
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
version:
- "16.0"
@@ -122,6 +108,7 @@ jobs:
- "17.0"
- "18.0"
- "18.2" # compiler polyfill
continue-on-error: true
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
@@ -133,11 +120,9 @@ jobs:
uses: actions/cache@v4
id: node_modules
with:
path: |
**/node_modules
key: runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
path: "**/node_modules"
key: runtime-node_modules-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
- run: yarn install --frozen-lockfile
if: steps.node_modules.outputs.cache-hit != 'true'
- name: Restore all archived build artifacts
uses: actions/download-artifact@v4
- name: Display structure of build
@@ -150,7 +135,6 @@ jobs:
needs: build_devtools_and_process_artifacts
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
version:
- "16.0"
@@ -158,6 +142,7 @@ jobs:
- "16.8" # hooks
- "17.0"
- "18.0"
continue-on-error: true
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
@@ -169,28 +154,17 @@ jobs:
uses: actions/cache@v4
id: node_modules
with:
path: |
**/node_modules
key: runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
path: "**/node_modules"
key: runtime-node_modules-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
- run: yarn install --frozen-lockfile
if: steps.node_modules.outputs.cache-hit != 'true'
- name: Restore all archived build artifacts
uses: actions/download-artifact@v4
- name: Display structure of build
run: ls -R build
- name: Check Playwright version
id: playwright_version
run: echo "playwright_version=$(npm ls @playwright/test | grep @playwright | sed 's/.*@//' | head -1)" >> "$GITHUB_OUTPUT"
- name: Cache Playwright Browsers for version ${{ steps.playwright_version.outputs.playwright_version }}
id: cache_playwright_browsers
uses: actions/cache@v4
with:
path: ~/.cache/ms-playwright
key: playwright-browsers-v6-${{ runner.arch }}-${{ runner.os }}-${{ steps.playwright_version.outputs.playwright_version }}
- run: npx playwright install --with-deps
if: steps.cache_playwright_browsers.outputs.cache-hit != 'true'
- run: npx playwright install-deps
if: steps.cache_playwright_browsers.outputs.cache-hit == 'true'
- 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 }}
@@ -201,5 +175,4 @@ jobs:
- uses: actions/upload-artifact@v4
with:
name: screenshots
path: ./tmp/playwright-artifacts
if-no-files-found: warn
path: ./tmp/screenshots

View File

@@ -6,14 +6,6 @@ on:
pull_request:
paths-ignore:
- compiler/**
workflow_dispatch:
inputs:
commit_sha:
required: false
type: string
default: ''
permissions: {}
concurrency:
group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.event.pull_request.number || github.run_id }}
@@ -25,95 +17,6 @@ env:
SEGMENT_DOWNLOAD_TIMEOUT_MINS: 1
jobs:
# ----- NODE_MODULES CACHE -----
# Centralize the node_modules cache so it is saved once and each subsequent job only needs to
# restore the cache. Prevents race conditions where multiple workflows try to write to the cache.
runtime_node_modules_cache:
name: Cache Runtime node_modules
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }}
- name: Check cache hit
uses: actions/cache/restore@v4
id: node_modules
with:
path: |
**/node_modules
key: runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
lookup-only: true
- uses: actions/setup-node@v4
if: steps.node_modules.outputs.cache-hit != 'true'
with:
node-version-file: '.nvmrc'
cache: yarn
cache-dependency-path: yarn.lock
- name: Warm with old cache
if: steps.node_modules.outputs.cache-hit != 'true'
uses: actions/cache/restore@v4
with:
path: |
**/node_modules
key: runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
restore-keys: |
runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-
runtime-node_modules-v6-
- run: yarn install --frozen-lockfile
if: steps.node_modules.outputs.cache-hit != 'true'
- name: Save cache
if: steps.node_modules.outputs.cache-hit != 'true'
uses: actions/cache/save@v4
with:
path: |
**/node_modules
key: runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
runtime_compiler_node_modules_cache:
name: Cache Runtime, Compiler node_modules
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }}
- name: Check cache hit
uses: actions/cache/restore@v4
id: node_modules
with:
path: |
**/node_modules
key: runtime-and-compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }}
lookup-only: true
- uses: actions/setup-node@v4
if: steps.node_modules.outputs.cache-hit != 'true'
with:
node-version-file: '.nvmrc'
cache: yarn
cache-dependency-path: |
yarn.lock
compiler/yarn.lock
- name: Warm with old cache
if: steps.node_modules.outputs.cache-hit != 'true'
uses: actions/cache/restore@v4
with:
path: |
**/node_modules
key: runtime-and-compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }}
restore-keys: |
runtime-and-compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-
runtime-and-compiler-node_modules-v6-
- run: yarn install --frozen-lockfile
if: steps.node_modules.outputs.cache-hit != 'true'
- run: yarn --cwd compiler install --frozen-lockfile
if: steps.node_modules.outputs.cache-hit != 'true'
- name: Save cache
if: steps.node_modules.outputs.cache-hit != 'true'
uses: actions/cache/save@v4
with:
path: |
**/node_modules
key: runtime-and-compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }}
# ----- FLOW -----
discover_flow_inline_configs:
name: Discover flow inline configs
@@ -123,7 +26,7 @@ jobs:
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }}
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- uses: actions/github-script@v7
id: set-matrix
with:
@@ -133,103 +36,87 @@ jobs:
flow:
name: Flow check ${{ matrix.flow_inline_config_shortname }}
needs: [discover_flow_inline_configs, runtime_node_modules_cache]
needs: discover_flow_inline_configs
runs-on: ubuntu-latest
continue-on-error: true
strategy:
fail-fast: false
matrix:
flow_inline_config_shortname: ${{ fromJSON(needs.discover_flow_inline_configs.outputs.matrix) }}
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }}
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: yarn
cache-dependency-path: yarn.lock
- name: Restore cached node_modules
uses: actions/cache/restore@v4
uses: actions/cache@v4
id: node_modules
with:
path: |
**/node_modules
key: runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
restore-keys: |
runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-
runtime-node_modules-v6-
path: "**/node_modules"
key: runtime-node_modules-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
if: steps.node_modules.outputs.cache-hit != 'true'
- 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
needs: [runtime_node_modules_cache]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }}
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: yarn
cache-dependency-path: yarn.lock
- name: Restore cached node_modules
uses: actions/cache/restore@v4
uses: actions/cache@v4
id: node_modules
with:
path: |
**/node_modules
key: runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
restore-keys: |
runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-
runtime-node_modules-v6-
path: "**/node_modules"
key: runtime-node_modules-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
if: steps.node_modules.outputs.cache-hit != 'true'
- run: |
yarn generate-inline-fizz-runtime
git diff --exit-code || (echo "There was a change to the Fizz runtime. Run \`yarn generate-inline-fizz-runtime\` and check in the result." && false)
git diff --quiet || (echo "There was a change to the Fizz runtime. Run `yarn generate-inline-fizz-runtime` and check in the result." && false)
# ----- FEATURE FLAGS -----
flags:
name: Check flags
needs: [runtime_node_modules_cache]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }}
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: yarn
cache-dependency-path: yarn.lock
- name: Restore cached node_modules
uses: actions/cache/restore@v4
uses: actions/cache@v4
id: node_modules
with:
path: |
**/node_modules
key: runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
path: "**/node_modules"
key: runtime-node_modules-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
if: steps.node_modules.outputs.cache-hit != 'true'
- run: yarn flags
# ----- TESTS -----
test:
name: yarn test ${{ matrix.params }} (Shard ${{ matrix.shard }})
needs: [runtime_compiler_node_modules_cache]
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
params:
- "-r=stable --env=development"
@@ -257,109 +144,59 @@ jobs:
- 3/5
- 4/5
- 5/5
continue-on-error: true
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }}
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: yarn
cache-dependency-path: |
yarn.lock
compiler/yarn.lock
- name: Restore cached node_modules
uses: actions/cache/restore@v4
id: node_modules
with:
path: |
**/node_modules
key: runtime-and-compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }}
restore-keys: |
runtime-and-compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-
runtime-and-compiler-node_modules-v6-
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
if: steps.node_modules.outputs.cache-hit != 'true'
- run: yarn --cwd compiler install --frozen-lockfile
if: steps.node_modules.outputs.cache-hit != 'true'
- run: yarn test ${{ matrix.params }} --ci --shard=${{ matrix.shard }}
# Hardcoded to improve parallelism
test-linter:
name: Test eslint-plugin-react-hooks
needs: [runtime_compiler_node_modules_cache]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: yarn
cache-dependency-path: |
yarn.lock
compiler/yarn.lock
cache-dependency-path: yarn.lock
- name: Restore cached node_modules
uses: actions/cache@v4
id: node_modules
with:
path: |
**/node_modules
key: runtime-and-compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }}
- name: Install runtime dependencies
run: yarn install --frozen-lockfile
if: steps.node_modules.outputs.cache-hit != 'true'
- name: Install compiler dependencies
run: yarn install --frozen-lockfile
working-directory: compiler
if: steps.node_modules.outputs.cache-hit != 'true'
- run: ./scripts/react-compiler/build-compiler.sh && ./scripts/react-compiler/link-compiler.sh
- run: yarn workspace eslint-plugin-react-hooks test
path: "**/node_modules"
key: runtime-node_modules-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
- run: yarn test ${{ matrix.params }} --ci --shard=${{ matrix.shard }}
# ----- BUILD -----
build_and_lint:
name: yarn build and lint
needs: [runtime_compiler_node_modules_cache]
runs-on: ubuntu-latest
strategy:
fail-fast: false
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,20,21,22,23,24]
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
with:
ref: ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }}
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: yarn
cache-dependency-path: |
yarn.lock
compiler/yarn.lock
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/restore@v4
uses: actions/cache@v4
id: node_modules
with:
path: |
**/node_modules
key: runtime-and-compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }}
restore-keys: |
runtime-and-compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-
runtime-and-compiler-node_modules-v6-
path: "**/node_modules"
key: runtime-node_modules-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
if: steps.node_modules.outputs.cache-hit != 'true'
- run: yarn --cwd compiler install --frozen-lockfile
if: steps.node_modules.outputs.cache-hit != 'true'
- run: yarn build --index=${{ matrix.worker_id }} --total=25 --r=${{ matrix.release_channel }} --ci
- run: yarn build --index=${{ matrix.worker_id }} --total=20 --r=${{ matrix.release_channel }} --ci
env:
CI: github
RELEASE_CHANNEL: ${{ matrix.release_channel }}
@@ -373,13 +210,11 @@ jobs:
with:
name: _build_${{ matrix.worker_id }}_${{ matrix.release_channel }}
path: build
if-no-files-found: error
test_build:
name: yarn test-build
needs: [build_and_lint, runtime_compiler_node_modules_cache]
needs: build_and_lint
strategy:
fail-fast: false
matrix:
test_params: [
# Intentionally passing these as strings instead of creating a
@@ -412,44 +247,29 @@ jobs:
# TODO: Test more persistent configurations?
]
shard:
- 1/10
- 2/10
- 3/10
- 4/10
- 5/10
- 6/10
- 7/10
- 8/10
- 9/10
- 10/10
- 1/3
- 2/3
- 3/3
continue-on-error: true
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }}
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: yarn
cache-dependency-path: |
yarn.lock
compiler/yarn.lock
cache-dependency-path: yarn.lock
- name: Restore cached node_modules
uses: actions/cache/restore@v4
uses: actions/cache@v4
id: node_modules
with:
path: |
**/node_modules
key: runtime-and-compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }}
restore-keys: |
runtime-and-compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-
runtime-and-compiler-node_modules-v6-
path: "**/node_modules"
key: runtime-node_modules-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
if: steps.node_modules.outputs.cache-hit != 'true'
- run: yarn --cwd compiler install --frozen-lockfile
if: steps.node_modules.outputs.cache-hit != 'true'
- name: Restore archived build
uses: actions/download-artifact@v4
with:
@@ -462,35 +282,26 @@ jobs:
process_artifacts_combined:
name: Process artifacts combined
needs: [build_and_lint, runtime_node_modules_cache]
permissions:
# https://github.com/actions/attest-build-provenance
id-token: write
attestations: write
needs: build_and_lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }}
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: yarn
cache-dependency-path: yarn.lock
- name: Restore cached node_modules
uses: actions/cache/restore@v4
uses: actions/cache@v4
id: node_modules
with:
path: |
**/node_modules
key: runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
restore-keys: |
runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-
runtime-node_modules-v6-
path: "**/node_modules"
key: runtime-node_modules-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
if: steps.node_modules.outputs.cache-hit != 'true'
- name: Restore archived build
uses: actions/download-artifact@v4
with:
@@ -499,7 +310,7 @@ jobs:
merge-multiple: true
- name: Display structure of build
run: ls -R build
- run: echo ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }} >> build/COMMIT_SHA
- run: echo ${{ github.sha }} >> build/COMMIT_SHA
- name: Scrape warning messages
run: |
mkdir -p ./build/__test_utils__
@@ -509,53 +320,35 @@ jobs:
# TODO: Migrate scripts to use `build` directory instead of `build2`
- run: cp ./build.tgz ./build2.tgz
- name: Archive build artifacts
id: upload_artifacts_combined
uses: actions/upload-artifact@v4
with:
name: artifacts_combined
path: |
./build.tgz
./build2.tgz
if-no-files-found: error
- uses: actions/attest-build-provenance@v2
# We don't verify builds generated from pull requests not originating from facebook/react.
# However, if the PR lands, the run on `main` will generate the attestation which can then
# be used to download a build via scripts/release/download-experimental-build.js.
#
# Note that this means that scripts/release/download-experimental-build.js must be run with
# --no-verify when downloading a build from a fork.
if: github.event_name == 'push' && github.ref_name == 'main' || github.event.pull_request.head.repo.full_name == github.repository
with:
subject-name: artifacts_combined.zip
subject-digest: sha256:${{ steps.upload_artifacts_combined.outputs.artifact-digest }}
check_error_codes:
name: Search build artifacts for unminified errors
needs: [build_and_lint, runtime_node_modules_cache]
needs: build_and_lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }}
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: yarn
cache-dependency-path: yarn.lock
- name: Restore cached node_modules
uses: actions/cache/restore@v4
uses: actions/cache@v4
id: node_modules
with:
path: |
**/node_modules
key: runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
restore-keys: |
runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-
runtime-node_modules-v6-
path: "**/node_modules"
key: runtime-node_modules-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
if: steps.node_modules.outputs.cache-hit != 'true'
- name: Restore archived build
uses: actions/download-artifact@v4
with:
@@ -567,35 +360,30 @@ jobs:
- name: Search build artifacts for unminified errors
run: |
yarn extract-errors
git diff --exit-code || (echo "Found unminified errors. Either update the error codes map or disable error minification for the affected build, if appropriate." && false)
git diff --quiet || (echo "Found unminified errors. Either update the error codes map or disable error minification for the affected build, if appropriate." && false)
check_release_dependencies:
name: Check release dependencies
needs: [build_and_lint, runtime_node_modules_cache]
needs: build_and_lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }}
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: yarn
cache-dependency-path: yarn.lock
- name: Restore cached node_modules
uses: actions/cache/restore@v4
uses: actions/cache@v4
id: node_modules
with:
path: |
**/node_modules
key: runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
restore-keys: |
runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-
runtime-node_modules-v6-
path: "**/node_modules"
key: runtime-node_modules-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
if: steps.node_modules.outputs.cache-hit != 'true'
- name: Restore archived build
uses: actions/download-artifact@v4
with:
@@ -613,23 +401,23 @@ jobs:
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }}
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: yarn
cache-dependency-path: yarn.lock
- name: Restore cached node_modules
uses: actions/cache@v4 # note: this does not reuse centralized cache since it has unique cache key
uses: actions/cache@v4
id: node_modules
with:
path: |
**/node_modules
key: fixtures_dom-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'fixtures/dom/yarn.lock') }}
path: "**/node_modules"
key: fixtures_dom-node_modules-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
- name: Ensure clean build directory
run: rm -rf build
- run: yarn --cwd fixtures/dom install --frozen-lockfile
if: steps.node_modules.outputs.cache-hit != 'true'
- 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:
@@ -654,7 +442,7 @@ jobs:
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }}
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
@@ -664,31 +452,14 @@ jobs:
# 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 # note: this does not reuse centralized cache since it has unique cache key
uses: actions/cache@v4
id: node_modules
with:
path: |
**/node_modules
key: fixtures_flight-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'fixtures/flight/yarn.lock') }}
path: "**/node_modules"
key: fixtures_flight-node_modules-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
if: steps.node_modules.outputs.cache-hit != 'true'
- run: yarn --cwd fixtures/flight install --frozen-lockfile
if: steps.node_modules.outputs.cache-hit != 'true'
- name: Check Playwright version
id: playwright_version
run: echo "playwright_version=$(npm ls @playwright/test | grep @playwright | sed 's/.*@//' | head -1)" >> "$GITHUB_OUTPUT"
- name: Cache Playwright Browsers for version ${{ steps.playwright_version.outputs.playwright_version }}
id: cache_playwright_browsers
uses: actions/cache@v4
with:
path: ~/.cache/ms-playwright
key: playwright-browsers-v6-${{ runner.arch }}-${{ runner.os }}-${{ steps.playwright_version.outputs.playwright_version }}
- name: Playwright install deps
if: steps.cache_playwright_browsers.outputs.cache-hit != 'true'
working-directory: fixtures/flight
run: npx playwright install --with-deps chromium
- name: Restore archived build
uses: actions/download-artifact@v4
with:
@@ -697,6 +468,18 @@ jobs:
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
@@ -708,185 +491,164 @@ jobs:
with:
name: flight-playwright-report
path: fixtures/flight/playwright-report
if-no-files-found: warn
- name: Archive Flight fixture artifacts
uses: actions/upload-artifact@v4
with:
name: flight-test-results
path: fixtures/flight/test-results
if-no-files-found: ignore
# ----- DEVTOOLS -----
build_devtools_and_process_artifacts:
name: Build DevTools and process artifacts
needs: [build_and_lint, runtime_node_modules_cache]
needs: build_and_lint
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
browser: [chrome, firefox, edge]
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }}
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: yarn
cache-dependency-path: yarn.lock
- name: Restore cached node_modules
uses: actions/cache/restore@v4
uses: actions/cache@v4
id: node_modules
with:
path: |
**/node_modules
key: runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
restore-keys: |
runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-
runtime-node_modules-v6-
path: "**/node_modules"
key: runtime-node_modules-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
if: steps.node_modules.outputs.cache-hit != 'true'
- name: Restore archived build
uses: actions/download-artifact@v4
with:
pattern: _build_*
path: build
merge-multiple: true
- run: ./scripts/ci/pack_and_store_devtools_artifacts.sh ${{ matrix.browser }}
- run: ./scripts/ci/pack_and_store_devtools_artifacts.sh
env:
RELEASE_CHANNEL: experimental
- name: Display structure of build
run: ls -R build
# Simplifies getting the extension for local testing
- name: Archive ${{ matrix.browser }} extension
- name: Archive devtools build
uses: actions/upload-artifact@v4
with:
name: react-devtools-${{ matrix.browser }}-extension
path: build/devtools/${{ matrix.browser }}-extension.zip
if-no-files-found: error
- name: Archive ${{ matrix.browser }} metadata
uses: actions/upload-artifact@v4
with:
name: react-devtools-${{ matrix.browser }}-metadata
path: build/devtools/webpack-stats.*.json
merge_devtools_artifacts:
name: Merge DevTools artifacts
needs: build_devtools_and_process_artifacts
runs-on: ubuntu-latest
steps:
- name: Merge artifacts
uses: actions/upload-artifact/merge@v4
with:
name: react-devtools
pattern: 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_and_lint, runtime_node_modules_cache]
needs: build_devtools_and_process_artifacts
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }}
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: yarn
cache-dependency-path: yarn.lock
- name: Restore cached node_modules
uses: actions/cache/restore@v4
uses: actions/cache@v4
id: node_modules
with:
path: |
**/node_modules
key: runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
restore-keys: |
runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-
runtime-node_modules-v6-
path: "**/node_modules"
key: runtime-node_modules-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
if: steps.node_modules.outputs.cache-hit != 'true'
- name: Restore archived build
uses: actions/download-artifact@v4
with:
pattern: _build_*
path: build
merge-multiple: true
- name: Check Playwright version
id: playwright_version
run: echo "playwright_version=$(npm ls @playwright/test | grep @playwright | sed 's/.*@//' | head -1)" >> "$GITHUB_OUTPUT"
- name: Cache Playwright Browsers for version ${{ steps.playwright_version.outputs.playwright_version }}
id: cache_playwright_browsers
uses: actions/cache@v4
with:
path: ~/.cache/ms-playwright
key: playwright-browsers-v6-${{ runner.arch }}-${{ runner.os }}-${{ steps.playwright_version.outputs.playwright_version }}
- name: Playwright install deps
if: steps.cache_playwright_browsers.outputs.cache-hit != 'true'
run: npx playwright install --with-deps chromium
- run: |
npx playwright install
sudo npx playwright install-deps
- run: ./scripts/ci/run_devtools_e2e_tests.js
env:
RELEASE_CHANNEL: experimental
- name: Archive Playwright report
uses: actions/upload-artifact@v4
with:
name: devtools-playwright-artifacts
path: tmp/playwright-artifacts
if-no-files-found: warn
# ----- SIZEBOT -----
sizebot:
if: ${{ github.event_name == 'pull_request' && github.ref_name != 'main' && github.event.pull_request.base.ref == 'main' }}
name: Run sizebot
needs: [build_and_lint]
permissions:
# We use github.token to download the build artifact from a previous runtime_build_and_test.yml run
actions: read
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
with:
ref: ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }}
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: yarn
cache-dependency-path: yarn.lock
- name: Restore cached node_modules
uses: actions/cache@v4 # note: this does not reuse centralized cache since it has unique cache key
uses: actions/cache@v4
id: node_modules
with:
path: |
**/node_modules
key: runtime-release-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'scripts/release/yarn.lock') }}
path: "**/node_modules"
key: ${{ runner.arch }}-${{ runner.os }}-modules-${{ hashFiles('yarn.lock', 'scripts/release/yarn.lock') }}
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
if: steps.node_modules.outputs.cache-hit != 'true'
- run: yarn --cwd scripts/release install --frozen-lockfile
if: steps.node_modules.outputs.cache-hit != 'true'
- run: yarn install --frozen-lockfile
working-directory: scripts/release
- name: Download artifacts for base revision
# The build could have been generated from a fork, so we must download the build without
# any verification. This is safe since we only use this for sizebot calculation and the
# unverified artifact is not used. Additionally this workflow runs in the pull_request
# trigger so only restricted permissions are available.
run: |
GH_TOKEN=${{ github.token }} scripts/release/download-experimental-build.js --commit=$(git rev-parse ${{ github.event.pull_request.base.sha }}) ${{ (github.event.pull_request.head.repo.full_name != github.repository && '--noVerify') || ''}}
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
# 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.
run: rm -rf ./base-build/node_modules
- name: Display structure of base-build from origin/main
- 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
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: yarn
cache-dependency-path: yarn.lock
- name: Restore cached node_modules
uses: actions/cache@v4
id: node_modules
with:
path: "**/node_modules"
key: runtime-node_modules-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
- name: Restore archived build for PR
uses: actions/download-artifact@v4
with:
@@ -899,11 +661,17 @@ jobs:
node ./scripts/print-warnings/print-warnings.js > build/__test_utils__/ReactAllWarnings.js
- name: Display structure of build for PR
run: ls -R build
- run: echo ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }} >> build/COMMIT_SHA
- 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
if-no-files-found: ignore

View File

@@ -16,13 +16,6 @@ on:
required: true
default: false
type: boolean
dry_run:
description: Perform a dry run (run everything except push)
required: true
default: false
type: boolean
permissions: {}
env:
TZ: /usr/share/zoneinfo/America/Los_Angeles
@@ -32,40 +25,6 @@ env:
jobs:
download_artifacts:
runs-on: ubuntu-latest
permissions:
# We use github.token to download the build artifact from a previous runtime_build_and_test.yml run
actions: read
steps:
- uses: actions/checkout@v4
- name: Restore cached node_modules
uses: actions/cache@v4
id: node_modules
with:
path: |
**/node_modules
key: runtime-release-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'scripts/release/yarn.lock') }}
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
if: steps.node_modules.outputs.cache-hit != 'true'
- run: yarn --cwd scripts/release install --frozen-lockfile
if: steps.node_modules.outputs.cache-hit != 'true'
- name: Download artifacts for base revision
run: |
GH_TOKEN=${{ github.token }} scripts/release/download-experimental-build.js --commit=${{ inputs.commit_sha || github.event.workflow_run.head_sha || github.sha }}
- name: Display structure of build
run: ls -R build
- name: Archive build
uses: actions/upload-artifact@v4
with:
name: build
path: build/
if-no-files-found: error
process_artifacts:
runs-on: ubuntu-latest
needs: [download_artifacts]
outputs:
www_branch_count: ${{ steps.check_branches.outputs.www_branch_count }}
fbsource_branch_count: ${{ steps.check_branches.outputs.fbsource_branch_count }}
@@ -105,11 +64,27 @@ 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"
- name: Restore downloaded build
uses: actions/download-artifact@v4
- uses: actions/setup-node@v4
with:
name: build
path: build
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: runtime-release-node_modules-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'scripts/release/yarn.lock') }}
- name: Ensure clean build directory
run: rm -rf build
- 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 || github.sha }}
- name: Display structure of build
run: ls -R build
- name: Strip @license from eslint plugin and react-refresh
@@ -132,10 +107,9 @@ jobs:
mkdir ./compiled/facebook-www/__test_utils__
mv build/__test_utils__/ReactAllWarnings.js ./compiled/facebook-www/__test_utils__/ReactAllWarnings.js
# Copy eslint-plugin-react-hooks
mkdir ./compiled/eslint-plugin-react-hooks
cp build/oss-experimental/eslint-plugin-react-hooks/cjs/eslint-plugin-react-hooks.development.js \
./compiled/eslint-plugin-react-hooks/index.js
# Move eslint-plugin-react-hooks into facebook-www
mv build/oss-experimental/eslint-plugin-react-hooks/cjs/eslint-plugin-react-hooks.development.js \
./compiled/facebook-www/eslint-plugin-react-hooks.js
# Move unstable_server-external-runtime.js into facebook-www
mv build/oss-experimental/react-dom/unstable_server-external-runtime.js \
@@ -167,21 +141,15 @@ jobs:
rm $RENDERER_FOLDER/ReactFabric-{dev,prod,profiling}.js
rm $RENDERER_FOLDER/ReactNativeRenderer-{dev,prod,profiling}.js
# Copy eslint-plugin-react-hooks
# NOTE: This is different from www, here we include the full package
# including package.json to include dependencies in fbsource.
mkdir "$BASE_FOLDER/tools"
cp -r build/oss-experimental/eslint-plugin-react-hooks "$BASE_FOLDER/tools"
# Move React Native version file
mv build/facebook-react-native/VERSION_NATIVE_FB ./compiled-rn/VERSION_NATIVE_FB
ls -R ./compiled-rn
- name: Add REVISION files
run: |
echo ${{ inputs.commit_sha || github.event.workflow_run.head_sha || github.sha }} >> ./compiled/facebook-www/REVISION
echo ${{ github.sha }} >> ./compiled/facebook-www/REVISION
cp ./compiled/facebook-www/REVISION ./compiled/facebook-www/REVISION_TRANSFORMS
echo ${{ inputs.commit_sha || github.event.workflow_run.head_sha || github.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: |
@@ -198,20 +166,15 @@ jobs:
with:
name: compiled
path: compiled/
if-no-files-found: error
- uses: actions/upload-artifact@v4
with:
name: compiled-rn
path: compiled-rn/
if-no-files-found: error
commit_www_artifacts:
needs: [download_artifacts, process_artifacts]
if: inputs.force == true || (github.ref == 'refs/heads/main' && needs.process_artifacts.outputs.www_branch_count == '0')
needs: download_artifacts
if: inputs.force == true || (github.ref == 'refs/heads/main' && needs.download_artifacts.outputs.www_branch_count == '0')
runs-on: ubuntu-latest
permissions:
# Used to push a commit to builds/facebook-www
contents: write
steps:
- uses: actions/checkout@v4
with:
@@ -223,12 +186,12 @@ jobs:
name: compiled
path: compiled/
- name: Revert version changes
if: needs.process_artifacts.outputs.last_version_classic != '' && needs.process_artifacts.outputs.last_version_modern != ''
if: needs.download_artifacts.outputs.last_version_classic != '' && needs.download_artifacts.outputs.last_version_modern != ''
env:
CURRENT_VERSION_CLASSIC: ${{ needs.process_artifacts.outputs.current_version_classic }}
CURRENT_VERSION_MODERN: ${{ needs.process_artifacts.outputs.current_version_modern }}
LAST_VERSION_CLASSIC: ${{ needs.process_artifacts.outputs.last_version_classic }}
LAST_VERSION_MODERN: ${{ needs.process_artifacts.outputs.last_version_modern }}
CURRENT_VERSION_CLASSIC: ${{ needs.download_artifacts.outputs.current_version_classic }}
CURRENT_VERSION_MODERN: ${{ needs.download_artifacts.outputs.current_version_modern }}
LAST_VERSION_CLASSIC: ${{ needs.download_artifacts.outputs.last_version_classic }}
LAST_VERSION_MODERN: ${{ needs.download_artifacts.outputs.last_version_modern }}
run: |
echo "Reverting $CURRENT_VERSION_CLASSIC to $LAST_VERSION_CLASSIC"
grep -rl "$CURRENT_VERSION_CLASSIC" ./compiled || echo "No files found with $CURRENT_VERSION_CLASSIC"
@@ -258,12 +221,12 @@ jobs:
echo "should_commit=false" >> "$GITHUB_OUTPUT"
fi
- name: Re-apply version changes
if: inputs.force == true || (steps.check_should_commit.outputs.should_commit == 'true' && needs.process_artifacts.outputs.last_version_classic != '' && needs.process_artifacts.outputs.last_version_modern != '')
if: inputs.force == true || (steps.check_should_commit.outputs.should_commit == 'true' && needs.download_artifacts.outputs.last_version_classic != '' && needs.download_artifacts.outputs.last_version_modern != '')
env:
CURRENT_VERSION_CLASSIC: ${{ needs.process_artifacts.outputs.current_version_classic }}
CURRENT_VERSION_MODERN: ${{ needs.process_artifacts.outputs.current_version_modern }}
LAST_VERSION_CLASSIC: ${{ needs.process_artifacts.outputs.last_version_classic }}
LAST_VERSION_MODERN: ${{ needs.process_artifacts.outputs.last_version_modern }}
CURRENT_VERSION_CLASSIC: ${{ needs.download_artifacts.outputs.current_version_classic }}
CURRENT_VERSION_MODERN: ${{ needs.download_artifacts.outputs.current_version_modern }}
LAST_VERSION_CLASSIC: ${{ needs.download_artifacts.outputs.last_version_classic }}
LAST_VERSION_MODERN: ${{ needs.download_artifacts.outputs.last_version_modern }}
run: |
echo "Re-applying $LAST_VERSION_CLASSIC to $CURRENT_VERSION_CLASSIC"
grep -rl "$LAST_VERSION_CLASSIC" ./compiled || echo "No files found with $LAST_VERSION_CLASSIC"
@@ -277,31 +240,24 @@ jobs:
- name: Will commit these changes
if: inputs.force == true || steps.check_should_commit.outputs.should_commit == 'true'
run: |
git add .
git status
- name: Check commit message
if: inputs.dry_run
run: |
git fetch origin --quiet
git show ${{ inputs.commit_sha || github.event.workflow_run.head_sha || github.sha }} --no-patch --pretty=format:"%B"
echo ":"
git status -u
- name: Commit changes to branch
if: inputs.force == true || steps.check_should_commit.outputs.should_commit == 'true'
run: |
git config --global user.email "${{ format('{0}@users.noreply.github.com', github.triggering_actor) }}"
git config --global user.name "${{ github.triggering_actor }}"
uses: stefanzweifel/git-auto-commit-action@v5
with:
commit_message: |
${{ github.event.workflow_run.head_commit.message || format('Manual build of {0}', github.event.workflow_run.head_sha || github.sha) }}
git fetch origin --quiet
git commit -m "$(git show ${{ inputs.commit_sha || github.event.workflow_run.head_sha || github.sha }} --no-patch --pretty=format:'%B%n%nDiffTrain build for [${{ inputs.commit_sha || github.event.workflow_run.head_sha || github.sha }}](https://github.com/facebook/react/commit/${{ inputs.commit_sha || github.event.workflow_run.head_sha || github.sha}})')" || echo "No changes to commit"
- name: Push changes to branch
if: inputs.dry_run == false && (inputs.force == true || steps.check_should_commit.outputs.should_commit == 'true')
run: git push
DiffTrain build for [${{ github.event.workflow_run.head_sha || github.sha }}](https://github.com/facebook/react/commit/${{ github.event.workflow_run.head_sha || github.sha }})
branch: builds/facebook-www
commit_user_name: ${{ github.triggering_actor }}
commit_user_email: ${{ format('{0}@users.noreply.github.com', github.triggering_actor) }}
create_branch: true
commit_fbsource_artifacts:
needs: [download_artifacts, process_artifacts]
permissions:
# Used to push a commit to builds/facebook-fbsource
contents: write
if: inputs.force == true || (github.ref == 'refs/heads/main' && needs.process_artifacts.outputs.fbsource_branch_count == '0')
needs: download_artifacts
if: inputs.force == true || (github.ref == 'refs/heads/main' && needs.download_artifacts.outputs.fbsource_branch_count == '0')
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
@@ -314,10 +270,10 @@ jobs:
name: compiled-rn
path: compiled-rn/
- name: Revert version changes
if: needs.process_artifacts.outputs.last_version_rn != ''
if: needs.download_artifacts.outputs.last_version_rn != ''
env:
CURRENT_VERSION: ${{ needs.process_artifacts.outputs.current_version_rn }}
LAST_VERSION: ${{ needs.process_artifacts.outputs.last_version_rn }}
CURRENT_VERSION: ${{ needs.download_artifacts.outputs.current_version_rn }}
LAST_VERSION: ${{ needs.download_artifacts.outputs.last_version_rn }}
run: |
echo "Reverting $CURRENT_VERSION to $LAST_VERSION"
grep -rl "$CURRENT_VERSION" ./compiled-rn || echo "No files found with $CURRENT_VERSION"
@@ -332,10 +288,10 @@ jobs:
git --no-pager diff -U0 --cached | grep '^[+-]' | head -n 100
echo "===================="
# Ignore REVISION or lines removing @generated headers.
if git diff --cached ':(exclude)*REVISION' ':(exclude)*/eslint-plugin-react-hooks/package.json' | grep -vE "^(@@|diff|index|\-\-\-|\+\+\+|\- \* @generated SignedSource)" | grep "^[+-]" > /dev/null; then
if git diff --cached ':(exclude)*REVISION' | grep -vE "^(@@|diff|index|\-\-\-|\+\+\+|\- \* @generated SignedSource)" | grep "^[+-]" > /dev/null; then
echo "Changes detected"
echo "===== Changes ====="
git --no-pager diff --cached ':(exclude)*REVISION' ':(exclude)*/eslint-plugin-react-hooks/package.json' | grep -vE "^(@@|diff|index|\-\-\-|\+\+\+|\- \* @generated SignedSource)" | grep "^[+-]" | head -n 50
git --no-pager diff --cached ':(exclude)*REVISION' | grep -vE "^(@@|diff|index|\-\-\-|\+\+\+|\- \* @generated SignedSource)" | grep "^[+-]" | head -n 50
echo "==================="
echo "should_commit=true" >> "$GITHUB_OUTPUT"
else
@@ -343,10 +299,10 @@ jobs:
echo "should_commit=false" >> "$GITHUB_OUTPUT"
fi
- name: Re-apply version changes
if: inputs.force == true || (steps.check_should_commit.outputs.should_commit == 'true' && needs.process_artifacts.outputs.last_version_rn != '')
if: inputs.force == true || (steps.check_should_commit.outputs.should_commit == 'true' && needs.download_artifacts.outputs.last_version_rn != '')
env:
CURRENT_VERSION: ${{ needs.process_artifacts.outputs.current_version_rn }}
LAST_VERSION: ${{ needs.process_artifacts.outputs.last_version_rn }}
CURRENT_VERSION: ${{ needs.download_artifacts.outputs.current_version_rn }}
LAST_VERSION: ${{ needs.download_artifacts.outputs.last_version_rn }}
run: |
echo "Re-applying $LAST_VERSION to $CURRENT_VERSION"
grep -rl "$LAST_VERSION" ./compiled-rn || echo "No files found with $LAST_VERSION"
@@ -453,19 +409,15 @@ jobs:
run: |
git add .
git status
- name: Check commit message
if: inputs.dry_run
run: |
git fetch origin --quiet
git show ${{ inputs.commit_sha || github.event.workflow_run.head_sha || github.sha }} --no-patch --pretty=format:"%B"
- name: Commit changes to branch
if: inputs.force == true || steps.check_should_commit.outputs.should_commit == 'true'
run: |
git config --global user.email "${{ format('{0}@users.noreply.github.com', github.triggering_actor) }}"
git config --global user.name "${{ github.triggering_actor }}"
uses: stefanzweifel/git-auto-commit-action@v5
with:
commit_message: |
${{ github.event.workflow_run.head_commit.message || format('Manual build of {0}', github.event.workflow_run.head_sha || github.sha) }}
git fetch origin --quiet
git commit -m "$(git show ${{ inputs.commit_sha || github.event.workflow_run.head_sha || github.sha }} --no-patch --pretty=format:'%B%n%nDiffTrain build for [${{ inputs.commit_sha || github.event.workflow_run.head_sha || github.sha }}](https://github.com/facebook/react/commit/${{ inputs.commit_sha || github.event.workflow_run.head_sha || github.sha}})')" || echo "No changes to commit"
- name: Push changes to branch
if: inputs.dry_run == false && (inputs.force == true || steps.check_should_commit.outputs.should_commit == 'true')
run: git push
DiffTrain build for [${{ github.event.workflow_run.head_sha || github.sha }}](https://github.com/facebook/react/commit/${{ github.event.workflow_run.head_sha || github.sha }})
branch: builds/facebook-fbsource
commit_user_name: ${{ github.triggering_actor }}
commit_user_email: ${{ format('{0}@users.noreply.github.com', github.triggering_actor) }}
create_branch: true

View File

@@ -1,51 +0,0 @@
name: (Runtime) Discord Notify
on:
pull_request_target:
types: [opened, ready_for_review]
paths-ignore:
- packages/react-devtools**
- compiler/**
- .github/workflows/compiler_**.yml
- .github/workflows/devtools**.yml
permissions: {}
jobs:
check_access:
if: ${{ github.event.pull_request.draft == false }}
runs-on: ubuntu-latest
outputs:
is_member_or_collaborator: ${{ steps.check_is_member_or_collaborator.outputs.is_member_or_collaborator }}
steps:
- run: echo ${{ github.event.pull_request.author_association }}
- name: Check is member or collaborator
id: check_is_member_or_collaborator
if: ${{ github.event.pull_request.author_association == 'MEMBER' || github.event.pull_request.author_association == 'COLLABORATOR' }}
run: echo "is_member_or_collaborator=true" >> "$GITHUB_OUTPUT"
check_maintainer:
if: ${{ needs.check_access.outputs.is_member_or_collaborator == 'true' || needs.check_access.outputs.is_member_or_collaborator == true }}
needs: [check_access]
uses: facebook/react/.github/workflows/shared_check_maintainer.yml@main
permissions:
# Used by check_maintainer
contents: read
with:
actor: ${{ github.event.pull_request.user.login }}
notify:
if: ${{ needs.check_maintainer.outputs.is_core_team == 'true' }}
needs: check_maintainer
runs-on: ubuntu-latest
steps:
- name: Discord Webhook Action
uses: tsickert/discord-webhook@86dc739f3f165f16dadc5666051c367efa1692f4
with:
webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL }}
embed-author-name: ${{ github.event.pull_request.user.login }}
embed-author-url: ${{ github.event.pull_request.user.html_url }}
embed-author-icon-url: ${{ github.event.pull_request.user.avatar_url }}
embed-title: '#${{ github.event.number }} (+${{github.event.pull_request.additions}} -${{github.event.pull_request.deletions}}): ${{ github.event.pull_request.title }}'
embed-description: ${{ github.event.pull_request.body }}
embed-url: ${{ github.event.pull_request.html_url }}

View File

@@ -1,65 +0,0 @@
name: (Runtime) ESLint Plugin E2E
on:
push:
branches: [main]
pull_request:
paths-ignore:
- compiler/**
permissions: {}
concurrency:
group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.event.pull_request.number || github.run_id }}
cancel-in-progress: true
env:
TZ: /usr/share/zoneinfo/America/Los_Angeles
jobs:
# ----- TESTS -----
test:
name: ESLint v${{ matrix.eslint_major }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
eslint_major:
- "6"
- "7"
- "8"
- "9"
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: yarn
cache-dependency-path: |
yarn.lock
compiler/yarn.lock
- name: Restore cached node_modules
uses: actions/cache@v4
id: node_modules
with:
path: |
**/node_modules
key: runtime-and-compiler-eslint_e2e-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock', 'fixtures/eslint-v*/yarn.lock') }}
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
if: steps.node_modules.outputs.cache-hit != 'true'
- run: yarn --cwd compiler install --frozen-lockfile
if: steps.node_modules.outputs.cache-hit != 'true'
- name: Install fixture dependencies
working-directory: ./fixtures/eslint-v${{ matrix.eslint_major }}
run: yarn --frozen-lockfile
if: steps.node_modules.outputs.cache-hit != 'true'
- name: Build plugin
working-directory: fixtures/eslint-v${{ matrix.eslint_major }}
run: node build.mjs
- name: Run lint test
working-directory: ./fixtures/eslint-v${{ matrix.eslint_major }}
run: yarn lint

View File

@@ -8,8 +8,6 @@ on:
- main
workflow_dispatch:
permissions: {}
env:
TZ: /usr/share/zoneinfo/America/Los_Angeles

View File

@@ -13,45 +13,21 @@ on:
dist_tag:
required: true
type: string
enableFailureNotification:
description: 'Whether to notify the team on Discord when the release fails. Useful if this workflow is called from an automation.'
required: false
type: boolean
only_packages:
description: Packages to publish (space separated)
type: string
skip_packages:
description: Packages to NOT publish (space separated)
type: string
dry:
required: true
description: Dry run instead of publish?
type: boolean
default: true
secrets:
DISCORD_WEBHOOK_URL:
description: 'Discord webhook URL to notify on failure. Only required if enableFailureNotification is true.'
required: false
GH_TOKEN:
required: true
NPM_TOKEN:
required: true
permissions: {}
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
permissions:
# We use github.token to download the build artifact from a previous runtime_build_and_test.yml run
actions: read
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
@@ -63,48 +39,14 @@ jobs:
uses: actions/cache@v4
id: node_modules
with:
path: |
**/node_modules
key: runtime-release-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'scripts/release/yarn.lock') }}
path: "**/node_modules"
key: runtime-release-node_modules-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'scripts/release/yarn.lock') }}
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
if: steps.node_modules.outputs.cache-hit != 'true'
- run: yarn --cwd scripts/release install --frozen-lockfile
if: steps.node_modules.outputs.cache-hit != 'true'
- run: cp ./scripts/release/ci-npmrc ~/.npmrc
- run: yarn install --frozen-lockfile
working-directory: scripts/release
- run: |
GH_TOKEN=${{ secrets.GH_TOKEN }} scripts/release/prepare-release-from-ci.js --skipTests -r ${{ inputs.release_channel }} --commit=${{ inputs.commit_sha }}
- name: Check prepared files
run: ls -R build/node_modules
- if: '${{ inputs.only_packages }}'
name: 'Publish ${{ inputs.only_packages }}'
run: |
scripts/release/publish.js \
--ci \
--tags=${{ inputs.dist_tag }} \
--onlyPackages=${{ inputs.only_packages }} ${{ (inputs.dry && '') || '\'}}
${{ inputs.dry && '--dry' || '' }}
- if: '${{ inputs.skip_packages }}'
name: 'Publish all packages EXCEPT ${{ inputs.skip_packages }}'
run: |
scripts/release/publish.js \
--ci \
--tags=${{ inputs.dist_tag }} \
--skipPackages=${{ inputs.skip_packages }} ${{ (inputs.dry && '') || '\'}}
${{ inputs.dry && '--dry' || '' }}
- if: '${{ !inputs.skip_packages && !inputs.only_packages }}'
name: 'Publish all packages'
run: |
scripts/release/publish.js \
--ci \
--tags=${{ inputs.dist_tag }} ${{ (inputs.dry && '') || '\'}}
${{ inputs.dry && '--dry' || '' }}
- name: Notify Discord on failure
if: failure() && inputs.enableFailureNotification == true
uses: tsickert/discord-webhook@86dc739f3f165f16dadc5666051c367efa1692f4
with:
webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL }}
embed-author-name: "GitHub Actions"
embed-title: '[Runtime] Publish of ${{ inputs.release_channel }}@${{ inputs.dist_tag}} release failed'
embed-url: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}/attempts/${{ github.run_attempt }}
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

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

View File

@@ -5,8 +5,6 @@ on:
# At 10 minutes past 16:00 on Mon, Tue, Wed, Thu, and Fri
- cron: 10 16 * * 1,2,3,4,5
permissions: {}
env:
TZ: /usr/share/zoneinfo/America/Los_Angeles
@@ -14,26 +12,16 @@ jobs:
publish_prerelease_canary:
name: Publish to Canary channel
uses: facebook/react/.github/workflows/runtime_prereleases.yml@main
permissions:
# We use github.token to download the build artifact from a previous runtime_build_and_test.yml run
actions: read
with:
commit_sha: ${{ github.sha }}
release_channel: stable
dist_tag: canary,next
enableFailureNotification: true
dry: false
secrets:
DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
publish_prerelease_experimental:
name: Publish to Experimental channel
uses: facebook/react/.github/workflows/runtime_prereleases.yml@main
permissions:
# We use github.token to download the build artifact from a previous runtime_build_and_test.yml run
actions: read
# 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
@@ -43,9 +31,5 @@ jobs:
commit_sha: ${{ github.sha }}
release_channel: experimental
dist_tag: experimental
enableFailureNotification: true
dry: false
secrets:
DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -1,128 +0,0 @@
name: (Runtime) Publish Releases from NPM Manual
on:
workflow_dispatch:
inputs:
version_to_promote:
required: true
description: Current npm version (non-experimental) to promote
type: string
version_to_publish:
required: true
description: Version to publish for the specified packages
type: string
only_packages:
description: Packages to publish (space separated)
type: string
skip_packages:
description: Packages to NOT publish (space separated)
type: string
tags:
description: NPM tags (space separated)
type: string
default: untagged
dry:
required: true
description: Dry run instead of publish?
type: boolean
default: true
force_notify:
description: Force a Discord notification?
type: boolean
default: false
permissions: {}
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
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
jobs:
notify:
if: ${{ inputs.force_notify || inputs.dry == false || inputs.dry == 'false' }}
runs-on: ubuntu-latest
steps:
- name: Discord Webhook Action
uses: tsickert/discord-webhook@86dc739f3f165f16dadc5666051c367efa1692f4
with:
webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL }}
embed-author-name: ${{ github.event.sender.login }}
embed-author-url: ${{ github.event.sender.html_url }}
embed-author-icon-url: ${{ github.event.sender.avatar_url }}
embed-title: "⚠️ Publishing release from NPM${{ (inputs.dry && ' (dry run)') || '' }}"
embed-description: |
```json
${{ toJson(inputs) }}
```
embed-url: https://github.com/facebook/react/actions/runs/${{ github.run_id }}
publish:
name: Publish releases
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: runtime-release-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'scripts/release/yarn.lock') }}
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
if: steps.node_modules.outputs.cache-hit != 'true'
- run: yarn --cwd scripts/release install --frozen-lockfile
if: steps.node_modules.outputs.cache-hit != 'true'
- run: cp ./scripts/release/ci-npmrc ~/.npmrc
- if: '${{ inputs.only_packages }}'
name: 'Prepare ${{ inputs.only_packages }} from NPM'
run: |
scripts/release/prepare-release-from-npm.js \
--ci \
--skipTests \
--version=${{ inputs.version_to_promote }} \
--publishVersion=${{ inputs.version_to_publish }} \
--onlyPackages=${{ inputs.only_packages }}
- if: '${{ inputs.skip_packages }}'
name: 'Prepare all packages EXCEPT ${{ inputs.skip_packages }} from NPM'
run: |
scripts/release/prepare-release-from-npm.js \
--ci \
--skipTests \
--version=${{ inputs.version_to_promote }} \
--publishVersion=${{ inputs.version_to_publish }} \
--skipPackages=${{ inputs.skip_packages }}
- name: Check prepared files
run: ls -R build/node_modules
- if: '${{ inputs.only_packages }}'
name: 'Publish ${{ inputs.only_packages }}'
run: |
scripts/release/publish.js \
--ci \
--tags=${{ inputs.tags }} \
--publishVersion=${{ inputs.version_to_publish }} \
--onlyPackages=${{ inputs.only_packages }} ${{ (inputs.dry && '') || '\'}}
${{ inputs.dry && '--dry' || '' }}
- if: '${{ inputs.skip_packages }}'
name: 'Publish all packages EXCEPT ${{ inputs.skip_packages }}'
run: |
scripts/release/publish.js \
--ci \
--tags=${{ inputs.tags }} \
--publishVersion=${{ inputs.version_to_publish }} \
--skipPackages=${{ inputs.skip_packages }} ${{ (inputs.dry && '') || '\'}}
${{ inputs.dry && '--dry' || '' }}
- name: Archive released package for debugging
uses: actions/upload-artifact@v4
with:
name: build
path: |
./build/node_modules

View File

@@ -1,58 +0,0 @@
name: (Shared) Check maintainer
on:
workflow_call:
inputs:
actor:
required: true
type: string
outputs:
is_core_team:
value: ${{ jobs.check_maintainer.outputs.is_core_team }}
permissions: {}
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:
check_maintainer:
runs-on: ubuntu-latest
permissions:
# We fetch the contents of the MAINTAINERS file
contents: read
outputs:
is_core_team: ${{ steps.check_if_actor_is_maintainer.outputs.result }}
steps:
- name: Check if actor is maintainer
id: check_if_actor_is_maintainer
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const actor = '${{ inputs.actor }}';
const res = await github.rest.repos.getContent({
owner: 'facebook',
repo: 'react',
path: 'MAINTAINERS',
ref: 'main',
headers: { Accept: 'application/vnd.github+json' }
});
if (res.status !== 200) {
console.error(res);
throw new Error('Unable to fetch MAINTAINERS file');
}
content = Buffer.from(res.data.content, 'base64').toString();
if (content == null || typeof content !== 'string') {
throw new Error('Unable to retrieve MAINTAINERS file');
}
const maintainers = new Set(content.split('\n'));
if (maintainers.has(actor)) {
console.log(`🟢 ${actor} is a maintainer`);
return true;
}
console.log(`🔴 ${actor} is NOT a maintainer`);
return null;

View File

@@ -1,41 +0,0 @@
# https://github.com/actions/cache/blob/main/tips-and-workarounds.md#force-deletion-of-caches-overriding-default-cache-eviction-policy
name: (Shared) Cleanup Merged Branch Caches
on:
pull_request:
types:
- closed
workflow_dispatch:
inputs:
pr_number:
required: true
type: string
permissions: {}
jobs:
cleanup:
runs-on: ubuntu-latest
permissions:
# `actions:write` permission is required to delete caches
# See also: https://docs.github.com/en/rest/actions/cache?apiVersion=2022-11-28#delete-a-github-actions-cache-for-a-repository-using-a-cache-id
actions: write
contents: read
steps:
- name: Cleanup
run: |
echo "Fetching list of cache key"
cacheKeysForPR=$(gh cache list --ref $BRANCH --limit 100 --json id --jq '.[].id')
## Setting this to not fail the workflow while deleting cache keys.
set +e
for cacheKey in $cacheKeysForPR
do
gh cache delete $cacheKey
echo "Deleting $cacheKey"
done
echo "Done"
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_REPO: ${{ github.repository }}
BRANCH: refs/pull/${{ inputs.pr_number || github.event.pull_request.number }}/merge

View File

@@ -1,36 +0,0 @@
# https://github.com/actions/cache/blob/main/tips-and-workarounds.md#force-deletion-of-caches-overriding-default-cache-eviction-policy
name: (Shared) Cleanup Stale Branch Caches
on:
schedule:
# Every 6 hours
- cron: 0 */6 * * *
workflow_dispatch:
permissions: {}
jobs:
cleanup:
runs-on: ubuntu-latest
permissions:
# `actions:write` permission is required to delete caches
# See also: https://docs.github.com/en/rest/actions/cache?apiVersion=2022-11-28#delete-a-github-actions-cache-for-a-repository-using-a-cache-id
actions: write
contents: read
steps:
- name: Cleanup
run: |
echo "Fetching list of cache keys"
cacheKeysForPR=$(gh cache list --limit 100 --json id,ref --jq '.[] | select(.ref != "refs/heads/main") | .id')
## Setting this to not fail the workflow while deleting cache keys.
set +e
for cacheKey in $cacheKeysForPR
do
gh cache delete $cacheKey
echo "Deleting $cacheKey"
done
echo "Done"
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_REPO: ${{ github.repository }}

View File

@@ -1,43 +0,0 @@
name: (Shared) Close Direct Sync Branch PRs
on:
pull_request:
branches:
- 'builds/facebook-**'
permissions: {}
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:
close_pr:
runs-on: ubuntu-latest
permissions:
# Used to create a review and close PRs
pull-requests: write
contents: write
steps:
- name: Close PR
uses: actions/github-script@v7
with:
script: |
const owner = context.repo.owner;
const repo = context.repo.repo;
const pullNumber = ${{ github.event.number }};
await github.rest.pulls.createReview({
owner,
repo,
pull_number: pullNumber,
body: 'Do not land changes to `${{ github.event.pull_request.base.ref }}`. Please re-open your PR targeting `main` instead.',
event: 'REQUEST_CHANGES'
});
await github.rest.pulls.update({
owner,
repo,
pull_number: pullNumber,
state: 'closed'
});

View File

@@ -0,0 +1,21 @@
name: (Shared) Discord Notify
on:
pull_request_target:
types: [labeled]
jobs:
notify:
if: ${{ github.event.label.name == 'React Core Team' }}
runs-on: ubuntu-latest
steps:
- name: Discord Webhook Action
uses: tsickert/discord-webhook@v6.0.0
with:
webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL }}
embed-author-name: ${{ github.event.pull_request.user.login }}
embed-author-url: ${{ github.event.pull_request.user.html_url }}
embed-author-icon-url: ${{ github.event.pull_request.user.avatar_url }}
embed-title: '#${{ github.event.number }} (+${{github.event.pull_request.additions}} -${{github.event.pull_request.deletions}}): ${{ github.event.pull_request.title }}'
embed-description: ${{ github.event.pull_request.body }}
embed-url: ${{ github.event.pull_request.html_url }}

View File

@@ -1,55 +0,0 @@
name: (Shared) Label Core Team PRs
on:
pull_request_target:
types: [opened]
permissions: {}
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:
check_access:
runs-on: ubuntu-latest
outputs:
is_member_or_collaborator: ${{ steps.check_is_member_or_collaborator.outputs.is_member_or_collaborator }}
steps:
- run: echo ${{ github.event.pull_request.author_association }}
- name: Check is member or collaborator
id: check_is_member_or_collaborator
if: ${{ github.event.pull_request.author_association == 'MEMBER' || github.event.pull_request.author_association == 'COLLABORATOR' }}
run: echo "is_member_or_collaborator=true" >> "$GITHUB_OUTPUT"
check_maintainer:
if: ${{ needs.check_access.outputs.is_member_or_collaborator == 'true' || needs.check_access.outputs.is_member_or_collaborator == true }}
needs: [check_access]
uses: facebook/react/.github/workflows/shared_check_maintainer.yml@main
permissions:
# Used by check_maintainer
contents: read
with:
actor: ${{ github.event.pull_request.user.login }}
label:
if: ${{ needs.check_maintainer.outputs.is_core_team == 'true' }}
runs-on: ubuntu-latest
needs: check_maintainer
permissions:
# Used to add labels on issues
issues: write
# Used to add labels on PRs
pull-requests: write
steps:
- name: Label PR as React Core Team
uses: actions/github-script@v7
with:
script: |
github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: ${{ github.event.number }},
labels: ['React Core Team']
});

View File

@@ -5,8 +5,6 @@ on:
branches: [main]
pull_request:
permissions: {}
concurrency:
group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.event.pull_request.number || github.run_id }}
cancel-in-progress: true
@@ -29,15 +27,12 @@ jobs:
cache-dependency-path: yarn.lock
- name: Restore cached node_modules
uses: actions/cache@v4
id: node_modules
with:
path: |
**/node_modules
key: shared-lint-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('**/yarn.lock') }}
path: "**/node_modules"
key: shared-lint-node_modules-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('**/yarn.lock') }}
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
if: steps.node_modules.outputs.cache-hit != 'true'
- run: yarn prettier-check
eslint:
@@ -52,15 +47,12 @@ jobs:
cache-dependency-path: yarn.lock
- name: Restore cached node_modules
uses: actions/cache@v4
id: node_modules
with:
path: |
**/node_modules
key: shared-lint-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('**/yarn.lock') }}
path: "**/node_modules"
key: shared-lint-node_modules-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('**/yarn.lock') }}
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
if: steps.node_modules.outputs.cache-hit != 'true'
- run: node ./scripts/tasks/eslint
check_license:
@@ -75,15 +67,12 @@ jobs:
cache-dependency-path: yarn.lock
- name: Restore cached node_modules
uses: actions/cache@v4
id: node_modules
with:
path: |
**/node_modules
key: shared-lint-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('**/yarn.lock') }}
path: "**/node_modules"
key: shared-lint-node_modules-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('**/yarn.lock') }}
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
if: steps.node_modules.outputs.cache-hit != 'true'
- run: ./scripts/ci/check_license.sh
test_print_warnings:
@@ -98,13 +87,10 @@ jobs:
cache-dependency-path: yarn.lock
- name: Restore cached node_modules
uses: actions/cache@v4
id: node_modules
with:
path: |
**/node_modules
key: shared-lint-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('**/yarn.lock') }}
path: "**/node_modules"
key: shared-lint-node_modules-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('**/yarn.lock') }}
- name: Ensure clean build directory
run: rm -rf build
- run: yarn install --frozen-lockfile
if: steps.node_modules.outputs.cache-hit != 'true'
- run: ./scripts/ci/test_print_warnings.sh

View File

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

2
.gitignore vendored
View File

@@ -23,7 +23,6 @@ chrome-user-data
.vscode
*.swp
*.swo
/tmp
packages/react-devtools-core/dist
packages/react-devtools-extensions/chrome/build
@@ -38,4 +37,3 @@ packages/react-devtools-fusebox/dist
packages/react-devtools-inline/dist
packages/react-devtools-shell/dist
packages/react-devtools-timeline/dist

2
.nvmrc
View File

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

View File

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

18
CHANGELOG-canary.md Normal file
View File

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

View File

@@ -1,128 +1,3 @@
## 19.2.0 (October 1st, 2025)
Below is a list of all new features, APIs, and bug fixes.
Read the [React 19.2 release post](https://react.dev/blog/2025/10/01/react-19-2) for more information.
### New React Features
- [`<Activity>`](https://react.dev/reference/react/Activity): A new API to hide and restore the UI and internal state of its children.
- [`useEffectEvent`](https://react.dev/reference/react/useEffectEvent) is a React Hook that lets you extract non-reactive logic into an [Effect Event](https://react.dev/learn/separating-events-from-effects#declaring-an-effect-event).
- [`cacheSignal`](https://react.dev/reference/react/cacheSignal) (for RSCs) lets your know when the `cache()` lifetime is over.
- [React Performance tracks](https://react.dev/reference/dev-tools/react-performance-tracks) appear on the Performance panels timeline in your browser developer tools
### New React DOM Features
- Added resume APIs for partial pre-rendering with Web Streams:
- [`resume`](https://react.dev/reference/react-dom/server/resume): to resume a prerender to a stream.
- [`resumeAndPrerender`](https://react.dev/reference/react-dom/static/resumeAndPrerender): to resume a prerender to HTML.
- Added resume APIs for partial pre-rendering with Node Streams:
- [`resumeToPipeableStream`](https://react.dev/reference/react-dom/server/resumeToPipeableStream): to resume a prerender to a stream.
- [`resumeAndPrerenderToNodeStream`](https://react.dev/reference/react-dom/static/resumeAndPrerenderToNodeStream): to resume a prerender to HTML.
- Updated [`prerender`](https://react.dev/reference/react-dom/static/prerender) APIs to return a `postponed` state that can be passed to the `resume` APIs.
### Notable changes
- React DOM now batches suspense boundary reveals, matching the behavior of client side rendering. This change is especially noticeable when animating the reveal of Suspense boundaries e.g. with the upcoming `<ViewTransition>` Component. React will batch as much reveals as possible before the first paint while trying to hit popular first-contentful paint metrics.
- Add Node Web Streams (`prerender`, `renderToReadableStream`) to server-side-rendering APIs for Node.js
- Use underscore instead of `:` IDs generated by useId
### All Changes
#### React
- `<Activity />` was developed over many years, starting before `ClassComponent.setState` (@acdlite @sebmarkbage and many others)
- Stringify context as "SomeContext" instead of "SomeContext.Provider" (@kassens [#33507](https://github.com/facebook/react/pull/33507))
- Include stack of cause of React instrumentation errors with `%o` placeholder (@eps1lon [#34198](https://github.com/facebook/react/pull/34198))
- Fix infinite `useDeferredValue` loop in popstate event (@acdlite [#32821](https://github.com/facebook/react/pull/32821))
- Fix a bug when an initial value was passed to `useDeferredValue` (@acdlite [#34376](https://github.com/facebook/react/pull/34376))
- Fix a crash when submitting forms with Client Actions (@sebmarkbage [#33055](https://github.com/facebook/react/pull/33055))
- Hide/unhide the content of dehydrated suspense boundaries if they resuspend (@sebmarkbage [#32900](https://github.com/facebook/react/pull/32900))
- Avoid stack overflow on wide trees during Hot Reload (@sophiebits [#34145](https://github.com/facebook/react/pull/34145))
- Improve Owner and Component stacks in various places (@sebmarkbage, @eps1lon: [#33629](https://github.com/facebook/react/pull/33629), [#33724](https://github.com/facebook/react/pull/33724), [#32735](https://github.com/facebook/react/pull/32735), [#33723](https://github.com/facebook/react/pull/33723))
- Add `cacheSignal` (@sebmarkbage [#33557](https://github.com/facebook/react/pull/33557))
#### React DOM
- Block on Suspensey Fonts during reveal of server-side-rendered content (@sebmarkbage [#33342](https://github.com/facebook/react/pull/33342))
- Use underscore instead of `:` for IDs generated by `useId` (@sebmarkbage, @eps1lon: [#32001](https://github.com/facebook/react/pull/32001), [https://github.com/facebook/react/pull/33342](https://github.com/facebook/react/pull/33342)[#33099](https://github.com/facebook/react/pull/33099), [#33422](https://github.com/facebook/react/pull/33422))
- Stop warning when ARIA 1.3 attributes are used (@Abdul-Omira [#34264](https://github.com/facebook/react/pull/34264))
- Allow `nonce` to be used on hoistable styles (@Andarist [#32461](https://github.com/facebook/react/pull/32461))
- Warn for using a React owned node as a Container if it also has text content (@sebmarkbage [#32774](https://github.com/facebook/react/pull/32774))
- s/HTML/text for for error messages if text hydration mismatches (@rickhanlonii [#32763](https://github.com/facebook/react/pull/32763))
- Fix a bug with `React.use` inside `React.lazy`\-ed Component (@hi-ogawa [#33941](https://github.com/facebook/react/pull/33941))
- Enable the `progressiveChunkSize` option for server-side-rendering APIs (@sebmarkbage [#33027](https://github.com/facebook/react/pull/33027))
- Fix a bug with deeply nested Suspense inside Suspense fallback when server-side-rendering (@gnoff [#33467](https://github.com/facebook/react/pull/33467))
- Avoid hanging when suspending after aborting while rendering (@gnoff [#34192](https://github.com/facebook/react/pull/34192))
- Add Node Web Streams to server-side-rendering APIs for Node.js (@sebmarkbage [#33475](https://github.com/facebook/react/pull/33475))
#### React Server Components
- Preload `<img>` and `<link>` using hints before they're rendered (@sebmarkbage [#34604](https://github.com/facebook/react/pull/34604))
- Log error if production elements are rendered during development (@eps1lon [#34189](https://github.com/facebook/react/pull/34189))
- Fix a bug when returning a Temporary reference (e.g. a Client Reference) from Server Functions (@sebmarkbage [#34084](https://github.com/facebook/react/pull/34084), @denk0403 [#33761](https://github.com/facebook/react/pull/33761))
- Pass line/column to `filterStackFrame` (@eps1lon [#33707](https://github.com/facebook/react/pull/33707))
- Support Async Modules in Turbopack Server References (@lubieowoce [#34531](https://github.com/facebook/react/pull/34531))
- Add support for .mjs file extension in Webpack (@jennyscript [#33028](https://github.com/facebook/react/pull/33028))
- Fix a wrong missing key warning (@unstubbable [#34350](https://github.com/facebook/react/pull/34350))
- Make console log resolve in predictable order (@sebmarkbage [#33665](https://github.com/facebook/react/pull/33665))
#### React Reconciler
- [createContainer](https://github.com/facebook/react/blob/v19.2.0/packages/react-reconciler/src/ReactFiberReconciler.js#L255-L261) and [createHydrationContainer](https://github.com/facebook/react/blob/v19.2.0/packages/react-reconciler/src/ReactFiberReconciler.js#L305-L312) had their parameter order adjusted after `on*` handlers to account for upcoming experimental APIs
## 19.1.1 (July 28, 2025)
### React
* Fixed Owner Stacks to work with ES2015 function.name semantics ([#33680](https://github.com/facebook/react/pull/33680) by @hoxyq)
## 19.1.0 (March 28, 2025)
### Owner Stack
An Owner Stack is a string representing the components that are directly responsible for rendering a particular component. You can log Owner Stacks when debugging or use Owner Stacks to enhance error overlays or other development tools. Owner Stacks are only available in development builds. Component Stacks in production are unchanged.
* An Owner Stack is a development-only stack trace that helps identify which components are responsible for rendering a particular component. An Owner Stack is distinct from a Component Stacks, which shows the hierarchy of components leading to an error.
* The [captureOwnerStack API](https://react.dev/reference/react/captureOwnerStack) is only available in development mode and returns a Owner Stack, if available. The API can be used to enhance error overlays or log component relationships when debugging. [#29923](https://github.com/facebook/react/pull/29923), [#32353](https://github.com/facebook/react/pull/32353), [#30306](https://github.com/facebook/react/pull/30306),
[#32538](https://github.com/facebook/react/pull/32538), [#32529](https://github.com/facebook/react/pull/32529), [#32538](https://github.com/facebook/react/pull/32538)
### React
* Enhanced support for Suspense boundaries to be used anywhere, including the client, server, and during hydration. [#32069](https://github.com/facebook/react/pull/32069), [#32163](https://github.com/facebook/react/pull/32163), [#32224](https://github.com/facebook/react/pull/32224), [#32252](https://github.com/facebook/react/pull/32252)
* Reduced unnecessary client rendering through improved hydration scheduling [#31751](https://github.com/facebook/react/pull/31751)
* Increased priority of client rendered Suspense boundaries [#31776](https://github.com/facebook/react/pull/31776)
* Fixed frozen fallback states by rendering unfinished Suspense boundaries on the client. [#31620](https://github.com/facebook/react/pull/31620)
* Reduced garbage collection pressure by improving Suspense boundary retries. [#31667](https://github.com/facebook/react/pull/31667)
* Fixed erroneous “Waiting for Paint” log when the passive effect phase was not delayed [#31526](https://github.com/facebook/react/pull/31526)
* Fixed a regression causing key warnings for flattened positional children in development mode. [#32117](https://github.com/facebook/react/pull/32117)
* Updated `useId` to use valid CSS selectors, changing format from `:r123:` to `«r123»`. [#32001](https://github.com/facebook/react/pull/32001)
* Added a dev-only warning for null/undefined created in useEffect, useInsertionEffect, and useLayoutEffect. [#32355](https://github.com/facebook/react/pull/32355)
* Fixed a bug where dev-only methods were exported in production builds. React.act is no longer available in production builds. [#32200](https://github.com/facebook/react/pull/32200)
* Improved consistency across prod and dev to improve compatibility with Google Closure Compiler and bindings [#31808](https://github.com/facebook/react/pull/31808)
* Improve passive effect scheduling for consistent task yielding. [#31785](https://github.com/facebook/react/pull/31785)
* Fixed asserts in React Native when passChildrenWhenCloningPersistedNodes is enabled for OffscreenComponent rendering. [#32528](https://github.com/facebook/react/pull/32528)
* Fixed component name resolution for Portal [#32640](https://github.com/facebook/react/pull/32640)
* Added support for beforetoggle and toggle events on the dialog element. [#32479](https://github.com/facebook/react/pull/32479)
### React DOM
* Fixed double warning when the `href` attribute is an empty string [#31783](https://github.com/facebook/react/pull/31783)
* Fixed an edge case where `getHoistableRoot()` didnt work properly when the container was a Document [#32321](https://github.com/facebook/react/pull/32321)
* Removed support for using HTML comments (e.g. `<!-- -->`) as a DOM container. [#32250](https://github.com/facebook/react/pull/32250)
* Added support for `<script>` and `<template>` tags to be nested within `<select>` tags. [#31837](https://github.com/facebook/react/pull/31837)
* Fixed responsive images to be preloaded as HTML instead of headers [#32445](https://github.com/facebook/react/pull/32445)
### use-sync-external-store
* Added `exports` field to `package.json` for `use-sync-external-store` to support various entrypoints. [#25231](https://github.com/facebook/react/pull/25231)
### React Server Components
* Added `unstable_prerender`, a new experimental API for prerendering React Server Components on the server [#31724](https://github.com/facebook/react/pull/31724)
* Fixed an issue where streams would hang when receiving new chunks after a global error [#31840](https://github.com/facebook/react/pull/31840), [#31851](https://github.com/facebook/react/pull/31851)
* Fixed an issue where pending chunks were counted twice. [#31833](https://github.com/facebook/react/pull/31833)
* Added support for streaming in edge environments [#31852](https://github.com/facebook/react/pull/31852)
* Added support for sending custom error names from a server so that they are available in the client for console replaying. [#32116](https://github.com/facebook/react/pull/32116)
* Updated the server component wire format to remove IDs for hints and console.log because they have no return value [#31671](https://github.com/facebook/react/pull/31671)
* Exposed `registerServerReference` in client builds to handle server references in different environments. [#32534](https://github.com/facebook/react/pull/32534)
* Added react-server-dom-parcel package which integrates Server Components with the [Parcel bundler](https://parceljs.org/) [#31725](https://github.com/facebook/react/pull/31725), [#32132](https://github.com/facebook/react/pull/32132), [#31799](https://github.com/facebook/react/pull/31799), [#32294](https://github.com/facebook/react/pull/32294), [#31741](https://github.com/facebook/react/pull/31741)
## 19.0.0 (December 5, 2024)
Below is a list of all new features, APIs, deprecations, and breaking changes. Read [React 19 release post](https://react.dev/blog/2024/04/25/react-19) and [React 19 upgrade guide](https://react.dev/blog/2024/04/25/react-19-upgrade-guide) for more information.

View File

@@ -1,23 +0,0 @@
acdlite
eps1lon
EugeneChoi4
gaearon
gnoff
unstubbable
hoxyq
jackpope
jbonta
jbrown215
josephsavona
kassens
mattcarrollcode
mofeiZ
mvitousek
pieterv
poteto
rickhanlonii
sebmarkbage
sethwebster
sophiebits
elicwhite
yuzhi

View File

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

View File

@@ -1,19 +0,0 @@
'use strict';
/**
* HACK: @poteto React Compiler inlines Zod in its build artifact. Zod spreads values passed to .map
* which causes issues in @babel/plugin-transform-spread in loose mode, as it will result in
* {undefined: undefined} which fails to parse.
*
* [@babel/plugin-transform-block-scoping', {throwIfClosureRequired: true}] also causes issues with
* the built version of the compiler. The minimal set of plugins needed for this file is reexported
* from babel.config-ts.
*
* I will remove this hack later when we move eslint-plugin-react-hooks into the compiler directory.
**/
const baseConfig = require('./babel.config-ts');
module.exports = {
plugins: baseConfig.plugins,
};

View File

@@ -1,18 +0,0 @@
/**
* This file is purely being used for local jest runs, and doesn't participate in the build process.
*/
'use strict';
module.exports = {
plugins: [
'@babel/plugin-syntax-jsx',
'@babel/plugin-transform-flow-strip-types',
['@babel/plugin-transform-class-properties', {loose: true}],
['@babel/plugin-transform-private-methods', {loose: true}],
'@babel/plugin-transform-classes',
],
presets: [
['@babel/preset-env', {targets: {node: 'current'}}],
'@babel/preset-typescript',
],
};

16
compiler/.gitignore vendored
View File

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

View File

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

1217
compiler/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

61
compiler/Cargo.toml Normal file
View File

@@ -0,0 +1,61 @@
[workspace]
resolver = "2"
members = ["crates/*"]
[workspace.package]
authors = ["The React Team https://react.dev/community/team"]
description = "React Compiler"
edition = "2021"
homepage = "https://github.com/facebook/react"
keywords = ["JavaScript", "TypeScript", "React", "React Compiler", "Compiler"]
license = "MIT"
repository = "https://github.com/facebook/react"
[workspace.dependencies]
# workspace crates
react_build_hir = { path = "crates/react_build_hir" }
react_diagnostics = { path = "crates/react_diagnostics" }
react_estree = { path = "crates/react_estree" }
react_estree_codegen = { path = "crates/react_estree_codegen" }
react_fixtures = { path = "crates/react_fixtures" }
react_hermes_parser = { path = "crates/react_hermes_parser" }
react_hir = { path = "crates/react_hir" }
react_optimization = { path = "crates/react_optimization" }
react_semantic_analysis = { path = "crates/react_semantic_analysis" }
react_ssa = { path = "crates/react_ssa" }
react_utils = { path = "crates/react_utils" }
# dependencies
indexmap = { version = "2.0.0", features = ["serde"] }
insta = { version = "1.30.0", features = ["glob"] }
miette = { version = "5.9.0" }
prettyplease = "0.2.10"
quote = "1.0.29"
serde = { version = "1.0.167", features = ["serde_derive"] }
serde_json = "1.0.100"
stacker = "0.1.15"
static_assertions = "1.1.0"
syn = "2.0.23"
thiserror = "1.0.41"
hermes = { git = "https://github.com/facebook/hermes.git" }
juno_support = { git = "https://github.com/facebook/hermes.git" }
[profile.release]
# configuration adapted from oxc
# https://github.com/Boshen/oxc/blob/ea85ee9f2d64dd284c5b7410f491d81fb879abae/Cargo.toml#L89-L97
opt-level = 3
lto = "fat"
codegen-units = 1
strip = "symbols"
debug = false
panic = "abort" # Let it crash and force ourselves to write safe Rust.
# Make insta run faster by compiling with release mode optimizations
# https://docs.rs/insta/latest/insta/#optional-faster-runs
[profile.dev.package.insta]
opt-level = 3
# Make insta diffing libary faster by compiling with release mode optimizations
# https://docs.rs/insta/latest/insta/#optional-faster-runs
[profile.dev.package.similar]
opt-level = 3

View File

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

View File

@@ -1,4 +1,4 @@
// @compilationMode:"infer"
// @compilationMode(infer)
function nonReactFn() {
  return {};
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,56 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import type {NextPage} from 'next';
import Head from 'next/head';
import {SnackbarProvider} from 'notistack';
import {Editor, Header, StoreProvider} from '../components';
import MessageSnackbar from '../components/Message';
const Home: NextPage = () => {
return (
<div className="flex flex-col w-screen h-screen font-light">
<Head>
<title>
{process.env.NODE_ENV === 'development'
? '[DEV] React Compiler Playground'
: 'React Compiler Playground'}
</title>
<meta
name="viewport"
content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0"></meta>
<link rel="icon" href="/favicon.ico" />
<link rel="manifest" href="/site.webmanifest" />
<link
rel="preload"
href="/fonts/Source-Code-Pro-Regular.woff2"
as="font"
type="font/woff2"
crossOrigin="anonymous"
/>
<link
rel="preload"
href="/fonts/Optimistic_Display_W_Lt.woff2"
as="font"
type="font/woff2"
crossOrigin="anonymous"
/>
</Head>
<StoreProvider>
<SnackbarProvider
preventDuplicate
maxSnack={10}
Components={{message: MessageSnackbar}}>
<Header />
<Editor />
</SnackbarProvider>
</StoreProvider>
</div>
);
};
export default Home;

View File

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

View File

@@ -1,215 +0,0 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import MonacoEditor, {loader, type Monaco} from '@monaco-editor/react';
import type {editor} from 'monaco-editor';
import * as monaco from 'monaco-editor';
import React, {
useState,
useRef,
unstable_ViewTransition as ViewTransition,
unstable_addTransitionType as addTransitionType,
startTransition,
} from 'react';
import {Resizable} from 're-resizable';
import {useStore, useStoreDispatch} from '../StoreContext';
import {monacoConfigOptions} from './monacoOptions';
import {IconChevron} from '../Icons/IconChevron';
import {CONFIG_PANEL_TRANSITION} from '../../lib/transitionTypes';
// @ts-expect-error - webpack asset/source loader handles .d.ts files as strings
import compilerTypeDefs from 'babel-plugin-react-compiler/dist/index.d.ts';
loader.config({monaco});
export default function ConfigEditor({
formattedAppliedConfig,
}: {
formattedAppliedConfig: string;
}): React.ReactElement {
const [isExpanded, setIsExpanded] = useState(false);
return (
// TODO: Use <Activity> when it is compatible with Monaco: https://github.com/suren-atoyan/monaco-react/issues/753
<>
<div
style={{
display: isExpanded ? 'block' : 'none',
}}>
<ExpandedEditor
onToggle={() => {
startTransition(() => {
addTransitionType(CONFIG_PANEL_TRANSITION);
setIsExpanded(false);
});
}}
formattedAppliedConfig={formattedAppliedConfig}
/>
</div>
<div
style={{
display: !isExpanded ? 'block' : 'none',
}}>
<CollapsedEditor
onToggle={() => {
startTransition(() => {
addTransitionType(CONFIG_PANEL_TRANSITION);
setIsExpanded(true);
});
}}
/>
</div>
</>
);
}
function ExpandedEditor({
onToggle,
formattedAppliedConfig,
}: {
onToggle: (expanded: boolean) => void;
formattedAppliedConfig: string;
}): React.ReactElement {
const store = useStore();
const dispatchStore = useStoreDispatch();
const debounceTimerRef = useRef<NodeJS.Timeout | null>(null);
const handleChange: (value: string | undefined) => void = (
value: string | undefined,
) => {
if (value === undefined) return;
if (debounceTimerRef.current) {
clearTimeout(debounceTimerRef.current);
}
debounceTimerRef.current = setTimeout(() => {
dispatchStore({
type: 'updateConfig',
payload: {
config: value,
},
});
}, 500); // 500ms debounce delay
};
const handleMount: (
_: editor.IStandaloneCodeEditor,
monaco: Monaco,
) => void = (_, monaco) => {
// Add the babel-plugin-react-compiler type definitions to Monaco
monaco.languages.typescript.typescriptDefaults.addExtraLib(
//@ts-expect-error - compilerTypeDefs is a string
compilerTypeDefs,
'file:///node_modules/babel-plugin-react-compiler/dist/index.d.ts',
);
monaco.languages.typescript.typescriptDefaults.setCompilerOptions({
target: monaco.languages.typescript.ScriptTarget.Latest,
allowNonTsExtensions: true,
moduleResolution: monaco.languages.typescript.ModuleResolutionKind.NodeJs,
module: monaco.languages.typescript.ModuleKind.ESNext,
noEmit: true,
strict: false,
esModuleInterop: true,
allowSyntheticDefaultImports: true,
jsx: monaco.languages.typescript.JsxEmit.React,
});
};
return (
<ViewTransition
update={{[CONFIG_PANEL_TRANSITION]: 'slide-in', default: 'none'}}>
<Resizable
minWidth={300}
maxWidth={600}
defaultSize={{width: 350}}
enable={{right: true, bottom: false}}>
<div className="bg-blue-10 relative h-full flex flex-col !h-[calc(100vh_-_3.5rem)] border border-gray-300">
<div
className="absolute w-8 h-16 bg-blue-10 rounded-r-full flex items-center justify-center z-[2] cursor-pointer border border-l-0 border-gray-300"
title="Minimize config editor"
onClick={onToggle}
style={{
top: '50%',
marginTop: '-32px',
right: '-32px',
borderTopLeftRadius: 0,
borderBottomLeftRadius: 0,
}}>
<IconChevron displayDirection="left" className="text-blue-50" />
</div>
<div className="flex-1 flex flex-col m-2 mb-2">
<div className="pb-2">
<h2 className="inline-block text-blue-50 py-1.5 px-1.5 xs:px-3 sm:px-4 text-sm">
Config Overrides
</h2>
</div>
<div className="flex-1 border border-gray-300">
<MonacoEditor
path={'config.ts'}
language={'typescript'}
value={store.config}
onMount={handleMount}
onChange={handleChange}
loading={''}
className="monaco-editor-config"
options={monacoConfigOptions}
/>
</div>
</div>
<div className="flex-1 flex flex-col m-2">
<div className="pb-2">
<h2 className="inline-block text-blue-50 py-1.5 px-1.5 xs:px-3 sm:px-4 text-sm">
Applied Configs
</h2>
</div>
<div className="flex-1 border border-gray-300">
<MonacoEditor
path={'applied-config.js'}
language={'javascript'}
value={formattedAppliedConfig}
loading={''}
className="monaco-editor-applied-config"
options={{
...monacoConfigOptions,
readOnly: true,
}}
/>
</div>
</div>
</div>
</Resizable>
</ViewTransition>
);
}
function CollapsedEditor({
onToggle,
}: {
onToggle: () => void;
}): React.ReactElement {
return (
<div
className="w-4 !h-[calc(100vh_-_3.5rem)]"
style={{position: 'relative'}}>
<div
className="absolute w-10 h-16 bg-blue-10 hover:translate-x-2 transition-transform rounded-r-full flex items-center justify-center z-[2] cursor-pointer border border-gray-300"
title="Expand config editor"
onClick={onToggle}
style={{
top: '50%',
marginTop: '-32px',
left: '-8px',
borderTopLeftRadius: 0,
borderBottomLeftRadius: 0,
}}>
<IconChevron displayDirection="right" className="text-blue-50" />
</div>
</div>
);
}

View File

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

View File

@@ -6,31 +6,22 @@
*/
import MonacoEditor, {loader, type Monaco} from '@monaco-editor/react';
import {
CompilerErrorDetail,
CompilerDiagnostic,
} from 'babel-plugin-react-compiler';
import {CompilerErrorDetail} from 'babel-plugin-react-compiler/src';
import invariant from 'invariant';
import type {editor} from 'monaco-editor';
import * as monaco from 'monaco-editor';
import {
useEffect,
useState,
unstable_ViewTransition as ViewTransition,
} from 'react';
import {Resizable} from 're-resizable';
import {useEffect, useState} from 'react';
import {renderReactCompilerMarkers} from '../../lib/reactCompilerMonacoDiagnostics';
import {useStore, useStoreDispatch} from '../StoreContext';
import TabbedWindow from '../TabbedWindow';
import {monacoOptions} from './monacoOptions';
import {CONFIG_PANEL_TRANSITION} from '../../lib/transitionTypes';
// @ts-expect-error TODO: Make TS recognize .d.ts files, in addition to loading them with webpack.
import React$Types from '../../node_modules/@types/react/index.d.ts';
loader.config({monaco});
type Props = {
errors: Array<CompilerErrorDetail | CompilerDiagnostic>;
errors: Array<CompilerErrorDetail>;
language: 'flow' | 'typescript';
};
@@ -45,13 +36,13 @@ export default function Input({errors, language}: Props): JSX.Element {
const uri = monaco.Uri.parse(`file:///index.js`);
const model = monaco.editor.getModel(uri);
invariant(model, 'Model must exist for the selected input file.');
renderReactCompilerMarkers({
monaco,
model,
details: errors,
source: store.source,
});
}, [monaco, errors, store.source]);
renderReactCompilerMarkers({monaco, model, details: errors});
/**
* N.B. that `tabSize` is a model property, not an editor property.
* So, the tab size has to be set per model.
*/
model.updateOptions({tabSize: 2});
}, [monaco, errors]);
useEffect(() => {
/**
@@ -83,11 +74,11 @@ export default function Input({errors, language}: Props): JSX.Element {
});
}, [monaco, language]);
const handleChange: (value: string | undefined) => void = async value => {
const handleChange: (value: string | undefined) => void = value => {
if (!value) return;
dispatchStore({
type: 'updateSource',
type: 'updateFile',
payload: {
source: value,
},
@@ -98,9 +89,6 @@ export default function Input({errors, language}: Props): JSX.Element {
_: editor.IStandaloneCodeEditor,
monaco: Monaco,
) => void = (_, monaco) => {
if (typeof window !== 'undefined') {
window['__MONACO_LOADED__'] = true;
}
setMonaco(monaco);
const tscOptions = {
@@ -139,42 +127,30 @@ export default function Input({errors, language}: Props): JSX.Element {
});
};
const editorContent = (
<MonacoEditor
path={'index.js'}
/**
* .js and .jsx files are specified to be TS so that Monaco can actually
* check their syntax using its TS language service. They are still JS files
* due to their extensions, so TS language features don't work.
*/
language={'javascript'}
value={store.source}
onMount={handleMount}
onChange={handleChange}
className="monaco-editor-input"
options={monacoOptions}
loading={''}
/>
);
const tabs = new Map([['Input', editorContent]]);
const [activeTab, setActiveTab] = useState('Input');
return (
<ViewTransition
update={{
[CONFIG_PANEL_TRANSITION]: 'container',
default: 'none',
}}>
<div className="flex-1 min-w-[550px] sm:min-w-0">
<div className="flex flex-col h-full !h-[calc(100vh_-_3.5rem)] border-r border-gray-200">
<TabbedWindow
tabs={tabs}
activeTab={activeTab}
onTabChange={setActiveTab}
/>
</div>
</div>
</ViewTransition>
<div className="relative flex flex-col flex-none border-r border-gray-200">
<Resizable
minWidth={650}
enable={{right: true}}
/**
* Restrict MonacoEditor's height, since the config autoLayout:true
* will grow the editor to fit within parent element
*/
className="!h-[calc(100vh_-_3.5rem)]">
<MonacoEditor
path={'index.js'}
/**
* .js and .jsx files are specified to be TS so that Monaco can actually
* check their syntax using its TS language service. They are still JS files
* due to their extensions, so TS language features don't work.
*/
language={'javascript'}
value={store.source}
onMount={handleMount}
onChange={handleChange}
options={monacoOptions}
/>
</Resizable>
</div>
);
}

View File

@@ -11,46 +11,19 @@ import {
InformationCircleIcon,
} from '@heroicons/react/outline';
import MonacoEditor, {DiffEditor} from '@monaco-editor/react';
import {
CompilerErrorDetail,
CompilerDiagnostic,
type CompilerError,
} from 'babel-plugin-react-compiler';
import {type CompilerError} from 'babel-plugin-react-compiler/src';
import parserBabel from 'prettier/plugins/babel';
import * as prettierPluginEstree from 'prettier/plugins/estree';
import * as prettier from 'prettier/standalone';
import {memo, ReactNode, useEffect, useState} from 'react';
import {type Store} from '../../lib/stores';
import {
memo,
ReactNode,
use,
useState,
Suspense,
unstable_ViewTransition as ViewTransition,
unstable_addTransitionType as addTransitionType,
startTransition,
} from 'react';
import AccordionWindow from '../AccordionWindow';
import TabbedWindow from '../TabbedWindow';
import {monacoOptions} from './monacoOptions';
import {BabelFileResult} from '@babel/core';
import {
CONFIG_PANEL_TRANSITION,
TOGGLE_INTERNALS_TRANSITION,
EXPAND_ACCORDION_TRANSITION,
} from '../../lib/transitionTypes';
import {LRUCache} from 'lru-cache';
const MemoizedOutput = memo(Output);
export default MemoizedOutput;
export const BASIC_OUTPUT_TAB_NAMES = ['Output', 'SourceMap'];
const tabifyCache = new LRUCache<Store, Promise<Map<string, ReactNode>>>({
max: 5,
});
export type PrintedCompilerPipelineValue =
| {
kind: 'hir';
@@ -71,7 +44,6 @@ export type CompilerOutput =
kind: 'ok';
transformOutput: CompilerTransformOutput;
results: Map<string, Array<PrintedCompilerPipelineValue>>;
errors: Array<CompilerErrorDetail | CompilerDiagnostic>;
}
| {
kind: 'err';
@@ -87,16 +59,12 @@ type Props = {
async function tabify(
source: string,
compilerOutput: CompilerOutput,
showInternals: boolean,
): Promise<Map<string, ReactNode>> {
const tabs = new Map<string, React.ReactNode>();
const reorderedTabs = new Map<string, React.ReactNode>();
const concattedResults = new Map<string, string>();
// Concat all top level function declaration results into a single tab for each pass
for (const [passName, results] of compilerOutput.results) {
if (!showInternals && !BASIC_OUTPUT_TAB_NAMES.includes(passName)) {
continue;
}
for (const result of results) {
switch (result.kind) {
case 'hir': {
@@ -155,36 +123,10 @@ async function tabify(
parser: transformOutput.language === 'flow' ? 'babel-flow' : 'babel-ts',
plugins: [parserBabel, prettierPluginEstree],
});
let output: string;
let language: string;
if (compilerOutput.errors.length === 0) {
output = code;
language = 'javascript';
} else {
language = 'markdown';
output = `
# Summary
React Compiler compiled this function successfully, but there are lint errors that indicate potential issues with the original code.
## ${compilerOutput.errors.length} Lint Errors
${compilerOutput.errors.map(e => e.printErrorMessage(source, {eslint: false})).join('\n\n')}
## Output
\`\`\`js
${code}
\`\`\`
`.trim();
}
reorderedTabs.set(
'Output',
'JS',
<TextTabContent
output={output}
language={language}
output={code}
diff={null}
showInfoPanel={false}></TextTabContent>,
);
@@ -200,18 +142,6 @@ ${code}
</>,
);
}
} else if (compilerOutput.kind === 'err') {
const errors = compilerOutput.error.printErrorMessage(source, {
eslint: false,
});
reorderedTabs.set(
'Output',
<TextTabContent
output={errors}
language="markdown"
diff={null}
showInfoPanel={false}></TextTabContent>,
);
}
tabs.forEach((tab, name) => {
reorderedTabs.set(name, tab);
@@ -219,25 +149,6 @@ ${code}
return reorderedTabs;
}
function tabifyCached(
store: Store,
compilerOutput: CompilerOutput,
): Promise<Map<string, ReactNode>> {
const cached = tabifyCache.get(store);
if (cached) return cached;
const result = tabify(store.source, compilerOutput, store.showInternals);
tabifyCache.set(store, result);
return result;
}
function Fallback(): JSX.Element {
return (
<div className="w-full h-monaco_small sm:h-monaco flex items-center justify-center">
Loading...
</div>
);
}
function utf16ToUTF8(s: string): string {
return unescape(encodeURIComponent(s));
}
@@ -251,40 +162,17 @@ function getSourceMapUrl(code: string, map: string): string | null {
}
function Output({store, compilerOutput}: Props): JSX.Element {
return (
<Suspense fallback={<Fallback />}>
<OutputContent store={store} compilerOutput={compilerOutput} />
</Suspense>
const [tabsOpen, setTabsOpen] = useState<Set<string>>(() => new Set(['JS']));
const [tabs, setTabs] = useState<Map<string, React.ReactNode>>(
() => new Map(),
);
}
useEffect(() => {
tabify(store.source, compilerOutput).then(tabs => {
setTabs(tabs);
});
}, [store.source, compilerOutput]);
function OutputContent({store, compilerOutput}: Props): JSX.Element {
const [tabsOpen, setTabsOpen] = useState<Set<string>>(
() => new Set(['Output']),
);
const [activeTab, setActiveTab] = useState<string>('Output');
/*
* Update the active tab back to the output or errors tab when the compilation state
* changes between success/failure.
*/
const [previousOutputKind, setPreviousOutputKind] = useState(
compilerOutput.kind,
);
const isFailure = compilerOutput.kind !== 'ok';
if (compilerOutput.kind !== previousOutputKind) {
setPreviousOutputKind(compilerOutput.kind);
if (isFailure) {
startTransition(() => {
addTransitionType(EXPAND_ACCORDION_TRANSITION);
setTabsOpen(prev => new Set(prev).add('Output'));
setActiveTab('Output');
});
}
}
const changedPasses: Set<string> = new Set(['Output', 'HIR']); // Initial and final passes should always be bold
const changedPasses: Set<string> = new Set(['JS', 'HIR']); // Initial and final passes should always be bold
let lastResult: string = '';
for (const [passName, results] of compilerOutput.results) {
for (const result of results) {
@@ -298,40 +186,31 @@ function OutputContent({store, compilerOutput}: Props): JSX.Element {
lastResult = currResult;
}
}
const tabs = use(tabifyCached(store, compilerOutput));
if (!store.showInternals) {
return (
<ViewTransition
update={{
[CONFIG_PANEL_TRANSITION]: 'container',
[TOGGLE_INTERNALS_TRANSITION]: '',
default: 'none',
}}>
<TabbedWindow
tabs={tabs}
activeTab={activeTab}
onTabChange={setActiveTab}
/>
</ViewTransition>
);
}
return (
<ViewTransition
update={{
[CONFIG_PANEL_TRANSITION]: 'accordion-container',
[TOGGLE_INTERNALS_TRANSITION]: '',
default: 'none',
}}>
<AccordionWindow
defaultTab={store.showInternals ? 'HIR' : 'Output'}
<>
<TabbedWindow
defaultTab="HIR"
setTabsOpen={setTabsOpen}
tabsOpen={tabsOpen}
tabs={tabs}
changedPasses={changedPasses}
/>
</ViewTransition>
{compilerOutput.kind === 'err' ? (
<div
className="flex flex-wrap absolute bottom-0 bg-white grow border-y border-grey-200 transition-all ease-in"
style={{width: 'calc(100vw - 650px)'}}>
<div className="w-full p-4 basis-full border-b">
<h2>COMPILER ERRORS</h2>
</div>
<pre
className="p-4 basis-full text-red-600 overflow-y-scroll whitespace-pre-wrap"
style={{width: 'calc(100vw - 650px)', height: '150px'}}>
<code>{compilerOutput.error.toString()}</code>
</pre>
</div>
) : null}
</>
);
}
@@ -339,12 +218,10 @@ function TextTabContent({
output,
diff,
showInfoPanel,
language,
}: {
output: string;
diff: string | null;
showInfoPanel: boolean;
language: string;
}): JSX.Element {
const [diffMode, setDiffMode] = useState(false);
return (
@@ -383,29 +260,20 @@ function TextTabContent({
<DiffEditor
original={diff}
modified={output}
loading={''}
options={{
...monacoOptions,
scrollbar: {
vertical: 'hidden',
},
dimension: {
width: 0,
height: 0,
},
readOnly: true,
lineNumbers: 'off',
glyphMargin: false,
// Undocumented see https://github.com/Microsoft/vscode/issues/30795#issuecomment-410998882
overviewRulerLanes: 0,
lineDecorationsWidth: 0,
lineNumbersMinChars: 0,
}}
/>
) : (
<MonacoEditor
language={language ?? 'javascript'}
defaultLanguage="javascript"
value={output}
loading={''}
className="monaco-editor-output"
options={{
...monacoOptions,
readOnly: true,

View File

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

View File

@@ -10,20 +10,14 @@ import {CheckIcon} from '@heroicons/react/solid';
import clsx from 'clsx';
import Link from 'next/link';
import {useSnackbar} from 'notistack';
import {
useState,
startTransition,
unstable_addTransitionType as addTransitionType,
} from 'react';
import {useState} from 'react';
import {defaultStore} from '../lib/defaultStore';
import {IconGitHub} from './Icons/IconGitHub';
import Logo from './Logo';
import {useStore, useStoreDispatch} from './StoreContext';
import {TOGGLE_INTERNALS_TRANSITION} from '../lib/transitionTypes';
import {useStoreDispatch} from './StoreContext';
export default function Header(): JSX.Element {
const [showCheck, setShowCheck] = useState(false);
const store = useStore();
const dispatchStore = useStoreDispatch();
const {enqueueSnackbar, closeSnackbar} = useSnackbar();
@@ -62,32 +56,6 @@ export default function Header(): JSX.Element {
<p className="hidden select-none sm:block">React Compiler Playground</p>
</div>
<div className="flex items-center text-[15px] gap-4">
<div className="flex items-center gap-2">
<label className="show-internals relative inline-block w-[34px] h-5">
<input
type="checkbox"
checked={store.showInternals}
onChange={() =>
startTransition(() => {
addTransitionType(TOGGLE_INTERNALS_TRANSITION);
dispatchStore({type: 'toggleInternals'});
})
}
className="absolute opacity-0 cursor-pointer h-full w-full m-0"
/>
<span
className={clsx(
'absolute inset-0 rounded-full cursor-pointer transition-all duration-250',
"before:content-[''] before:absolute before:w-4 before:h-4 before:left-0.5 before:bottom-0.5",
'before:bg-white before:rounded-full before:transition-transform before:duration-250',
'focus-within:shadow-[0_0_1px_#2196F3]',
store.showInternals
? 'bg-link before:translate-x-3.5'
: 'bg-gray-300',
)}></span>
</label>
<span className="text-secondary">Show Internals</span>
</div>
<button
title="Reset Playground"
aria-label="Reset Playground"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,11 +0,0 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
export const CONFIG_PANEL_TRANSITION = 'config-panel';
export const TOGGLE_TAB_TRANSITION = 'toggle-tab';
export const TOGGLE_INTERNALS_TRANSITION = 'toggle-internals';
export const EXPAND_ACCORDION_TRANSITION = 'open-accordion';

View File

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

View File

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

View File

@@ -4,7 +4,7 @@
"private": true,
"scripts": {
"dev": "cd ../.. && concurrently --kill-others -n compiler,runtime,playground \"yarn workspace babel-plugin-react-compiler run watch\" \"yarn workspace react-compiler-runtime run watch\" \"wait-on packages/babel-plugin-react-compiler/dist/index.js && cd apps/playground && NODE_ENV=development next dev\"",
"build:compiler": "cd ../.. && concurrently -n compiler,runtime \"yarn workspace babel-plugin-react-compiler run build --dts\" \"yarn workspace react-compiler-runtime run build\"",
"build:compiler": "cd ../.. && concurrently -n compiler,runtime \"yarn workspace babel-plugin-react-compiler run build\" \"yarn workspace react-compiler-runtime run build\"",
"build": "yarn build:compiler && next build",
"postbuild": "node ./scripts/downloadFonts.js",
"preinstall": "cd ../.. && yarn install --frozen-lockfile",
@@ -12,7 +12,7 @@
"vercel-build": "yarn build",
"start": "next start",
"lint": "next lint",
"test": "playwright test --workers=4"
"test": "playwright test"
},
"dependencies": {
"@babel/core": "^7.18.9",
@@ -22,43 +22,42 @@
"@babel/plugin-transform-block-scoping": "^7.18.9",
"@babel/plugin-transform-modules-commonjs": "^7.18.9",
"@babel/preset-react": "^7.18.9",
"@babel/preset-typescript": "^7.26.0",
"@babel/preset-typescript": "^7.18.9",
"@babel/traverse": "^7.18.9",
"@babel/types": "7.26.3",
"@babel/types": "7.18.9",
"@heroicons/react": "^1.0.6",
"@monaco-editor/react": "^4.4.6",
"@playwright/test": "^1.51.1",
"@playwright/test": "^1.42.1",
"@use-gesture/react": "^10.2.22",
"hermes-eslint": "^0.25.0",
"hermes-parser": "^0.25.0",
"invariant": "^2.2.4",
"lru-cache": "^11.2.2",
"lz-string": "^1.5.0",
"monaco-editor": "^0.52.0",
"next": "15.6.0-canary.7",
"next": "^15.0.1",
"notistack": "^3.0.0-alpha.7",
"prettier": "^3.3.3",
"pretty-format": "^29.3.1",
"re-resizable": "^6.9.16",
"react": "19.1.1",
"react-dom": "19.1.1"
"react": "19.0.0-rc-77b637d6-20241016",
"react-dom": "19.0.0-rc-77b637d6-20241016"
},
"devDependencies": {
"@types/node": "18.11.9",
"@types/react": "19.1.13",
"@types/react-dom": "19.1.9",
"@types/react": "npm:types-react@19.0.0-rc.1",
"@types/react-dom": "npm:types-react-dom@19.0.0-rc.1",
"autoprefixer": "^10.4.13",
"clsx": "^1.2.1",
"concurrently": "^7.4.0",
"eslint": "^8.28.0",
"eslint-config-next": "15.5.2",
"eslint-config-next": "^15.0.1",
"monaco-editor-webpack-plugin": "^7.1.0",
"postcss": "^8.4.31",
"tailwindcss": "^3.2.4",
"wait-on": "^7.2.0"
},
"resolutions": {
"@types/react": "19.1.12",
"@types/react-dom": "19.1.9"
"@types/react": "npm:types-react@19.0.0-rc.1",
"@types/react-dom": "npm:types-react-dom@19.0.0-rc.1"
}
}

View File

@@ -23,7 +23,7 @@ export default defineConfig({
// Test directory
testDir: path.join(__dirname, '__tests__/e2e'),
// If a test fails, retry it additional 2 times
retries: 3,
retries: 2,
// Artifacts folder where screenshots, videos, and traces are stored.
outputDir: 'test-results/',
// Note: we only use text snapshots, so its safe to omit the host environment name
@@ -55,16 +55,12 @@ export default defineConfig({
// contextOptions: {
// ignoreHTTPSErrors: true,
// },
viewport: {width: 1920, height: 1080},
},
projects: [
{
name: 'chromium',
use: {
...devices['Desktop Chrome'],
viewport: {width: 1920, height: 1080},
},
use: {...devices['Desktop Chrome']},
},
// {
// name: 'Desktop Firefox',

View File

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

View File

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

View File

@@ -6,9 +6,6 @@
"dom.iterable",
"esnext"
],
"types": [
"react/experimental"
],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
@@ -19,7 +16,7 @@
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react-jsx",
"jsx": "preserve",
"incremental": true,
"plugins": [
{

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,21 @@
[package]
name = "react_build_hir"
version = "0.1.0"
publish = false
authors.workspace = true
description.workspace = true
edition.workspace = true
homepage.workspace = true
keywords.workspace = true
license.workspace = true
repository.workspace = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
react_hir = { workspace = true }
react_estree = { workspace = true}
indexmap = { workspace = true }
react_diagnostics = { workspace = true }
react_semantic_analysis = { workspace = true }
thiserror = { workspace = true }

View File

@@ -0,0 +1,3 @@
# Build-HIR
This crate converts from `react_estree` into React Compiler's HIR format as the first phase of compilation.

View File

@@ -0,0 +1,746 @@
/*
* 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.
*/
use std::collections::HashSet;
use react_diagnostics::Diagnostic;
use react_estree::{
AssignmentPropertyOrRestElement, AssignmentTarget, BlockStatement, Expression,
ExpressionOrSpread, ExpressionOrSuper, ForInit, Function, IntoFunction, JsValue, Pattern,
Statement, VariableDeclaration, VariableDeclarationKind,
};
use react_hir::{
ArrayDestructureItem, BlockKind, BranchTerminal, Destructure, DestructurePattern, Environment,
ForTerminal, GotoKind, Identifier, IdentifierOperand, InstructionKind, InstructionValue,
JSXAttribute, JSXElement, LValue, LoadGlobal, LoadLocal, ObjectDestructureItem,
ObjectDestructureProperty, PlaceOrSpread, TerminalValue,
};
use crate::builder::{Builder, LoopScope};
use crate::context::get_context_identifiers;
use crate::error::BuildHIRError;
/// Converts a React function in ESTree format into HIR. Returns the HIR
/// if it was constructed sucessfully, otherwise a list of diagnostics
/// if the input could be not be converted to HIR.
///
/// Failures generally include nonsensical input (`delete 1`) or syntax
/// that is not yet supported.
pub fn build(env: &Environment, fun: &Function) -> Result<Box<react_hir::Function>, Diagnostic> {
let mut builder = Builder::new(env);
let mut params = Vec::with_capacity(fun.params.len());
for param in &fun.params {
match param {
Pattern::Identifier(param) => {
let identifier = lower_identifier_for_assignment(
env,
&mut builder,
InstructionKind::Let,
param,
)?;
params.push(identifier);
}
_ => {
return Err(Diagnostic::todo(
"Support non-identifier params",
param.range(),
));
}
}
}
match &fun.body {
Some(react_estree::FunctionBody::BlockStatement(body)) => {
lower_block_statement(env, &mut builder, body)?
}
Some(react_estree::FunctionBody::Expression(body)) => {
lower_expression(env, &mut builder, body)?;
}
None => {
return Err(Diagnostic::invalid_syntax(
BuildHIRError::EmptyFunction,
fun.range,
));
}
}
// In case the function did not explicitly return, terminate the final
// block with an explicit `return undefined`. If the function *did* return,
// this will be unreachable and get pruned later.
let implicit_return_value = builder.push(InstructionValue::Primitive(react_hir::Primitive {
value: JsValue::Undefined,
}));
builder.terminate(
TerminalValue::Return(react_hir::ReturnTerminal {
value: implicit_return_value,
}),
react_hir::BlockKind::Block,
);
let body = builder.build()?;
Ok(Box::new(react_hir::Function {
id: fun.id.as_ref().map(|id| id.name.clone()),
body,
params,
// TODO: populate context!
context: Default::default(),
is_async: fun.is_async,
is_generator: fun.is_generator,
}))
}
fn lower_block_statement(
env: &Environment,
builder: &mut Builder,
stmt: &BlockStatement,
) -> Result<(), Diagnostic> {
for stmt in &stmt.body {
lower_statement(env, builder, stmt, None)?;
}
Ok(())
}
/// Convert a statement to HIR. This will often result in multiple instructions and blocks
/// being created as statements often describe control flow.
fn lower_statement(
env: &Environment,
builder: &mut Builder,
stmt: &Statement,
label: Option<String>,
) -> Result<(), Diagnostic> {
match stmt {
Statement::BlockStatement(stmt) => {
lower_block_statement(env, builder, stmt)?;
}
Statement::BreakStatement(stmt) => {
let block = builder.resolve_break(stmt.label.as_ref())?;
builder.terminate(
TerminalValue::Goto(react_hir::GotoTerminal {
block,
kind: GotoKind::Break,
}),
BlockKind::Block,
);
}
Statement::ContinueStatement(stmt) => {
let block = builder.resolve_continue(stmt.label.as_ref())?;
builder.terminate(
TerminalValue::Goto(react_hir::GotoTerminal {
block,
kind: GotoKind::Continue,
}),
BlockKind::Block,
);
}
Statement::ReturnStatement(stmt) => {
let value = match &stmt.argument {
Some(argument) => lower_expression(env, builder, argument)?,
None => builder.push(InstructionValue::Primitive(react_hir::Primitive {
value: JsValue::Undefined,
})),
};
builder.terminate(
TerminalValue::Return(react_hir::ReturnTerminal { value }),
BlockKind::Block,
);
}
Statement::ExpressionStatement(stmt) => {
lower_expression(env, builder, &stmt.expression)?;
}
Statement::EmptyStatement(_) => {
// no-op
}
Statement::VariableDeclaration(stmt) => {
lower_variable_declaration(env, builder, stmt)?;
}
Statement::IfStatement(stmt) => {
// block for what follows the if statement, though this may
// not be reachable
let fallthrough_block = builder.reserve(BlockKind::Block);
let consequent_block = builder.enter(BlockKind::Block, |builder| {
lower_statement(env, builder, &stmt.consequent, None)?;
Ok(TerminalValue::Goto(react_hir::GotoTerminal {
block: fallthrough_block.id,
kind: GotoKind::Break,
}))
})?;
let alternate_block = builder.enter(BlockKind::Block, |builder| {
if let Some(alternate) = &stmt.alternate {
lower_statement(env, builder, alternate, None)?;
}
Ok(TerminalValue::Goto(react_hir::GotoTerminal {
block: fallthrough_block.id,
kind: GotoKind::Break,
}))
})?;
let test = lower_expression(env, builder, &stmt.test)?;
let terminal = TerminalValue::If(react_hir::IfTerminal {
test,
consequent: consequent_block,
alternate: alternate_block,
fallthrough: Some(fallthrough_block.id),
});
builder.terminate_with_fallthrough(terminal, fallthrough_block);
}
Statement::ForStatement(stmt) => {
// Block for the loop's test condition
let test_block = builder.reserve(BlockKind::Loop);
// Block for code following the loop
let fallthrough_block = builder.reserve(BlockKind::Block);
let init_block = builder.enter(BlockKind::Loop, |builder| {
if let Some(ForInit::VariableDeclaration(decl)) = &stmt.init {
lower_variable_declaration(env, builder, decl)?;
Ok(TerminalValue::Goto(react_hir::GotoTerminal {
block: test_block.id,
kind: GotoKind::Break,
}))
} else {
Err(Diagnostic::todo(
BuildHIRError::ForStatementIsMissingInitializer,
None,
))
}
})?;
let update_block = stmt
.update
.as_ref()
.map(|update| {
builder.enter(BlockKind::Loop, |builder| {
lower_expression(env, builder, update)?;
Ok(TerminalValue::Goto(react_hir::GotoTerminal {
block: test_block.id,
kind: GotoKind::Break,
}))
})
})
.transpose()?;
let body_block = builder.enter(BlockKind::Block, |builder| {
let loop_ = LoopScope {
label,
continue_block: update_block.unwrap_or(test_block.id),
break_block: fallthrough_block.id,
};
builder.enter_loop(loop_, |builder| {
lower_statement(env, builder, &stmt.body, None)?;
Ok(TerminalValue::Goto(react_hir::GotoTerminal {
block: update_block.unwrap_or(test_block.id),
kind: GotoKind::Continue,
}))
})
})?;
let terminal = TerminalValue::For(ForTerminal {
body: body_block,
init: init_block,
test: test_block.id,
fallthrough: fallthrough_block.id,
update: update_block,
});
builder.terminate_with_fallthrough(terminal, test_block);
if let Some(test) = &stmt.test {
let test_value = lower_expression(env, builder, test)?;
let terminal = TerminalValue::Branch(BranchTerminal {
test: test_value,
consequent: body_block,
alternate: fallthrough_block.id,
});
builder.terminate_with_fallthrough(terminal, fallthrough_block);
} else {
return Err(Diagnostic::todo(
BuildHIRError::ForStatementIsMissingTest,
stmt.range,
));
}
}
_ => todo!("Lower {stmt:#?}"),
}
Ok(())
}
fn lower_variable_declaration(
env: &Environment,
builder: &mut Builder,
stmt: &VariableDeclaration,
) -> Result<(), Diagnostic> {
let kind = match stmt.kind {
VariableDeclarationKind::Const => InstructionKind::Const,
VariableDeclarationKind::Let => InstructionKind::Let,
VariableDeclarationKind::Var => {
return Err(Diagnostic::unsupported(
BuildHIRError::VariableDeclarationKindIsVar,
stmt.range,
));
}
};
for declaration in &stmt.declarations {
if let Some(init) = &declaration.init {
let value = lower_expression(env, builder, init)?;
lower_assignment_pattern(env, builder, kind, &declaration.id, value)?;
} else {
match &declaration.id {
Pattern::Identifier(id) => {
let identifier = env.resolve_variable_declaration(id.as_ref(), &id.name);
if let Some(identifier) = identifier {
builder.push(InstructionValue::DeclareLocal(react_hir::DeclareLocal {
lvalue: LValue {
identifier: IdentifierOperand {
identifier,
effect: None,
},
kind,
},
}));
} else {
return Err(Diagnostic::invariant(
BuildHIRError::VariableDeclarationBindingIsNonLocal,
id.range,
));
}
}
_ => {
return Err(Diagnostic::invalid_syntax(
"Expected an identifier for variable declaration without an intializer. Destructuring requires an initial value",
declaration.range,
));
}
}
}
}
Ok(())
}
/// Converts an ESTree Expression into an HIR InstructionValue. Note that while only a single
/// InstructionValue is returned, this function is recursive and may cause multiple instructions
/// to be emitted, possibly across multiple basic blocks (in the case of expressions with control
/// flow semenatics such as logical, conditional, and optional expressions).
fn lower_expression(
env: &Environment,
builder: &mut Builder,
expr: &Expression,
) -> Result<IdentifierOperand, Diagnostic> {
let value = match expr {
Expression::Identifier(expr) => {
let identifier = env.resolve_variable_reference(expr.as_ref());
if let Some(identifier) = identifier {
let place = IdentifierOperand {
effect: None,
identifier,
};
InstructionValue::LoadLocal(LoadLocal { place })
} else {
InstructionValue::LoadGlobal(LoadGlobal {
name: expr.name.clone(),
})
}
}
Expression::Literal(expr) => InstructionValue::Primitive(react_hir::Primitive {
value: expr.value.clone(),
}),
Expression::NumericLiteral(expr) => InstructionValue::Primitive(react_hir::Primitive {
value: JsValue::Number(expr.value),
}),
Expression::BooleanLiteral(expr) => InstructionValue::Primitive(react_hir::Primitive {
value: JsValue::Boolean(expr.value),
}),
Expression::StringLiteral(expr) => InstructionValue::Primitive(react_hir::Primitive {
value: JsValue::String(expr.value.clone()),
}),
Expression::NullLiteral(_expr) => InstructionValue::Primitive(react_hir::Primitive {
value: JsValue::Null,
}),
Expression::ArrayExpression(expr) => {
let mut elements = Vec::with_capacity(expr.elements.len());
for expr in &expr.elements {
let element = match expr {
Some(react_estree::ExpressionOrSpread::SpreadElement(expr)) => Some(
PlaceOrSpread::Spread(lower_expression(env, builder, &expr.argument)?),
),
Some(react_estree::ExpressionOrSpread::Expression(expr)) => {
Some(PlaceOrSpread::Place(lower_expression(env, builder, expr)?))
}
None => None,
};
elements.push(element);
}
InstructionValue::Array(react_hir::Array { elements })
}
Expression::AssignmentExpression(expr) => match expr.operator {
react_estree::AssignmentOperator::Equals => {
let right = lower_expression(env, builder, &expr.right)?;
return lower_assignment(
env,
builder,
InstructionKind::Reassign,
&expr.left,
right,
);
}
_ => todo!("lower assignment expr {:#?}", expr),
},
Expression::BinaryExpression(expr) => {
let left = lower_expression(env, builder, &expr.left)?;
let right = lower_expression(env, builder, &expr.right)?;
InstructionValue::Binary(react_hir::Binary {
left,
operator: expr.operator,
right,
})
}
Expression::FunctionExpression(expr) => {
InstructionValue::Function(lower_function(env, builder, expr.as_ref())?)
}
Expression::ArrowFunctionExpression(expr) => {
InstructionValue::Function(lower_function(env, builder, expr.as_ref())?)
}
Expression::CallExpression(expr) => {
let callee_expr = match &expr.callee {
ExpressionOrSuper::Super(callee) => {
return Err(Diagnostic::unsupported(
BuildHIRError::UnsupportedSuperExpression,
callee.range,
));
}
ExpressionOrSuper::Expression(callee) => callee,
};
if matches!(&callee_expr, Expression::MemberExpression(_)) {
return Err(Diagnostic::todo("Support method calls", expr.range));
}
let callee = lower_expression(env, builder, callee_expr)?;
let arguments = lower_arguments(env, builder, &expr.arguments)?;
InstructionValue::Call(react_hir::Call { callee, arguments })
}
Expression::JSXElement(expr) => {
InstructionValue::JSXElement(lower_jsx_element(env, builder, expr)?)
}
_ => todo!("Lower expr {expr:#?}"),
};
Ok(builder.push(value))
}
fn lower_arguments(
env: &Environment,
builder: &mut Builder,
args: &[ExpressionOrSpread],
) -> Result<Vec<PlaceOrSpread>, Diagnostic> {
let mut arguments = Vec::with_capacity(args.len());
for arg in args {
let element = match arg {
react_estree::ExpressionOrSpread::SpreadElement(arg) => {
PlaceOrSpread::Spread(lower_expression(env, builder, &arg.argument)?)
}
react_estree::ExpressionOrSpread::Expression(arg) => {
PlaceOrSpread::Place(lower_expression(env, builder, arg)?)
}
};
arguments.push(element);
}
Ok(arguments)
}
fn lower_function<T: IntoFunction>(
env: &Environment,
_builder: &mut Builder,
function: &T,
) -> Result<react_hir::FunctionExpression, Diagnostic> {
let context_identifiers = get_context_identifiers(env, function);
let mut context = Vec::new();
let mut seen = HashSet::new();
for declaration_id in context_identifiers {
if let Some(identifier) = env.resolve_declaration_id(declaration_id) {
if !seen.insert(identifier.id) {
continue;
}
context.push(IdentifierOperand {
effect: None,
identifier,
});
}
}
let mut fun = build(env, function.function())?;
fun.context = context;
Ok(react_hir::FunctionExpression {
// TODO: collect dependencies!
dependencies: Default::default(),
lowered_function: fun,
})
}
fn lower_jsx_element(
env: &Environment,
builder: &mut Builder,
expr: &react_estree::JSXElement,
) -> Result<JSXElement, Diagnostic> {
let props: Result<Vec<JSXAttribute>, Diagnostic> = expr
.opening_element
.attributes
.iter()
.map(|attr| lower_jsx_attribute(env, builder, attr))
.collect();
let _props = props?;
let children: Result<Vec<IdentifierOperand>, Diagnostic> = expr
.children
.iter()
.map(|child| {
let child = lower_jsx_child(env, builder, child)?;
Ok(child)
})
.collect();
let _children = children?;
todo!("lower jsx element");
// Ok(JSXElement {
// tag: todo!(),
// props,
// children: if children.is_empty() {
// None
// } else {
// Some(children)
// },
// })
}
fn lower_jsx_attribute(
_env: &Environment,
_builder: &mut Builder,
_attr: &react_estree::JSXAttributeOrSpread,
) -> Result<JSXAttribute, Diagnostic> {
todo!("lower jsx attribute")
}
fn lower_jsx_child(
_env: &Environment,
_builder: &mut Builder,
_child: &react_estree::JSXChildItem,
) -> Result<IdentifierOperand, Diagnostic> {
todo!("lower jsx child")
}
fn lower_assignment(
env: &Environment,
builder: &mut Builder,
kind: InstructionKind,
lvalue: &AssignmentTarget,
value: IdentifierOperand,
) -> Result<IdentifierOperand, Diagnostic> {
Ok(match lvalue {
AssignmentTarget::Pattern(lvalue) => {
lower_assignment_pattern(env, builder, kind, lvalue, value)?
}
_ => todo!("lower assignment for {:#?}", lvalue),
})
}
// TODO: change the success type to void, no caller uses it
fn lower_assignment_pattern(
env: &Environment,
builder: &mut Builder,
kind: InstructionKind,
lvalue: &Pattern,
value: IdentifierOperand,
) -> Result<IdentifierOperand, Diagnostic> {
Ok(match lvalue {
Pattern::Identifier(lvalue) => {
let identifier = lower_identifier_for_assignment(env, builder, kind, lvalue)?;
builder.push(InstructionValue::StoreLocal(react_hir::StoreLocal {
lvalue: LValue { identifier, kind },
value,
}))
}
Pattern::ArrayPattern(lvalue) => {
let mut items = Vec::with_capacity(lvalue.elements.len());
let mut followups: Vec<(Identifier, &Pattern)> = Vec::new();
for element in &lvalue.elements {
match element {
None => items.push(ArrayDestructureItem::Hole),
Some(Pattern::Identifier(element)) => {
let identifier =
lower_identifier_for_assignment(env, builder, kind, element)?;
items.push(ArrayDestructureItem::Value(identifier));
}
Some(Pattern::RestElement(element)) => {
if let Pattern::Identifier(element) = &element.argument {
let identifier = lower_identifier_for_assignment(
env,
builder,
kind,
element.as_ref(),
)?;
items.push(ArrayDestructureItem::Spread(identifier));
} else {
let temporary = env.new_temporary();
items.push(ArrayDestructureItem::Spread(IdentifierOperand {
identifier: temporary.clone(),
effect: None,
}));
followups.push((temporary, &element.argument));
}
}
Some(element) => {
let temporary = env.new_temporary();
items.push(ArrayDestructureItem::Value(IdentifierOperand {
identifier: temporary.clone(),
effect: None,
}));
followups.push((temporary, element));
}
}
}
let temporary = builder.push(InstructionValue::Destructure(Destructure {
kind,
pattern: DestructurePattern::Array(items),
value,
}));
for (temporary, pattern) in followups {
lower_assignment_pattern(
env,
builder,
kind,
pattern,
IdentifierOperand {
identifier: temporary,
effect: None,
},
)?;
}
temporary
}
Pattern::ObjectPattern(lvalue) => {
let mut properties = Vec::with_capacity(lvalue.properties.len());
let mut followups: Vec<(Identifier, &Pattern)> = Vec::new();
for property in &lvalue.properties {
match property {
AssignmentPropertyOrRestElement::RestElement(property) => {
if let Pattern::Identifier(element) = &property.argument {
let identifier = lower_identifier_for_assignment(
env,
builder,
kind,
element.as_ref(),
)?;
properties.push(ObjectDestructureItem::Spread(identifier));
} else {
let temporary = env.new_temporary();
properties.push(ObjectDestructureItem::Spread(IdentifierOperand {
identifier: temporary.clone(),
effect: None,
}));
followups.push((temporary, &property.argument));
}
}
AssignmentPropertyOrRestElement::AssignmentProperty(property) => {
if property.is_computed {
return Err(Diagnostic::todo(
"Handle computed properties in ObjectPattern",
property.range,
));
}
let key = if let Expression::Identifier(key) = &property.key {
key.name.as_str()
} else {
return Err(Diagnostic::todo(
"Support non-identifier object keys in non-computed ObjectPattern",
property.range,
));
};
if let Pattern::Identifier(value) = &property.value {
let value = lower_identifier_for_assignment(env, builder, kind, value)?;
properties.push(ObjectDestructureItem::Property(
ObjectDestructureProperty {
name: key.to_string(),
value,
},
));
} else {
let temporary = env.new_temporary();
properties.push(ObjectDestructureItem::Property(
ObjectDestructureProperty {
name: key.to_string(),
value: IdentifierOperand {
identifier: temporary.clone(),
effect: None,
},
},
));
followups.push((temporary, &property.value));
}
}
}
}
let temporary = builder.push(InstructionValue::Destructure(Destructure {
kind,
pattern: DestructurePattern::Object(properties),
value,
}));
for (temporary, pattern) in followups {
lower_assignment_pattern(
env,
builder,
kind,
pattern,
IdentifierOperand {
identifier: temporary,
effect: None,
},
)?;
}
temporary
}
_ => todo!("lower assignment pattern for {:#?}", lvalue),
})
}
fn lower_identifier_for_assignment(
env: &Environment,
_builder: &mut Builder,
kind: InstructionKind,
node: &react_estree::Identifier,
) -> Result<IdentifierOperand, Diagnostic> {
match kind {
InstructionKind::Reassign => {
let identifier = env.resolve_variable_reference(node);
if let Some(identifier) = identifier {
Ok(IdentifierOperand {
identifier,
effect: None,
})
} else {
// Reassigning a global
Err(
Diagnostic::invalid_react(BuildHIRError::ReassignedGlobal, node.range)
.annotate(format!("Cannot reassign `{}`", &node.name), node.range),
)
}
}
_ => {
// Declaration
let identifier = env.resolve_variable_declaration(node, &node.name).unwrap();
Ok(IdentifierOperand {
identifier,
effect: None,
})
}
}
}

View File

@@ -0,0 +1,322 @@
/*
* 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.
*/
use std::cell::RefCell;
use std::rc::Rc;
use react_diagnostics::Diagnostic;
use react_hir::{
initialize_hir, BasicBlock, BlockId, BlockKind, Blocks, Environment, GotoKind, IdentifierData,
IdentifierOperand, InstrIx, Instruction, InstructionIdGenerator, InstructionValue, Terminal,
TerminalValue, Type, HIR,
};
use crate::BuildHIRError;
/// Helper struct used when converting from ESTree to HIR. Includes:
/// - Variable resolution
/// - Label resolution (for labeled statements and break/continue)
/// - Access to the environment
///
/// As well as representing the incomplete form of the HIR. Usage
/// generally involves driving calls to enter/exit blocks, resolve
/// labels and variables, and then calling `build()` when the HIR
/// is complete.
pub(crate) struct Builder<'e> {
#[allow(dead_code)]
environment: &'e Environment,
completed: Blocks,
instructions: Vec<Instruction>,
entry: BlockId,
wip: WipBlock,
id_gen: InstructionIdGenerator,
scopes: Vec<ControlFlowScope>,
}
pub(crate) struct WipBlock {
pub id: BlockId,
pub kind: BlockKind,
pub instructions: Vec<InstrIx>,
}
#[derive(Clone, PartialEq, Eq, Debug)]
enum ControlFlowScope {
Loop(LoopScope),
// Switch(SwitchScope),
#[allow(dead_code)]
Label(LabelScope),
}
#[derive(Clone, PartialEq, Eq, Debug)]
pub(crate) struct LoopScope {
pub label: Option<String>,
pub continue_block: BlockId,
pub break_block: BlockId,
}
#[derive(Clone, PartialEq, Eq, Debug)]
pub(crate) struct LabelScope {
pub label: String,
pub block: BlockId,
}
impl ControlFlowScope {
fn label(&self) -> Option<&String> {
match self {
Self::Loop(scope) => scope.label.as_ref(),
Self::Label(scope) => Some(&scope.label),
}
}
fn break_block(&self) -> BlockId {
match self {
Self::Loop(scope) => scope.break_block,
Self::Label(scope) => scope.block,
}
}
}
impl<'e> Builder<'e> {
pub(crate) fn new(environment: &'e Environment) -> Self {
let entry = environment.next_block_id();
let current = WipBlock {
id: entry,
kind: BlockKind::Block,
instructions: Default::default(),
};
Self {
environment,
completed: Default::default(),
instructions: Default::default(),
entry,
wip: current,
id_gen: InstructionIdGenerator::new(),
scopes: Default::default(),
}
}
/// Completes the builder and returns the HIR if it was valid,
/// or a Diagnostic if a validation error occured.
///
/// TODO: refine the type, only invariants should be possible here,
/// not other types of errors
pub(crate) fn build(self) -> Result<HIR, Diagnostic> {
let mut hir = HIR {
entry: self.entry,
blocks: self.completed,
instructions: self.instructions,
};
// Run all the initialization passes
initialize_hir(&mut hir)?;
Ok(hir)
}
/// Adds a new instruction to the end of the work in progress block
pub(crate) fn push(&mut self, value: InstructionValue) -> IdentifierOperand {
let lvalue = IdentifierOperand {
identifier: self.environment.new_temporary(),
effect: None,
};
let instr = Instruction {
id: self.id_gen.next(),
lvalue: lvalue.clone(),
value,
};
let ix = InstrIx::new(self.instructions.len() as u32);
self.instructions.push(instr);
self.wip.instructions.push(ix);
lvalue
}
/// Terminates the work in progress block with the given terminal, and starts a new
/// work in progress block with the given kind
pub(crate) fn terminate(&mut self, terminal: TerminalValue, next_kind: BlockKind) {
let next_wip = WipBlock {
id: self.environment.next_block_id(),
kind: next_kind,
instructions: Default::default(),
};
self.terminate_with_fallthrough(terminal, next_wip)
}
pub(crate) fn terminate_with_fallthrough(
&mut self,
terminal: TerminalValue,
fallthrough: WipBlock,
) {
let prev_wip = std::mem::replace(&mut self.wip, fallthrough);
self.completed.insert(Box::new(BasicBlock {
id: prev_wip.id,
kind: prev_wip.kind,
instructions: prev_wip.instructions,
terminal: Terminal {
id: self.id_gen.next(),
value: terminal,
},
predecessors: Default::default(),
phis: Default::default(),
}));
}
pub(crate) fn reserve(&mut self, kind: BlockKind) -> WipBlock {
WipBlock {
id: self.environment.next_block_id(),
kind,
instructions: Default::default(),
}
}
pub(crate) fn enter<F>(&mut self, kind: BlockKind, f: F) -> Result<BlockId, Diagnostic>
where
F: FnOnce(&mut Self) -> Result<TerminalValue, Diagnostic>,
{
let wip = self.reserve(kind);
let id = wip.id;
self.enter_reserved(wip, f)?;
Ok(id)
}
fn enter_reserved<F>(&mut self, wip: WipBlock, f: F) -> Result<(), Diagnostic>
where
F: FnOnce(&mut Self) -> Result<TerminalValue, Diagnostic>,
{
let current = std::mem::replace(&mut self.wip, wip);
let (result, terminal) = match f(self) {
Ok(terminal) => (Ok(()), terminal),
Err(error) => (
Err(error),
// TODO: add a `Terminal::Error` variant
TerminalValue::Goto(react_hir::GotoTerminal {
block: current.id,
kind: GotoKind::Break,
}),
),
};
let completed = std::mem::replace(&mut self.wip, current);
self.completed.insert(Box::new(BasicBlock {
id: completed.id,
kind: completed.kind,
instructions: completed.instructions,
terminal: Terminal {
id: self.id_gen.next(),
value: terminal,
},
predecessors: Default::default(),
phis: Default::default(),
}));
result
}
pub(crate) fn enter_loop<F>(
&mut self,
scope: LoopScope,
f: F,
) -> Result<TerminalValue, Diagnostic>
where
F: FnOnce(&mut Self) -> Result<TerminalValue, Diagnostic>,
{
self.scopes.push(ControlFlowScope::Loop(scope.clone()));
let terminal = f(self);
let last = self.scopes.pop().unwrap();
assert_eq!(last, ControlFlowScope::Loop(scope));
terminal
}
/// Returns a new temporary identifier
/// This may be necessary for destructuring with default values. there
/// we synthesize a temporary identifier to store the possibly-missing value
/// into, and emit a later StoreLocal for the original identifier
#[allow(dead_code)]
pub(crate) fn make_temporary(&self) -> react_hir::Identifier {
react_hir::Identifier {
id: self.environment.next_identifier_id(),
name: None,
data: Rc::new(RefCell::new(IdentifierData {
mutable_range: Default::default(),
scope: None,
type_: Type::Var(self.environment.next_type_var_id()),
})),
}
}
/// Resolves the target for the given break label (if present), or returns the default
/// break target given the current context. Returns a diagnostic if the label is
/// provided but cannot be resolved.
pub(crate) fn resolve_break(
&self,
label: Option<&react_estree::Identifier>,
) -> Result<BlockId, Diagnostic> {
for scope in self.scopes.iter().rev() {
match (label, scope.label()) {
// If this is an unlabeled break, return the most recent break target
(None, _) => return Ok(scope.break_block()),
// If the break is labeled and matches the current scope, return its break target
(Some(label), Some(scope_label)) if &label.name == scope_label => {
return Ok(scope.break_block());
}
// Otherwise keep searching
_ => continue,
}
}
Err(Diagnostic::invalid_syntax(
BuildHIRError::UnresolvedBreakTarget,
None,
))
}
/// Resolves the target for the given continue label (if present), or returns the default
/// continue target given the current context. Returns a diagnostic if the label is
/// provided but cannot be resolved.
pub(crate) fn resolve_continue(
&self,
label: Option<&react_estree::Identifier>,
) -> Result<BlockId, Diagnostic> {
for scope in self.scopes.iter().rev() {
match scope {
ControlFlowScope::Loop(scope) => {
match (label, &scope.label) {
// If this is an unlabeled continue, return the first matching loop
(None, _) => return Ok(scope.continue_block),
// If the continue is labeled and matches the current scope, return its continue target
(Some(label), Some(scope_label))
if label.name.as_str() == scope_label.as_str() =>
{
return Ok(scope.continue_block);
}
// Otherwise keep searching
_ => continue,
}
}
_ => {
match (label, scope.label()) {
(Some(label), Some(scope_label)) if label.name.as_str() == scope_label => {
// Error, the continue referred to a label that is not a loop
return Err(Diagnostic::invalid_syntax(
BuildHIRError::ContinueTargetIsNotALoop,
None,
));
}
_ => continue,
}
}
}
}
Err(Diagnostic::invalid_syntax(
BuildHIRError::UnresolvedContinueTarget,
None,
))
}
}

View File

@@ -0,0 +1,44 @@
/*
* 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.
*/
use std::collections::HashSet;
use react_estree::IntoFunction;
use react_hir::Environment;
use react_semantic_analysis::{DeclarationId, ScopeView};
pub(crate) fn get_context_identifiers<T: IntoFunction>(
env: &Environment,
node: &T,
) -> Vec<DeclarationId> {
let function_scope = env.scope(node.function()).unwrap();
let mut free = FreeVariables::default();
let mut seen = HashSet::new();
populate_free_variable_references(&mut free, &mut seen, function_scope);
free
}
type FreeVariables = Vec<DeclarationId>;
fn populate_free_variable_references(
free: &mut FreeVariables,
seen: &mut HashSet<DeclarationId>,
scope: ScopeView<'_>,
) {
for reference in scope.references() {
if !seen.insert(reference.declaration().id()) {
continue;
}
let declaration_scope = reference.declaration().scope();
if !declaration_scope.is_descendant_of(scope) {
free.push(reference.declaration().id())
}
}
for child in scope.children() {
populate_free_variable_references(free, seen, child);
}
}

View File

@@ -0,0 +1,63 @@
/*
* 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.
*/
use thiserror::Error;
/// Errors which can occur during HIR construction
#[derive(Error, Debug)]
pub enum BuildHIRError {
/// ErrorSeverity::Unsupported
#[error(
"Variable declarations must be `let` or `const`, `var` declarations are not supported"
)]
VariableDeclarationKindIsVar,
/// ErrorSeverity::Invariant
#[error("Invariant: Expected variable declaration to declare a fresh binding")]
VariableDeclarationBindingIsNonLocal,
/// ErrorSeverity::Todo
#[error("`for` statements must have an initializer, eg `for (**let i = 0**; ...)`")]
ForStatementIsMissingInitializer,
/// ErrorSeverity::Todo
#[error(
"`for` statements must have a test condition, eg `for (let i = 0; **i < count**; ...)`"
)]
ForStatementIsMissingTest,
/// ErrorSeverity::Invariant
#[error("Invariant: Expected an expression node")]
NonExpressionInExpressionPosition,
/// ErrorSeverity::InvalidReact
#[error("React functions may not reassign variables defined outside of the component or hook")]
ReassignedGlobal,
/// ErrorSeverity::InvalidSyntax
#[error("Could not resolve a target for `break` statement")]
UnresolvedBreakTarget,
/// ErrorSeverity::InvalidSyntax
#[error("Could not resolve a target for `continue` statement")]
UnresolvedContinueTarget,
/// ErrorSeverity::InvalidSyntax
#[error("Labeled `continue` statements must use the label of a loop statement")]
ContinueTargetIsNotALoop,
/// ErrorSeverity::Invariant
#[error("Invariant: Identifier was not resolved (did name resolution run successfully?)")]
UnknownIdentifier,
/// ErrorSeverity::InvalidSyntax
#[error("Expected function to have a body")]
EmptyFunction,
#[error("`super` is not suppported")]
UnsupportedSuperExpression,
}

View File

@@ -0,0 +1,14 @@
/*
* 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.
*/
mod build;
mod builder;
mod context;
mod error;
pub use build::build;
pub use error::*;

View File

@@ -0,0 +1,23 @@
[package]
name = "react_diagnostics"
version = "0.1.0"
publish = false
authors.workspace = true
description.workspace = true
edition.workspace = true
homepage.workspace = true
keywords.workspace = true
license.workspace = true
repository.workspace = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
# TODO: extract SourceRange into a separate crate so that
# we don't depend on full estree here
react_estree = { workspace = true }
# TODO: consider extracting a separate react_miette crate which does
# the translation from react_diagnostics::Diagnostic to miette::Diagnostic
miette = { workspace = true }
thiserror = { workspace = true }
static_assertions = { workspace = true }

View File

@@ -0,0 +1,10 @@
# react_diagnostics
Types for representing compiler diagnostics. Includes a general-purpose representation
of diagnostics with related information which can be converted into `miette::Diagnostic` to exploit miette's pretty printing of errors.
Unlike miette, lsp_types, and other diagnostic libraries, the error severities match
React Compiler's semantics. The intent is that a given diagnostic may be displayed as
an error, warning, or not displayed at all depending on the context in which the
compiler is being used. For example, an ESLint plugin powered by React Compiler may ignore
InvalidSyntax diagnostics, whereas the regular compiler may report them as errors.

View File

@@ -0,0 +1,285 @@
/*
* 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.
*/
use std::error::Error;
use std::fmt::{Debug, Display, Write};
use miette::SourceSpan;
use react_estree::SourceRange;
use static_assertions::assert_impl_all;
use thiserror::Error;
pub type Diagnostics = Vec<Diagnostic>;
pub type DiagnosticsResult<T> = Result<T, Diagnostics>;
#[derive(Debug)]
pub struct WithDiagnostics<T> {
pub item: T,
pub diagnostics: Vec<Diagnostic>,
}
impl<T> From<WithDiagnostics<T>> for Result<T, Diagnostics> {
fn from(s: WithDiagnostics<T>) -> Result<T, Diagnostics> {
if s.diagnostics.is_empty() {
Ok(s.item)
} else {
Err(s.diagnostics)
}
}
}
pub fn diagnostics_result<T>(result: T, diagnostics: Diagnostics) -> DiagnosticsResult<T> {
if diagnostics.is_empty() {
Ok(result)
} else {
Err(diagnostics)
}
}
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Error)]
pub enum DiagnosticSeverity {
/// A feature that is intended to work but not yet implemented
#[error("Not implemented")]
Todo,
/// Syntax that is valid but intentionally not supported
#[error("Unsupported")]
Unsupported,
/// Invalid syntax
#[error("Invalid JavaScript")]
InvalidSyntax,
/// Valid syntax, but invalid React
#[error("Invalid React")]
InvalidReact,
/// Internal compiler error (ICE)
#[error("Internal error")]
Invariant,
}
/// A diagnostic message as a result of validating some code. This struct is
/// modeled after the LSP Diagnostic type:
/// https://microsoft.github.io/language-server-protocol/specification#diagnostic
///
/// Changes from LSP:
/// - `location` is different from LSP in that it's a file + span instead of
/// just a span.
/// - Unused fields are omitted.
/// - Severity is a custom enum that represents React-specific categories of error.
/// The translation to an LSP error/warning/etc depends on compiler settings and
/// invocation context.
#[derive(Debug)]
pub struct Diagnostic(Box<DiagnosticData>);
impl Diagnostic {
fn with_severity<T: 'static + DiagnosticDisplay>(
severity: DiagnosticSeverity,
message: T,
range: Option<SourceRange>,
) -> Self {
Self(Box::new(DiagnosticData {
message: Box::new(message),
span: range.map(source_span_from_range),
related_information: Vec::new(),
severity,
data: Vec::new(),
}))
}
/// Creates a new Todo Diagnostic.
/// Additional locations can be added with the `.annotate()` function.
pub fn todo<T: 'static + DiagnosticDisplay>(message: T, range: Option<SourceRange>) -> Self {
Diagnostic::with_severity(DiagnosticSeverity::Todo, message, range)
}
/// Creates a new Unsupported Diagnostic.
/// Additional locations can be added with the `.annotate()` function.
pub fn unsupported<T: 'static + DiagnosticDisplay>(
message: T,
range: Option<SourceRange>,
) -> Self {
Diagnostic::with_severity(DiagnosticSeverity::Unsupported, message, range)
}
/// Creates a new InvalidSyntax Diagnostic.
/// Additional locations can be added with the `.annotate()` function.
pub fn invalid_syntax<T: 'static + DiagnosticDisplay>(
message: T,
range: Option<SourceRange>,
) -> Self {
Diagnostic::with_severity(DiagnosticSeverity::InvalidSyntax, message, range)
}
/// Creates a new InvalidReact Diagnostic.
/// Additional locations can be added with the `.annotate()` function.
pub fn invalid_react<T: 'static + DiagnosticDisplay>(
message: T,
range: Option<SourceRange>,
) -> Self {
Diagnostic::with_severity(DiagnosticSeverity::InvalidReact, message, range)
}
/// Creates a new InvalidReact Diagnostic.
/// Additional locations can be added with the `.annotate()` function.
pub fn invariant<T: 'static + DiagnosticDisplay>(
message: T,
range: Option<SourceRange>,
) -> Self {
Diagnostic::with_severity(DiagnosticSeverity::Invariant, message, range)
}
/// Annotates this error with an additional location and associated message.
pub fn annotate<T: 'static + DiagnosticDisplay>(
mut self,
message: T,
range: Option<SourceRange>,
) -> Self {
self.0
.related_information
.push(DiagnosticRelatedInformation {
message: Box::new(message),
span: range.map(source_span_from_range),
});
self
}
pub fn message(&self) -> &impl DiagnosticDisplay {
&self.0.message
}
pub fn span(&self) -> Option<SourceSpan> {
self.0.span
}
pub fn get_data(&self) -> &[impl DiagnosticDisplay] {
&self.0.data
}
pub fn severity(&self) -> DiagnosticSeverity {
self.0.severity
}
pub fn related_information(&self) -> &[DiagnosticRelatedInformation] {
&self.0.related_information
}
pub fn print_without_source(&self) -> String {
let mut result = String::new();
writeln!(
result,
"{message}:{span:?}",
message = &self.0.message,
span = self.0.span
)
.unwrap();
if !self.0.related_information.is_empty() {
for (ix, related) in self.0.related_information.iter().enumerate() {
writeln!(
result,
"[related {ix}] {message}:{span:?}",
ix = ix + 1,
message = related.message,
span = related.span
)
.unwrap();
}
};
result
}
}
impl Display for Diagnostic {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0.message)
}
}
impl Error for Diagnostic {}
impl miette::Diagnostic for Diagnostic {
fn help<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
Some(Box::new(self.0.message.to_string()))
}
fn labels(&self) -> Option<Box<dyn Iterator<Item = miette::LabeledSpan> + '_>> {
let related_items = &self.0.related_information;
let mut spans: Vec<miette::LabeledSpan> = Vec::new();
for related in related_items {
if let Some(span) = related.span {
spans.push(miette::LabeledSpan::new_with_span(
Some(related.message.to_string()),
span,
))
}
}
if spans.is_empty() {
if let Some(span) = self.0.span {
spans.push(miette::LabeledSpan::new_with_span(
Some(self.0.message.to_string()),
span,
))
}
}
Some(Box::new(spans.into_iter()))
}
}
// Ensure Diagnostic is thread-safe
assert_impl_all!(Diagnostic: Send, Sync);
#[derive(Debug)]
struct DiagnosticData {
/// Human readable error message.
message: Box<dyn DiagnosticDisplay>,
/// The primary location of this diagnostic.
span: Option<SourceSpan>,
/// Related diagnostic information, such as other definitions in the case of
/// a duplicate definition error.
related_information: Vec<DiagnosticRelatedInformation>,
severity: DiagnosticSeverity,
/// A list with data that can be passed to the code actions
/// `data` is used in the LSP protocol:
/// @see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#diagnostic
data: Vec<Box<dyn DiagnosticDisplay>>,
}
/// Secondary locations attached to a diagnostic.
#[derive(Debug)]
pub struct DiagnosticRelatedInformation {
/// The message of this related diagnostic information.
pub message: Box<dyn DiagnosticDisplay>,
/// The location of this related diagnostic information.
pub span: Option<SourceSpan>,
}
/// Trait for diagnostic messages to allow structs that capture
/// some data and can lazily convert it to a message.
pub trait DiagnosticDisplay: Debug + Display + Send + Sync {}
/// Automatically implement the trait if constraints are met, so that
/// implementors don't need to.
impl<T> DiagnosticDisplay for T where T: Debug + Display + Send + Sync {}
impl From<Diagnostic> for Diagnostics {
fn from(diagnostic: Diagnostic) -> Self {
vec![diagnostic]
}
}
fn source_span_from_range(range: SourceRange) -> SourceSpan {
SourceSpan::new(
(range.start as usize).into(),
((u32::from(range.end) - range.start) as usize).into(),
)
}

View File

@@ -0,0 +1,19 @@
/*
* 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.
*/
mod diagnostic;
pub use diagnostic::*;
/// Returns Ok(()) if the condition is true, otherwise returns Err()
/// with the diagnostic produced by the provided callback
pub fn invariant<F>(cond: bool, f: F) -> Result<(), Diagnostic>
where
F: Fn() -> Diagnostic,
{
if cond { Ok(()) } else { Err(f()) }
}

View File

@@ -0,0 +1,22 @@
[package]
name = "react_estree"
version = "0.1.0"
publish = false
authors.workspace = true
description.workspace = true
edition.workspace = true
homepage.workspace = true
keywords.workspace = true
license.workspace = true
repository.workspace = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
insta = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
static_assertions = { workspace = true }
[build-dependencies]
react_estree_codegen = { workspace = true }

View File

@@ -0,0 +1,17 @@
# react_estree
This crate is a Rust representation of the [ESTree format](https://github.com/estree/estree/tree/master) and
popular extenions including JSX and (eventually) Flow and TypeScript.
This crate is intended as the main interchange format with outside code. A typical integration with React Compiler
will look as follows:
1. Host Compiler parses into the host AST format.
2. Host Compiler converts into `react_estree`.
3. Host Compiler invokes React Compiler to compile the input, which (conceptually)
returns the resulting code in `react_estree` format.
4. Host Compiler convert back from `react_estree` to its host AST format.
Because React Compiler is intended to support JavaScript-based toolchains, `react_estree` is designed to support
accurate serialization to/from estree-compatible JSON. We may also support the Babel AST format
(a variant of ESTree) as well, depending on demand.

View File

@@ -0,0 +1,31 @@
/*
* 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.
*/
use react_estree_codegen::estree;
// Example custom build script.
fn main() {
// Re-run if the codegen files change
println!("cargo:rerun-if-changed=../react_estree_codegen/src/codegen.rs");
println!("cargo:rerun-if-changed=../react_estree_codegen/src/lib.rs");
println!("cargo:rerun-if-changed=../react_estree_codegen/src/ecmascript.json");
println!("cargo:rerun-if-changed=../react_estree_codegen");
let src = estree();
let copyright = "
/*
* 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.
*/
"
.to_string();
let trimmed_copyright = copyright.trim();
let contents = format!("{trimmed_copyright}\n{src}");
std::fs::write("src/generated.rs", contents).unwrap();
}

View File

@@ -0,0 +1,30 @@
/*
* 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.
*/
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub enum Binding {
Global,
Module(BindingId),
Local(BindingId),
}
#[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub struct BindingId(u32);
impl BindingId {
pub fn new(value: u32) -> Self {
Self(value)
}
}
impl From<BindingId> for u32 {
fn from(value: BindingId) -> Self {
value.0
}
}

View File

@@ -0,0 +1,528 @@
{
"type": "Program",
"loc": {
"source": null,
"start": {
"line": 1,
"column": 0
},
"end": {
"line": 7,
"column": 1
}
},
"body": [
{
"type": "FunctionDeclaration",
"loc": {
"source": null,
"start": {
"line": 1,
"column": 0
},
"end": {
"line": 7,
"column": 1
}
},
"id": {
"type": "Identifier",
"loc": {
"source": null,
"start": {
"line": 1,
"column": 9
},
"end": {
"line": 1,
"column": 18
}
},
"name": "Component",
"typeAnnotation": null,
"optional": false,
"range": [
9,
18
]
},
"params": [
{
"type": "Identifier",
"loc": {
"source": null,
"start": {
"line": 1,
"column": 19
},
"end": {
"line": 1,
"column": 24
}
},
"name": "props",
"typeAnnotation": null,
"optional": false,
"range": [
19,
24
]
}
],
"body": {
"type": "BlockStatement",
"loc": {
"source": null,
"start": {
"line": 1,
"column": 26
},
"end": {
"line": 7,
"column": 1
}
},
"body": [
{
"type": "VariableDeclaration",
"loc": {
"source": null,
"start": {
"line": 2,
"column": 2
},
"end": {
"line": 2,
"column": 12
}
},
"kind": "let",
"declarations": [
{
"type": "VariableDeclarator",
"loc": {
"source": null,
"start": {
"line": 2,
"column": 6
},
"end": {
"line": 2,
"column": 11
}
},
"init": {
"type": "Literal",
"loc": {
"source": null,
"start": {
"line": 2,
"column": 10
},
"end": {
"line": 2,
"column": 11
}
},
"value": 0,
"range": [
38,
39
],
"raw": "0"
},
"id": {
"type": "Identifier",
"loc": {
"source": null,
"start": {
"line": 2,
"column": 6
},
"end": {
"line": 2,
"column": 7
}
},
"name": "x",
"typeAnnotation": null,
"optional": false,
"range": [
34,
35
]
},
"range": [
34,
39
]
}
],
"range": [
30,
40
]
},
{
"type": "ForStatement",
"loc": {
"source": null,
"start": {
"line": 3,
"column": 2
},
"end": {
"line": 5,
"column": 3
}
},
"init": {
"type": "VariableDeclaration",
"loc": {
"source": null,
"start": {
"line": 3,
"column": 7
},
"end": {
"line": 3,
"column": 16
}
},
"kind": "let",
"declarations": [
{
"type": "VariableDeclarator",
"loc": {
"source": null,
"start": {
"line": 3,
"column": 11
},
"end": {
"line": 3,
"column": 16
}
},
"init": {
"type": "Literal",
"loc": {
"source": null,
"start": {
"line": 3,
"column": 15
},
"end": {
"line": 3,
"column": 16
}
},
"value": 0,
"range": [
56,
57
],
"raw": "0"
},
"id": {
"type": "Identifier",
"loc": {
"source": null,
"start": {
"line": 3,
"column": 11
},
"end": {
"line": 3,
"column": 12
}
},
"name": "i",
"typeAnnotation": null,
"optional": false,
"range": [
52,
53
]
},
"range": [
52,
57
]
}
],
"range": [
48,
57
]
},
"test": {
"type": "BinaryExpression",
"loc": {
"source": null,
"start": {
"line": 3,
"column": 18
},
"end": {
"line": 3,
"column": 24
}
},
"left": {
"type": "Identifier",
"loc": {
"source": null,
"start": {
"line": 3,
"column": 18
},
"end": {
"line": 3,
"column": 19
}
},
"name": "i",
"typeAnnotation": null,
"optional": false,
"range": [
59,
60
]
},
"right": {
"type": "Literal",
"loc": {
"source": null,
"start": {
"line": 3,
"column": 22
},
"end": {
"line": 3,
"column": 24
}
},
"value": 10,
"range": [
63,
65
],
"raw": "10"
},
"operator": "<",
"range": [
59,
65
]
},
"update": {
"type": "UpdateExpression",
"loc": {
"source": null,
"start": {
"line": 3,
"column": 26
},
"end": {
"line": 3,
"column": 29
}
},
"operator": "++",
"argument": {
"type": "Identifier",
"loc": {
"source": null,
"start": {
"line": 3,
"column": 26
},
"end": {
"line": 3,
"column": 27
}
},
"name": "i",
"typeAnnotation": null,
"optional": false,
"range": [
67,
68
]
},
"prefix": false,
"range": [
67,
70
]
},
"body": {
"type": "BlockStatement",
"loc": {
"source": null,
"start": {
"line": 3,
"column": 31
},
"end": {
"line": 5,
"column": 3
}
},
"body": [
{
"type": "ExpressionStatement",
"loc": {
"source": null,
"start": {
"line": 4,
"column": 4
},
"end": {
"line": 4,
"column": 11
}
},
"expression": {
"type": "AssignmentExpression",
"loc": {
"source": null,
"start": {
"line": 4,
"column": 4
},
"end": {
"line": 4,
"column": 10
}
},
"operator": "+=",
"left": {
"type": "Identifier",
"loc": {
"source": null,
"start": {
"line": 4,
"column": 4
},
"end": {
"line": 4,
"column": 5
}
},
"name": "x",
"typeAnnotation": null,
"optional": false,
"range": [
78,
79
]
},
"right": {
"type": "Identifier",
"loc": {
"source": null,
"start": {
"line": 4,
"column": 9
},
"end": {
"line": 4,
"column": 10
}
},
"name": "i",
"typeAnnotation": null,
"optional": false,
"range": [
83,
84
]
},
"range": [
78,
84
]
},
"directive": null,
"range": [
78,
85
]
}
],
"range": [
72,
89
]
},
"range": [
43,
89
]
},
{
"type": "ReturnStatement",
"loc": {
"source": null,
"start": {
"line": 6,
"column": 2
},
"end": {
"line": 6,
"column": 11
}
},
"argument": {
"type": "Identifier",
"loc": {
"source": null,
"start": {
"line": 6,
"column": 9
},
"end": {
"line": 6,
"column": 10
}
},
"name": "x",
"typeAnnotation": null,
"optional": false,
"range": [
99,
100
]
},
"range": [
92,
101
]
}
],
"range": [
26,
103
]
},
"typeParameters": null,
"returnType": null,
"predicate": null,
"generator": false,
"async": false,
"range": [
0,
103
]
}
],
"comments": [],
"interpreter": null,
"range": [
0,
103
],
"sourceType": "script"
}

View File

@@ -0,0 +1,104 @@
{
"type": "Program",
"loc": {
"source": null,
"start": {
"line": 1,
"column": 0
},
"end": {
"line": 1,
"column": 26
}
},
"body": [
{
"type": "ImportDeclaration",
"loc": {
"source": null,
"start": {
"line": 1,
"column": 0
},
"end": {
"line": 1,
"column": 26
}
},
"specifiers": [
{
"type": "ImportDefaultSpecifier",
"loc": {
"source": null,
"start": {
"line": 1,
"column": 7
},
"end": {
"line": 1,
"column": 12
}
},
"local": {
"type": "Identifier",
"loc": {
"source": null,
"start": {
"line": 1,
"column": 7
},
"end": {
"line": 1,
"column": 12
}
},
"name": "React",
"typeAnnotation": null,
"optional": false,
"range": [
7,
12
]
},
"range": [
7,
12
]
}
],
"source": {
"type": "Literal",
"loc": {
"source": null,
"start": {
"line": 1,
"column": 18
},
"end": {
"line": 1,
"column": 25
}
},
"value": "react",
"range": [
18,
25
],
"raw": "'react'"
},
"attributes": [],
"importKind": "value",
"range": [
0,
26
]
}
],
"comments": [],
"interpreter": null,
"range": [
0,
26
],
"sourceType": "module"
}

View File

@@ -0,0 +1,189 @@
{
"type": "Program",
"loc": {
"source": null,
"start": {
"line": 1,
"column": 0
},
"end": {
"line": 3,
"column": 1
}
},
"range": [
0,
51
],
"body": [
{
"type": "FunctionDeclaration",
"loc": {
"source": null,
"start": {
"line": 1,
"column": 0
},
"end": {
"line": 3,
"column": 1
}
},
"range": [
0,
51
],
"id": {
"type": "Identifier",
"loc": {
"source": null,
"start": {
"line": 1,
"column": 9
},
"end": {
"line": 1,
"column": 18
}
},
"range": [
9,
18
],
"name": "Component",
"typeAnnotation": null,
"optional": false
},
"params": [
{
"type": "Identifier",
"loc": {
"source": null,
"start": {
"line": 1,
"column": 19
},
"end": {
"line": 1,
"column": 24
}
},
"range": [
19,
24
],
"name": "props",
"typeAnnotation": null,
"optional": false
}
],
"body": {
"type": "BlockStatement",
"loc": {
"source": null,
"start": {
"line": 1,
"column": 26
},
"end": {
"line": 3,
"column": 1
}
},
"range": [
26,
51
],
"body": [
{
"type": "ReturnStatement",
"loc": {
"source": null,
"start": {
"line": 2,
"column": 2
},
"end": {
"line": 2,
"column": 21
}
},
"range": [
30,
49
],
"argument": {
"type": "MemberExpression",
"loc": {
"source": null,
"start": {
"line": 2,
"column": 9
},
"end": {
"line": 2,
"column": 20
}
},
"range": [
37,
48
],
"object": {
"type": "Identifier",
"loc": {
"source": null,
"start": {
"line": 2,
"column": 9
},
"end": {
"line": 2,
"column": 14
}
},
"range": [
37,
42
],
"name": "props",
"typeAnnotation": null,
"optional": false
},
"property": {
"type": "Identifier",
"loc": {
"source": null,
"start": {
"line": 2,
"column": 15
},
"end": {
"line": 2,
"column": 20
}
},
"range": [
43,
48
],
"name": "value",
"typeAnnotation": null,
"optional": false
},
"computed": false
}
}
]
},
"async": false,
"generator": false,
"predicate": null,
"expression": false,
"returnType": null,
"typeParameters": null
}
],
"comments": [],
"errors": []
}

View File

@@ -0,0 +1,351 @@
{
"type": "Program",
"loc": {
"source": null,
"start": {
"line": 2,
"column": 0
},
"end": {
"line": 4,
"column": 1
}
},
"body": [
{
"type": "FunctionDeclaration",
"loc": {
"source": null,
"start": {
"line": 2,
"column": 0
},
"end": {
"line": 4,
"column": 1
}
},
"id": {
"type": "Identifier",
"loc": {
"source": null,
"start": {
"line": 2,
"column": 9
},
"end": {
"line": 2,
"column": 12
}
},
"name": "foo",
"typeAnnotation": null,
"optional": false,
"range": [
10,
13
]
},
"params": [],
"body": {
"type": "BlockStatement",
"loc": {
"source": null,
"start": {
"line": 2,
"column": 15
},
"end": {
"line": 4,
"column": 1
}
},
"body": [
{
"type": "ReturnStatement",
"loc": {
"source": null,
"start": {
"line": 3,
"column": 2
},
"end": {
"line": 3,
"column": 36
}
},
"argument": {
"type": "JSXElement",
"loc": {
"source": null,
"start": {
"line": 3,
"column": 9
},
"end": {
"line": 3,
"column": 36
}
},
"openingElement": {
"type": "JSXOpeningElement",
"loc": {
"source": null,
"start": {
"line": 3,
"column": 9
},
"end": {
"line": 3,
"column": 26
}
},
"name": {
"type": "JSXMemberExpression",
"loc": {
"source": null,
"start": {
"line": 3,
"column": 10
},
"end": {
"line": 3,
"column": 17
}
},
"object": {
"type": "JSXIdentifier",
"loc": {
"source": null,
"start": {
"line": 3,
"column": 10
},
"end": {
"line": 3,
"column": 13
}
},
"name": "Foo",
"range": [
28,
31
]
},
"property": {
"type": "JSXIdentifier",
"loc": {
"source": null,
"start": {
"line": 3,
"column": 14
},
"end": {
"line": 3,
"column": 17
}
},
"name": "Bar",
"range": [
32,
35
]
},
"range": [
28,
35
]
},
"attributes": [
{
"type": "JSXAttribute",
"loc": {
"source": null,
"start": {
"line": 3,
"column": 18
},
"end": {
"line": 3,
"column": 24
}
},
"name": {
"type": "JSXIdentifier",
"loc": {
"source": null,
"start": {
"line": 3,
"column": 18
},
"end": {
"line": 3,
"column": 19
}
},
"name": "a",
"range": [
36,
37
]
},
"value": {
"type": "JSXExpressionContainer",
"loc": {
"source": null,
"start": {
"line": 3,
"column": 20
},
"end": {
"line": 3,
"column": 24
}
},
"expression": {
"type": "Literal",
"loc": {
"source": null,
"start": {
"line": 3,
"column": 21
},
"end": {
"line": 3,
"column": 23
}
},
"value": 10,
"range": [
39,
41
],
"raw": "10"
},
"range": [
38,
42
]
},
"range": [
36,
42
]
}
],
"selfClosing": false,
"range": [
27,
44
]
},
"children": [],
"closingElement": {
"type": "JSXClosingElement",
"loc": {
"source": null,
"start": {
"line": 3,
"column": 26
},
"end": {
"line": 3,
"column": 36
}
},
"name": {
"type": "JSXMemberExpression",
"loc": {
"source": null,
"start": {
"line": 3,
"column": 28
},
"end": {
"line": 3,
"column": 35
}
},
"object": {
"type": "JSXIdentifier",
"loc": {
"source": null,
"start": {
"line": 3,
"column": 28
},
"end": {
"line": 3,
"column": 31
}
},
"name": "Foo",
"range": [
46,
49
]
},
"property": {
"type": "JSXIdentifier",
"loc": {
"source": null,
"start": {
"line": 3,
"column": 32
},
"end": {
"line": 3,
"column": 35
}
},
"name": "Bar",
"range": [
50,
53
]
},
"range": [
46,
53
]
},
"range": [
44,
54
]
},
"range": [
27,
54
]
},
"range": [
20,
54
]
}
],
"range": [
16,
56
]
},
"typeParameters": null,
"returnType": null,
"predicate": null,
"generator": false,
"async": false,
"range": [
1,
56
]
}
],
"comments": [],
"interpreter": null,
"range": [
1,
56
],
"sourceType": "script"
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,149 @@
/*
* 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.
*/
// Manual extensions to generated types
use crate::{
ArrowFunctionExpression, Class, ClassDeclaration, ClassExpression, Function,
FunctionDeclaration, FunctionExpression, ImportDeclarationSpecifier, JSXElementName,
JSXMemberExpression, JSXMemberExpressionOrIdentifier, Pattern, SourceRange, SourceType,
};
/// Sentinel trait to distinguish AST *node* types
pub trait ESTreeNode {}
impl Default for SourceType {
fn default() -> Self {
Self::Module
}
}
impl Pattern {
pub fn range(&self) -> Option<SourceRange> {
match self {
Self::ArrayPattern(pattern) => pattern.range,
Self::AssignmentPattern(pattern) => pattern.range,
Self::Identifier(pattern) => pattern.range,
Self::ObjectPattern(pattern) => pattern.range,
Self::RestElement(pattern) => pattern.range,
}
}
}
impl ImportDeclarationSpecifier {
pub fn range(&self) -> Option<SourceRange> {
match self {
Self::ImportDefaultSpecifier(specifier) => specifier.range,
Self::ImportNamespaceSpecifier(specifier) => specifier.range,
Self::ImportSpecifier(specifier) => specifier.range,
}
}
}
impl JSXElementName {
pub fn root_name(&self) -> &str {
match self {
Self::JSXIdentifier(name) => &name.name,
Self::JSXMemberExpression(name) => name.root_name(),
Self::JSXNamespacedName(name) => &name.namespace.name,
}
}
}
impl JSXMemberExpression {
pub fn root_name(&self) -> &str {
match &self.object {
JSXMemberExpressionOrIdentifier::JSXMemberExpression(object) => object.root_name(),
JSXMemberExpressionOrIdentifier::JSXIdentifier(object) => &object.name,
}
}
}
pub trait IntoFunction: ESTreeNode {
fn function(&self) -> &Function;
fn into_function(self) -> Function;
}
impl IntoFunction for FunctionDeclaration {
fn function(&self) -> &Function {
&self.function
}
fn into_function(self) -> Function {
self.function
}
}
impl IntoFunction for FunctionExpression {
fn function(&self) -> &Function {
&self.function
}
fn into_function(self) -> Function {
self.function
}
}
impl IntoFunction for ArrowFunctionExpression {
fn function(&self) -> &Function {
&self.function
}
fn into_function(self) -> Function {
self.function
}
}
impl ESTreeNode for Function {}
impl IntoFunction for Function {
fn function(&self) -> &Function {
self
}
fn into_function(self) -> Function {
self
}
}
pub trait IntoClass: ESTreeNode {
fn class(&self) -> &Class;
fn into_class(self) -> Class;
}
impl IntoClass for ClassDeclaration {
fn class(&self) -> &Class {
&self.class
}
fn into_class(self) -> Class {
self.class
}
}
impl IntoClass for ClassExpression {
fn class(&self) -> &Class {
&self.class
}
fn into_class(self) -> Class {
self.class
}
}
impl ESTreeNode for Class {}
impl IntoClass for Class {
fn class(&self) -> &Class {
self
}
fn into_class(self) -> Class {
self
}
}

View File

@@ -0,0 +1,295 @@
/*
* 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.
*/
use serde::de::Visitor;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum JsValue {
Boolean(bool),
Null,
Number(Number),
String(String),
Undefined,
}
impl JsValue {
pub fn is_truthy(&self) -> bool {
match &self {
JsValue::Boolean(value) => *value,
JsValue::Number(value) => value.is_truthy(),
JsValue::String(value) => !value.is_empty(),
JsValue::Null => false,
JsValue::Undefined => false,
}
}
// Partial implementation of loose equality for javascript, returns Some for supported
// cases w the equality result, and None for unsupported cases
pub fn loosely_equals(&self, other: &Self) -> Option<bool> {
// https://tc39.es/ecma262/multipage/abstract-operations.html#sec-islooselyequal
match (&self, &other) {
// 1. If Type(x) is Type(y), then
// a. Return IsStrictlyEqual(x, y).
(JsValue::Number(left), JsValue::Number(right)) => Some(left.equals(*right)),
(JsValue::Null, JsValue::Null) => Some(true),
(JsValue::Undefined, JsValue::Undefined) => Some(true),
(JsValue::Boolean(left), JsValue::Boolean(right)) => Some(left == right),
(JsValue::String(left), JsValue::String(right)) => Some(left == right),
// 2. If x is null and y is undefined, return true.
(JsValue::Null, JsValue::Undefined) => Some(true),
// 3. If x is undefined and y is null, return true.
(JsValue::Undefined, JsValue::Null) => Some(true),
_ => None,
}
}
pub fn not_loosely_equals(&self, other: &Self) -> Option<bool> {
self.loosely_equals(other).map(|value| !value)
}
// Complete implementation of strict equality for javascript
pub fn strictly_equals(&self, other: &Self) -> bool {
// https://tc39.es/ecma262/multipage/abstract-operations.html#sec-isstrictlyequal
match (&self, &other) {
(JsValue::Number(left), JsValue::Number(right)) => left.equals(*right),
(JsValue::Null, JsValue::Null) => true,
(JsValue::Undefined, JsValue::Undefined) => true,
(JsValue::Boolean(left), JsValue::Boolean(right)) => left == right,
(JsValue::String(left), JsValue::String(right)) => left == right,
_ => false,
}
}
pub fn not_strictly_equals(&self, other: &Self) -> bool {
!self.strictly_equals(other)
}
}
impl Serialize for JsValue {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
match self {
Self::Boolean(b) => serializer.serialize_bool(*b),
Self::Null => serializer.serialize_none(),
Self::Number(n) => serializer.serialize_f64(n.into()),
Self::String(s) => serializer.serialize_str(s),
Self::Undefined => serializer.serialize_unit(),
}
}
}
impl<'de> Deserialize<'de> for JsValue {
#[inline]
fn deserialize<D>(deserializer: D) -> Result<JsValue, D::Error>
where
D: serde::Deserializer<'de>,
{
struct ValueVisitor;
impl<'de> Visitor<'de> for ValueVisitor {
type Value = JsValue;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("valid primitive JSON value (null, boolean, number, or string")
}
#[inline]
fn visit_bool<E>(self, value: bool) -> Result<JsValue, E> {
Ok(JsValue::Boolean(value))
}
#[inline]
fn visit_i64<E>(self, value: i64) -> Result<JsValue, E> {
if (MIN_SAFE_INT..=MAX_SAFE_INT).contains(&value) {
Ok(JsValue::Number((value as f64).into()))
} else {
panic!("Invalid number")
}
}
#[inline]
fn visit_u64<E>(self, value: u64) -> Result<JsValue, E> {
if value as i64 <= MAX_SAFE_INT {
Ok(JsValue::Number((value as f64).into()))
} else {
panic!("Invalid number")
}
}
#[inline]
fn visit_f64<E>(self, value: f64) -> Result<JsValue, E> {
Ok(JsValue::Number(value.into()))
}
#[inline]
fn visit_str<E>(self, value: &str) -> Result<JsValue, E>
where
E: serde::de::Error,
{
self.visit_string(String::from(value))
}
#[inline]
fn visit_string<E>(self, value: String) -> Result<JsValue, E> {
Ok(JsValue::String(value))
}
#[inline]
fn visit_none<E>(self) -> Result<JsValue, E> {
Ok(JsValue::Null)
}
#[inline]
fn visit_some<D>(self, deserializer: D) -> Result<JsValue, D::Error>
where
D: serde::Deserializer<'de>,
{
Deserialize::deserialize(deserializer)
}
#[inline]
fn visit_unit<E>(self) -> Result<JsValue, E> {
Ok(JsValue::Undefined)
}
}
deserializer.deserialize_any(ValueVisitor)
}
}
/// Represents a JavaScript Number as its binary representation so that
/// -1 == -1, NaN == Nan etc.
/// Note: NaN is *always* represented as the f64::NAN constant to allow
/// comparison of NaNs.
#[derive(Clone, Copy, Eq, PartialEq, PartialOrd, Ord, Debug, Hash)]
pub struct Number(u64);
pub const MAX_SAFE_INT: i64 = 9007199254740991;
pub const MIN_SAFE_INT: i64 = -9007199254740991;
impl From<f64> for Number {
fn from(value: f64) -> Self {
if value.is_nan() {
Self(f64::NAN.to_bits())
} else {
Self(value.to_bits())
}
}
}
impl From<u32> for Number {
fn from(value: u32) -> Self {
f64::from(value).into()
}
}
impl From<Number> for f64 {
fn from(number: Number) -> Self {
let value = f64::from_bits(number.0);
assert!(!f64::is_nan(value) || number.0 == f64::NAN.to_bits());
value
}
}
impl From<&Number> for f64 {
fn from(number: &Number) -> Self {
let value = f64::from_bits(number.0);
assert!(!f64::is_nan(value) || number.0 == f64::NAN.to_bits());
value
}
}
impl Number {
pub fn equals(self, other: Self) -> bool {
f64::from(self) == f64::from(other)
}
pub fn not_equals(self, other: Self) -> bool {
!self.equals(other)
}
pub fn is_truthy(self) -> bool {
let value = f64::from(self);
!(self.0 == f64::NAN.to_bits() || value == 0.0 || value == -0.0)
}
}
impl std::ops::Add for Number {
type Output = Number;
fn add(self, rhs: Self) -> Self::Output {
let result = f64::from(self) + f64::from(rhs);
Self::from(result)
}
}
impl std::ops::Sub for Number {
type Output = Number;
fn sub(self, rhs: Self) -> Self::Output {
let result = f64::from(self) - f64::from(rhs);
Self::from(result)
}
}
impl std::ops::Mul for Number {
type Output = Number;
fn mul(self, rhs: Self) -> Self::Output {
let result = f64::from(self) * f64::from(rhs);
Self::from(result)
}
}
impl std::ops::Div for Number {
type Output = Number;
fn div(self, rhs: Self) -> Self::Output {
let result = f64::from(self) / f64::from(rhs);
Self::from(result)
}
}
impl Serialize for Number {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_f64(self.into())
}
}
impl<'de> Deserialize<'de> for Number {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
struct ValueVisitor;
impl<'de> Visitor<'de> for ValueVisitor {
type Value = Number;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("value JavaScript number value")
}
fn visit_f64<E>(self, v: f64) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(v.into())
}
}
deserializer.deserialize_any(ValueVisitor)
}
}

View File

@@ -0,0 +1,38 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
mod binding;
mod generated;
mod generated_extensions;
mod js_value;
mod range;
mod visit;
pub use binding::{Binding, BindingId};
pub use generated::*;
pub use generated_extensions::*;
pub use js_value::{JsValue, Number};
pub use range::SourceRange;
pub use visit::*;
#[cfg(test)]
mod tests {
use insta::{assert_snapshot, glob};
use super::*;
#[test]
fn fixtures() {
glob!("fixtures/**.json", |path| {
println!("{:?}", path);
let input = std::fs::read_to_string(path).unwrap();
let ast: Program = serde_json::from_str(&input).unwrap();
let serialized = serde_json::to_string_pretty(&ast).unwrap();
assert_snapshot!(format!("Input:\n{input}\n\nOutput:\n{serialized}"));
});
}
}

View File

@@ -0,0 +1,31 @@
/*
* 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.
*/
use std::num::NonZeroU32;
use serde::ser::SerializeTuple;
use serde::{Deserialize, Serialize};
#[derive(Deserialize, Copy, Clone, Debug, PartialEq, PartialOrd, Hash)]
pub struct SourceRange {
pub start: u32,
pub end: NonZeroU32,
}
// ESTree and Babel store the `range` as `[start, end]`, so we customize
// the serialization to use a tuple representation.
impl Serialize for SourceRange {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let mut tuple = serializer.serialize_tuple(2)?;
tuple.serialize_element(&self.start)?;
tuple.serialize_element(&self.end)?;
tuple.end()
}
}

View File

@@ -0,0 +1,213 @@
---
source: crates/react_estree/src/lib.rs
expression: "format!(\"Input:\\n{input}\\n\\nOutput:\\n{serialized}\")"
input_file: crates/react_estree/src/fixtures/import.json
---
Input:
{
"type": "Program",
"loc": {
"source": null,
"start": {
"line": 1,
"column": 0
},
"end": {
"line": 1,
"column": 26
}
},
"body": [
{
"type": "ImportDeclaration",
"loc": {
"source": null,
"start": {
"line": 1,
"column": 0
},
"end": {
"line": 1,
"column": 26
}
},
"specifiers": [
{
"type": "ImportDefaultSpecifier",
"loc": {
"source": null,
"start": {
"line": 1,
"column": 7
},
"end": {
"line": 1,
"column": 12
}
},
"local": {
"type": "Identifier",
"loc": {
"source": null,
"start": {
"line": 1,
"column": 7
},
"end": {
"line": 1,
"column": 12
}
},
"name": "React",
"typeAnnotation": null,
"optional": false,
"range": [
7,
12
]
},
"range": [
7,
12
]
}
],
"source": {
"type": "Literal",
"loc": {
"source": null,
"start": {
"line": 1,
"column": 18
},
"end": {
"line": 1,
"column": 25
}
},
"value": "react",
"range": [
18,
25
],
"raw": "'react'"
},
"attributes": [],
"importKind": "value",
"range": [
0,
26
]
}
],
"comments": [],
"interpreter": null,
"range": [
0,
26
],
"sourceType": "module"
}
Output:
{
"type": "Program",
"body": [
{
"type": "ImportDeclaration",
"specifiers": [
{
"type": "ImportDefaultSpecifier",
"local": {
"type": "Identifier",
"name": "React",
"typeAnnotation": null,
"loc": {
"source": null,
"start": {
"line": 1,
"column": 7
},
"end": {
"line": 1,
"column": 12
}
},
"range": [
7,
12
]
},
"loc": {
"source": null,
"start": {
"line": 1,
"column": 7
},
"end": {
"line": 1,
"column": 12
}
},
"range": [
7,
12
]
}
],
"source": {
"type": "Literal",
"value": "react",
"raw": "'react'",
"regex": null,
"bigint": null,
"loc": {
"source": null,
"start": {
"line": 1,
"column": 18
},
"end": {
"line": 1,
"column": 25
}
},
"range": [
18,
25
]
},
"loc": {
"source": null,
"start": {
"line": 1,
"column": 0
},
"end": {
"line": 1,
"column": 26
}
},
"range": [
0,
26
]
}
],
"sourceType": "module",
"loc": {
"source": null,
"start": {
"line": 1,
"column": 0
},
"end": {
"line": 1,
"column": 26
}
},
"range": [
0,
26
]
}

View File

@@ -0,0 +1,379 @@
---
source: crates/react_estree/src/lib.rs
expression: "format!(\"Input:\\n{input}\\n\\nOutput:\\n{serialized}\")"
input_file: crates/react_estree/src/fixtures/simple.json
---
Input:
{
"type": "Program",
"loc": {
"source": null,
"start": {
"line": 1,
"column": 0
},
"end": {
"line": 3,
"column": 1
}
},
"range": [
0,
51
],
"body": [
{
"type": "FunctionDeclaration",
"loc": {
"source": null,
"start": {
"line": 1,
"column": 0
},
"end": {
"line": 3,
"column": 1
}
},
"range": [
0,
51
],
"id": {
"type": "Identifier",
"loc": {
"source": null,
"start": {
"line": 1,
"column": 9
},
"end": {
"line": 1,
"column": 18
}
},
"range": [
9,
18
],
"name": "Component",
"typeAnnotation": null,
"optional": false
},
"params": [
{
"type": "Identifier",
"loc": {
"source": null,
"start": {
"line": 1,
"column": 19
},
"end": {
"line": 1,
"column": 24
}
},
"range": [
19,
24
],
"name": "props",
"typeAnnotation": null,
"optional": false
}
],
"body": {
"type": "BlockStatement",
"loc": {
"source": null,
"start": {
"line": 1,
"column": 26
},
"end": {
"line": 3,
"column": 1
}
},
"range": [
26,
51
],
"body": [
{
"type": "ReturnStatement",
"loc": {
"source": null,
"start": {
"line": 2,
"column": 2
},
"end": {
"line": 2,
"column": 21
}
},
"range": [
30,
49
],
"argument": {
"type": "MemberExpression",
"loc": {
"source": null,
"start": {
"line": 2,
"column": 9
},
"end": {
"line": 2,
"column": 20
}
},
"range": [
37,
48
],
"object": {
"type": "Identifier",
"loc": {
"source": null,
"start": {
"line": 2,
"column": 9
},
"end": {
"line": 2,
"column": 14
}
},
"range": [
37,
42
],
"name": "props",
"typeAnnotation": null,
"optional": false
},
"property": {
"type": "Identifier",
"loc": {
"source": null,
"start": {
"line": 2,
"column": 15
},
"end": {
"line": 2,
"column": 20
}
},
"range": [
43,
48
],
"name": "value",
"typeAnnotation": null,
"optional": false
},
"computed": false
}
}
]
},
"async": false,
"generator": false,
"predicate": null,
"expression": false,
"returnType": null,
"typeParameters": null
}
],
"comments": [],
"errors": []
}
Output:
{
"type": "Program",
"body": [
{
"type": "FunctionDeclaration",
"id": {
"type": "Identifier",
"name": "Component",
"typeAnnotation": null,
"loc": {
"source": null,
"start": {
"line": 1,
"column": 9
},
"end": {
"line": 1,
"column": 18
}
},
"range": [
9,
18
]
},
"params": [
{
"type": "Identifier",
"name": "props",
"typeAnnotation": null,
"loc": {
"source": null,
"start": {
"line": 1,
"column": 19
},
"end": {
"line": 1,
"column": 24
}
},
"range": [
19,
24
]
}
],
"body": {
"type": "BlockStatement",
"body": [
{
"type": "ReturnStatement",
"argument": {
"type": "MemberExpression",
"object": {
"type": "Identifier",
"name": "props",
"typeAnnotation": null,
"loc": {
"source": null,
"start": {
"line": 2,
"column": 9
},
"end": {
"line": 2,
"column": 14
}
},
"range": [
37,
42
]
},
"property": {
"type": "Identifier",
"name": "value",
"typeAnnotation": null,
"loc": {
"source": null,
"start": {
"line": 2,
"column": 15
},
"end": {
"line": 2,
"column": 20
}
},
"range": [
43,
48
]
},
"computed": false,
"loc": {
"source": null,
"start": {
"line": 2,
"column": 9
},
"end": {
"line": 2,
"column": 20
}
},
"range": [
37,
48
]
},
"loc": {
"source": null,
"start": {
"line": 2,
"column": 2
},
"end": {
"line": 2,
"column": 21
}
},
"range": [
30,
49
]
}
],
"loc": {
"source": null,
"start": {
"line": 1,
"column": 26
},
"end": {
"line": 3,
"column": 1
}
},
"range": [
26,
51
]
},
"generator": false,
"async": false,
"loc": null,
"range": null,
"loc": {
"source": null,
"start": {
"line": 1,
"column": 0
},
"end": {
"line": 3,
"column": 1
}
},
"range": [
0,
51
]
}
],
"sourceType": "module",
"loc": {
"source": null,
"start": {
"line": 1,
"column": 0
},
"end": {
"line": 3,
"column": 1
}
},
"range": [
0,
51
]
}

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