Compare commits
108 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eaf36e3188 | ||
|
|
9db3f8b484 | ||
|
|
06beb9f43e | ||
|
|
5dc00d6b2b | ||
|
|
c498bfce8b | ||
|
|
8e9a5fc6c1 | ||
|
|
89e8875ec4 | ||
|
|
2d0a5e399f | ||
|
|
0c28a09eef | ||
|
|
143d3e1b89 | ||
|
|
693803a9bb | ||
|
|
24dfad3abb | ||
|
|
bb74190c26 | ||
|
|
5010364d34 | ||
|
|
9938f83ca2 | ||
|
|
2af218a728 | ||
|
|
b06bb35ce9 | ||
|
|
197d6a0403 | ||
|
|
ad09027c16 | ||
|
|
8b9629c810 | ||
|
|
3a5335676f | ||
|
|
b75af04670 | ||
|
|
f765082996 | ||
|
|
7b21c46489 | ||
|
|
e25e8c7575 | ||
|
|
cd7d236682 | ||
|
|
71d0896a4a | ||
|
|
914319ae59 | ||
|
|
3ef31d196a | ||
|
|
17f88c80ed | ||
|
|
3fbd6b7b50 | ||
|
|
ebf7318e87 | ||
|
|
620c838fb6 | ||
|
|
7213509649 | ||
|
|
4c54da77fb | ||
|
|
efd890422d | ||
|
|
b303610c33 | ||
|
|
fea92d8462 | ||
|
|
bc6184dd99 | ||
|
|
ce578f9c59 | ||
|
|
45d942f94a | ||
|
|
b8bedc267f | ||
|
|
4a36d3eab7 | ||
|
|
2ddf8caa9d | ||
|
|
95ff37f5f5 | ||
|
|
3c75bf21dd | ||
|
|
3e04b2a214 | ||
|
|
fc21d5a7db | ||
|
|
35ab8ffef7 | ||
|
|
68013725ac | ||
|
|
bf39780a06 | ||
|
|
b04254fdce | ||
|
|
539bbdbd86 | ||
|
|
e71d4205ae | ||
|
|
2ed34eba0d | ||
|
|
707b3fc6b2 | ||
|
|
7ff4d057b6 | ||
|
|
08075929f2 | ||
|
|
4eea4fcf41 | ||
|
|
58e9a4b74f | ||
|
|
39cad7afc4 | ||
|
|
1d6c8168db | ||
|
|
961b625ab5 | ||
|
|
8a3c5e1a8d | ||
|
|
5e9b48778c | ||
|
|
c44e4a2505 | ||
|
|
31ecc9804a | ||
|
|
ff697fc58b | ||
|
|
096dd7385d | ||
|
|
717584167b | ||
|
|
3fbfb9baaf | ||
|
|
8571249eb8 | ||
|
|
8da36d0508 | ||
|
|
ea05b750a5 | ||
|
|
3366146796 | ||
|
|
365c031fd2 | ||
|
|
a9d63f3f97 | ||
|
|
6a7650c75c | ||
|
|
efb22d8850 | ||
|
|
540cd65252 | ||
|
|
c0f08ae74a | ||
|
|
b10cb4c01e | ||
|
|
f0c767e2a2 | ||
|
|
b2f6365745 | ||
|
|
b81c92be62 | ||
|
|
040f8286e9 | ||
|
|
450f8df886 | ||
|
|
7a728dffd1 | ||
|
|
e5dd82a79d | ||
|
|
731ae3e0ad | ||
|
|
deca96520f | ||
|
|
0b1a9e90c5 | ||
|
|
8b2046d0ce | ||
|
|
d20c2802b4 | ||
|
|
0a7cf20b22 | ||
|
|
b286430c8a | ||
|
|
d3b8ff6e58 | ||
|
|
a7fa8702ee | ||
|
|
95671b4eb3 | ||
|
|
6377903074 | ||
|
|
095ce8a311 | ||
|
|
18a11339c3 | ||
|
|
d726d692ed | ||
|
|
50c5cdb653 | ||
|
|
deb7859bb0 | ||
|
|
1825990c56 | ||
|
|
1de32a5e75 | ||
|
|
ef4bc8b4f9 |
@@ -1,10 +1,11 @@
|
||||
{
|
||||
"packages": ["packages/react", "packages/react-dom", "packages/scheduler"],
|
||||
"packages": ["packages/react", "packages/react-dom", "packages/react-server-dom-webpack", "packages/scheduler"],
|
||||
"buildCommand": "download-build-in-codesandbox-ci",
|
||||
"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"],
|
||||
|
||||
@@ -615,6 +615,8 @@ module.exports = {
|
||||
GetAnimationsOptions: 'readonly',
|
||||
Animatable: 'readonly',
|
||||
ScrollTimeline: 'readonly',
|
||||
EventListenerOptionsOrUseCapture: 'readonly',
|
||||
FocusOptions: 'readonly',
|
||||
|
||||
spyOnDev: 'readonly',
|
||||
spyOnDevAndProd: 'readonly',
|
||||
|
||||
18
.github/ISSUE_TEMPLATE/19.md
vendored
18
.github/ISSUE_TEMPLATE/19.md
vendored
@@ -1,18 +0,0 @@
|
||||
---
|
||||
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.
|
||||
-->
|
||||
12
.github/workflows/compiler_discord_notify.yml
vendored
12
.github/workflows/compiler_discord_notify.yml
vendored
@@ -10,7 +10,19 @@ on:
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
check_access:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
is_member_or_collaborator: ${{ steps.check_is_member_or_collaborator.outputs.is_member_or_collaborator }}
|
||||
steps:
|
||||
- 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
|
||||
|
||||
5
.github/workflows/compiler_prereleases.yml
vendored
5
.github/workflows/compiler_prereleases.yml
vendored
@@ -16,6 +16,9 @@ on:
|
||||
version_name:
|
||||
required: true
|
||||
type: string
|
||||
tag_version:
|
||||
required: false
|
||||
type: string
|
||||
secrets:
|
||||
NPM_TOKEN:
|
||||
required: true
|
||||
@@ -55,4 +58,4 @@ jobs:
|
||||
- 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 }}
|
||||
scripts/release/publish.js --frfr --ci --versionName=${{ inputs.version_name }} --tag=${{ inputs.dist_tag }} ${{ inputs.tag_version && format('--tagVersion={0}', inputs.tag_version) || '' }}
|
||||
|
||||
@@ -14,6 +14,9 @@ on:
|
||||
version_name:
|
||||
required: true
|
||||
type: string
|
||||
tag_version:
|
||||
required: false
|
||||
type: string
|
||||
|
||||
permissions: {}
|
||||
|
||||
@@ -29,5 +32,6 @@ jobs:
|
||||
release_channel: ${{ inputs.release_channel }}
|
||||
dist_tag: ${{ inputs.dist_tag }}
|
||||
version_name: ${{ inputs.version_name }}
|
||||
tag_version: ${{ inputs.tag_version }}
|
||||
secrets:
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
name: (Compiler) Publish Prereleases Weekly
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# At 10 minutes past 9:00 on Mon
|
||||
- cron: 10 9 * * 1
|
||||
|
||||
permissions: {}
|
||||
|
||||
env:
|
||||
TZ: /usr/share/zoneinfo/America/Los_Angeles
|
||||
|
||||
jobs:
|
||||
publish_prerelease_beta:
|
||||
name: Publish to beta channel
|
||||
uses: facebook/react/.github/workflows/compiler_prereleases.yml@main
|
||||
with:
|
||||
commit_sha: ${{ github.sha }}
|
||||
release_channel: beta
|
||||
dist_tag: beta
|
||||
version_name: '19.0.0'
|
||||
secrets:
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
12
.github/workflows/runtime_discord_notify.yml
vendored
12
.github/workflows/runtime_discord_notify.yml
vendored
@@ -10,7 +10,19 @@ on:
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
check_access:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
is_member_or_collaborator: ${{ steps.check_is_member_or_collaborator.outputs.is_member_or_collaborator }}
|
||||
steps:
|
||||
- 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
|
||||
|
||||
15
.github/workflows/runtime_prereleases.yml
vendored
15
.github/workflows/runtime_prereleases.yml
vendored
@@ -13,7 +13,14 @@ 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
|
||||
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:
|
||||
@@ -58,3 +65,11 @@ jobs:
|
||||
GH_TOKEN=${{ secrets.GH_TOKEN }} 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 }}
|
||||
- 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: 'Publish of $${{ inputs.release_channel }} release failed'
|
||||
embed-url: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}/attempts/${{ github.run_attempt }}
|
||||
|
||||
@@ -21,7 +21,9 @@ jobs:
|
||||
commit_sha: ${{ github.sha }}
|
||||
release_channel: stable
|
||||
dist_tag: canary,next
|
||||
enableFailureNotification: true
|
||||
secrets:
|
||||
DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }}
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -40,6 +42,8 @@ jobs:
|
||||
commit_sha: ${{ github.sha }}
|
||||
release_channel: experimental
|
||||
dist_tag: experimental
|
||||
enableFailureNotification: true
|
||||
secrets:
|
||||
DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }}
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
13
.github/workflows/shared_label_core_team_prs.yml
vendored
13
.github/workflows/shared_label_core_team_prs.yml
vendored
@@ -2,6 +2,7 @@ name: (Shared) Label Core Team PRs
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened]
|
||||
|
||||
permissions: {}
|
||||
|
||||
@@ -11,7 +12,19 @@ env:
|
||||
SEGMENT_DOWNLOAD_TIMEOUT_MINS: 1
|
||||
|
||||
jobs:
|
||||
check_access:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
is_member_or_collaborator: ${{ steps.check_is_member_or_collaborator.outputs.is_member_or_collaborator }}
|
||||
steps:
|
||||
- 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
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
## 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).
|
||||
47
CHANGELOG.md
47
CHANGELOG.md
@@ -1,3 +1,50 @@
|
||||
## 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 Complier 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 [#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()` didn’t 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.
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
//
|
||||
// 0.0.0-experimental-241c4467e-20200129
|
||||
|
||||
const ReactVersion = '19.1.0';
|
||||
const ReactVersion = '19.2.0';
|
||||
|
||||
// The label used by the @canary channel. Represents the upcoming release's
|
||||
// stability. Most of the time, this will be "canary", but we may temporarily
|
||||
@@ -33,7 +33,7 @@ const canaryChannelLabel = 'canary';
|
||||
const rcNumber = 0;
|
||||
|
||||
const stablePackages = {
|
||||
'eslint-plugin-react-hooks': '5.2.0',
|
||||
'eslint-plugin-react-hooks': '6.1.0',
|
||||
'jest-react': '0.17.0',
|
||||
react: ReactVersion,
|
||||
'react-art': ReactVersion,
|
||||
@@ -42,12 +42,12 @@ const stablePackages = {
|
||||
'react-server-dom-turbopack': ReactVersion,
|
||||
'react-server-dom-parcel': ReactVersion,
|
||||
'react-is': ReactVersion,
|
||||
'react-reconciler': '0.32.0',
|
||||
'react-refresh': '0.17.0',
|
||||
'react-reconciler': '0.33.0',
|
||||
'react-refresh': '0.18.0',
|
||||
'react-test-renderer': ReactVersion,
|
||||
'use-subscription': '1.11.0',
|
||||
'use-sync-external-store': '1.5.0',
|
||||
scheduler: '0.26.0',
|
||||
'use-subscription': '1.12.0',
|
||||
'use-sync-external-store': '1.6.0',
|
||||
scheduler: '0.27.0',
|
||||
};
|
||||
|
||||
// These packages do not exist in the @canary or @latest channel, only
|
||||
|
||||
59
compiler/CHANGELOG.md
Normal file
59
compiler/CHANGELOG.md
Normal file
@@ -0,0 +1,59 @@
|
||||
## 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)
|
||||
@@ -27,7 +27,7 @@
|
||||
"@babel/types": "7.26.3",
|
||||
"@heroicons/react": "^1.0.6",
|
||||
"@monaco-editor/react": "^4.4.6",
|
||||
"@playwright/test": "^1.42.1",
|
||||
"@playwright/test": "^1.51.1",
|
||||
"@use-gesture/react": "^10.2.22",
|
||||
"hermes-eslint": "^0.25.0",
|
||||
"hermes-parser": "^0.25.0",
|
||||
|
||||
@@ -781,12 +781,12 @@
|
||||
resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33"
|
||||
integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==
|
||||
|
||||
"@playwright/test@^1.42.1":
|
||||
version "1.47.2"
|
||||
resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.47.2.tgz#dbe7051336bfc5cc599954214f9111181dbc7475"
|
||||
integrity sha512-jTXRsoSPONAs8Za9QEQdyjFn+0ZQFjCiIztAIF6bi1HqhBzG9Ma7g1WotyiGqFSBRZjIEqMdT8RUlbk1QVhzCQ==
|
||||
"@playwright/test@^1.51.1":
|
||||
version "1.51.1"
|
||||
resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.51.1.tgz#75357d513221a7be0baad75f01e966baf9c41a2e"
|
||||
integrity sha512-nM+kEaTSAoVlXmMPH10017vn3FSiFqr/bh4fKg9vmAdMfd9SDqRZNvPSiAHADc/itWak+qPvMPZQOPwCBW7k7Q==
|
||||
dependencies:
|
||||
playwright "1.47.2"
|
||||
playwright "1.51.1"
|
||||
|
||||
"@rtsao/scc@^1.1.0":
|
||||
version "1.1.0"
|
||||
@@ -1249,14 +1249,14 @@ camelcase-css@^2.0.1:
|
||||
integrity sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==
|
||||
|
||||
caniuse-lite@^1.0.30001579:
|
||||
version "1.0.30001669"
|
||||
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001669.tgz#fda8f1d29a8bfdc42de0c170d7f34a9cf19ed7a3"
|
||||
integrity sha512-DlWzFDJqstqtIVx1zeSpIMLjunf5SmwOw0N2Ck/QSQdS8PLS4+9HrLaYei4w8BIAL7IB/UEDu889d8vhCTPA0w==
|
||||
version "1.0.30001715"
|
||||
resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001715.tgz"
|
||||
integrity sha512-7ptkFGMm2OAOgvZpwgA4yjQ5SQbrNVGdRjzH0pBdy1Fasvcr+KAeECmbCAECzTuDuoX0FCY8KzUxjf9+9kfZEw==
|
||||
|
||||
caniuse-lite@^1.0.30001646, caniuse-lite@^1.0.30001663:
|
||||
version "1.0.30001664"
|
||||
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001664.tgz#d588d75c9682d3301956b05a3749652a80677df4"
|
||||
integrity sha512-AmE7k4dXiNKQipgn7a2xg558IRqPN3jMQY/rOsbxDhrd0tyChwbITBfiwtnqz8bi2M5mIWbxAYBvk7W7QBUS2g==
|
||||
version "1.0.30001715"
|
||||
resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001715.tgz"
|
||||
integrity sha512-7ptkFGMm2OAOgvZpwgA4yjQ5SQbrNVGdRjzH0pBdy1Fasvcr+KAeECmbCAECzTuDuoX0FCY8KzUxjf9+9kfZEw==
|
||||
|
||||
chalk@^2.4.2:
|
||||
version "2.4.2"
|
||||
@@ -3008,17 +3008,17 @@ pirates@^4.0.1:
|
||||
resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.6.tgz#3018ae32ecfcff6c29ba2267cbf21166ac1f36b9"
|
||||
integrity sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==
|
||||
|
||||
playwright-core@1.47.2:
|
||||
version "1.47.2"
|
||||
resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.47.2.tgz#7858da9377fa32a08be46ba47d7523dbd9460a4e"
|
||||
integrity sha512-3JvMfF+9LJfe16l7AbSmU555PaTl2tPyQsVInqm3id16pdDfvZ8TTZ/pyzmkbDrZTQefyzU7AIHlZqQnxpqHVQ==
|
||||
playwright-core@1.51.1:
|
||||
version "1.51.1"
|
||||
resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.51.1.tgz#d57f0393e02416f32a47cf82b27533656a8acce1"
|
||||
integrity sha512-/crRMj8+j/Nq5s8QcvegseuyeZPxpQCZb6HNk3Sos3BlZyAknRjoyJPFWkpNn8v0+P3WiwqFF8P+zQo4eqiNuw==
|
||||
|
||||
playwright@1.47.2:
|
||||
version "1.47.2"
|
||||
resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.47.2.tgz#155688aa06491ee21fb3e7555b748b525f86eb20"
|
||||
integrity sha512-nx1cLMmQWqmA3UsnjaaokyoUpdVaaDhJhMoxX2qj3McpjnsqFHs516QAKYhqHAgOP+oCFTEOCOAaD1RgD/RQfA==
|
||||
playwright@1.51.1:
|
||||
version "1.51.1"
|
||||
resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.51.1.tgz#ae1467ee318083968ad28d6990db59f47a55390f"
|
||||
integrity sha512-kkx+MB2KQRkyxjYPc3a0wLZZoDczmppyGJIvQ43l+aZihkaVvmu/21kiyaHeHjiFxjxNNFnUncKmcGIyOojsaw==
|
||||
dependencies:
|
||||
playwright-core "1.47.2"
|
||||
playwright-core "1.51.1"
|
||||
optionalDependencies:
|
||||
fsevents "2.3.2"
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
"prettier": "^3.3.3",
|
||||
"prettier-plugin-hermes-parser": "^0.26.0",
|
||||
"prompt-promise": "^1.0.3",
|
||||
"rimraf": "^5.0.10",
|
||||
"rimraf": "^6.0.1",
|
||||
"to-fast-properties": "^2.0.0",
|
||||
"tsup": "^8.4.0",
|
||||
"typescript": "^5.4.3",
|
||||
@@ -45,7 +45,6 @@
|
||||
"yargs": "^17.7.2"
|
||||
},
|
||||
"resolutions": {
|
||||
"rimraf": "5.0.10",
|
||||
"@babel/types": "7.26.3"
|
||||
},
|
||||
"packageManager": "yarn@1.22.22"
|
||||
|
||||
@@ -182,7 +182,9 @@ export type LoggerEvent =
|
||||
| CompileDiagnosticEvent
|
||||
| CompileSkipEvent
|
||||
| PipelineErrorEvent
|
||||
| TimingEvent;
|
||||
| TimingEvent
|
||||
| AutoDepsDecorationsEvent
|
||||
| AutoDepsEligibleEvent;
|
||||
|
||||
export type CompileErrorEvent = {
|
||||
kind: 'CompileError';
|
||||
@@ -219,6 +221,16 @@ export type TimingEvent = {
|
||||
kind: 'Timing';
|
||||
measurement: PerformanceMeasure;
|
||||
};
|
||||
export type AutoDepsDecorationsEvent = {
|
||||
kind: 'AutoDepsDecorations';
|
||||
fnLoc: t.SourceLocation;
|
||||
decorations: Array<t.SourceLocation>;
|
||||
};
|
||||
export type AutoDepsEligibleEvent = {
|
||||
kind: 'AutoDepsEligible';
|
||||
fnLoc: t.SourceLocation;
|
||||
depArrayLoc: t.SourceLocation;
|
||||
};
|
||||
|
||||
export type Logger = {
|
||||
logEvent: (filename: string | null, event: LoggerEvent) => void;
|
||||
|
||||
@@ -392,6 +392,11 @@ function runWithEnvironment(
|
||||
|
||||
if (env.config.inferEffectDependencies) {
|
||||
inferEffectDependencies(hir);
|
||||
log({
|
||||
kind: 'hir',
|
||||
name: 'InferEffectDependencies',
|
||||
value: hir,
|
||||
});
|
||||
}
|
||||
|
||||
if (env.config.inlineJsxTransform) {
|
||||
|
||||
@@ -469,6 +469,23 @@ export function compileProgram(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Otherwise if 'use no forget/memo' is present, we still run the code through the compiler
|
||||
* for validation but we don't mutate the babel AST. This allows us to flag if there is an
|
||||
* unused 'use no forget/memo' directive.
|
||||
*/
|
||||
if (pass.opts.ignoreUseNoForget === false && optOutDirectives.length > 0) {
|
||||
for (const directive of optOutDirectives) {
|
||||
pass.opts.logger?.logEvent(pass.filename, {
|
||||
kind: 'CompileSkip',
|
||||
fnLoc: fn.node.body.loc ?? null,
|
||||
reason: `Skipped due to '${directive.value.value}' directive.`,
|
||||
loc: directive.loc ?? null,
|
||||
});
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
pass.opts.logger?.logEvent(pass.filename, {
|
||||
kind: 'CompileSuccess',
|
||||
fnLoc: fn.node.loc ?? null,
|
||||
@@ -492,23 +509,6 @@ export function compileProgram(
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Otherwise if 'use no forget/memo' is present, we still run the code through the compiler
|
||||
* for validation but we don't mutate the babel AST. This allows us to flag if there is an
|
||||
* unused 'use no forget/memo' directive.
|
||||
*/
|
||||
if (pass.opts.ignoreUseNoForget === false && optOutDirectives.length > 0) {
|
||||
for (const directive of optOutDirectives) {
|
||||
pass.opts.logger?.logEvent(pass.filename, {
|
||||
kind: 'CompileSkip',
|
||||
fnLoc: fn.node.body.loc ?? null,
|
||||
reason: `Skipped due to '${directive.value.value}' directive.`,
|
||||
loc: directive.loc ?? null,
|
||||
});
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!pass.opts.noEmit) {
|
||||
return compileResult.compiledFn;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import type * as BabelCore from '@babel/core';
|
||||
import {hasOwnProperty} from '../Utils/utils';
|
||||
import {PluginOptions} from './Options';
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {NodePath} from '@babel/core';
|
||||
import * as t from '@babel/types';
|
||||
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {CompilerError} from '..';
|
||||
import {
|
||||
BlockId,
|
||||
|
||||
@@ -2406,6 +2406,19 @@ function lowerExpression(
|
||||
kind: 'TypeCastExpression',
|
||||
value: lowerExpressionToTemporary(builder, expr.get('expression')),
|
||||
typeAnnotation: typeAnnotation.node,
|
||||
typeAnnotationKind: 'cast',
|
||||
type: lowerType(typeAnnotation.node),
|
||||
loc: exprLoc,
|
||||
};
|
||||
}
|
||||
case 'TSSatisfiesExpression': {
|
||||
let expr = exprPath as NodePath<t.TSSatisfiesExpression>;
|
||||
const typeAnnotation = expr.get('typeAnnotation');
|
||||
return {
|
||||
kind: 'TypeCastExpression',
|
||||
value: lowerExpressionToTemporary(builder, expr.get('expression')),
|
||||
typeAnnotation: typeAnnotation.node,
|
||||
typeAnnotationKind: 'satisfies',
|
||||
type: lowerType(typeAnnotation.node),
|
||||
loc: exprLoc,
|
||||
};
|
||||
@@ -2417,6 +2430,7 @@ function lowerExpression(
|
||||
kind: 'TypeCastExpression',
|
||||
value: lowerExpressionToTemporary(builder, expr.get('expression')),
|
||||
typeAnnotation: typeAnnotation.node,
|
||||
typeAnnotationKind: 'as',
|
||||
type: lowerType(typeAnnotation.node),
|
||||
loc: exprLoc,
|
||||
};
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {CompilerError} from '../CompilerError';
|
||||
import {getScopes, recursivelyTraverseItems} from './AssertValidBlockNesting';
|
||||
import {Environment} from './Environment';
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {CompilerError} from '../CompilerError';
|
||||
import {inRange} from '../ReactiveScopes/InferReactiveScopeVariables';
|
||||
import {printDependency} from '../ReactiveScopes/PrintReactiveFunction';
|
||||
@@ -12,6 +19,7 @@ import {
|
||||
BasicBlock,
|
||||
BlockId,
|
||||
DependencyPathEntry,
|
||||
FunctionExpression,
|
||||
GeneratedSource,
|
||||
getHookKind,
|
||||
HIRFunction,
|
||||
@@ -23,6 +31,7 @@ import {
|
||||
PropertyLiteral,
|
||||
ReactiveScopeDependency,
|
||||
ScopeId,
|
||||
TInstruction,
|
||||
} from './HIR';
|
||||
|
||||
const DEBUG_PRINT = false;
|
||||
@@ -120,6 +129,33 @@ export function collectHoistablePropertyLoads(
|
||||
});
|
||||
}
|
||||
|
||||
export function collectHoistablePropertyLoadsInInnerFn(
|
||||
fnInstr: TInstruction<FunctionExpression>,
|
||||
temporaries: ReadonlyMap<IdentifierId, ReactiveScopeDependency>,
|
||||
hoistableFromOptionals: ReadonlyMap<BlockId, ReactiveScopeDependency>,
|
||||
): ReadonlyMap<BlockId, BlockInfo> {
|
||||
const fn = fnInstr.value.loweredFunc.func;
|
||||
const initialContext: CollectHoistablePropertyLoadsContext = {
|
||||
temporaries,
|
||||
knownImmutableIdentifiers: new Set(),
|
||||
hoistableFromOptionals,
|
||||
registry: new PropertyPathRegistry(),
|
||||
nestedFnImmutableContext: null,
|
||||
assumedInvokedFns: fn.env.config.enableTreatFunctionDepsAsConditional
|
||||
? new Set()
|
||||
: getAssumedInvokedFunctions(fn),
|
||||
};
|
||||
const nestedFnImmutableContext = new Set(
|
||||
fn.context
|
||||
.filter(place =>
|
||||
isImmutableAtInstr(place.identifier, fnInstr.id, initialContext),
|
||||
)
|
||||
.map(place => place.identifier.id),
|
||||
);
|
||||
initialContext.nestedFnImmutableContext = nestedFnImmutableContext;
|
||||
return collectHoistablePropertyLoadsImpl(fn, initialContext);
|
||||
}
|
||||
|
||||
type CollectHoistablePropertyLoadsContext = {
|
||||
temporaries: ReadonlyMap<IdentifierId, ReactiveScopeDependency>;
|
||||
knownImmutableIdentifiers: ReadonlySet<IdentifierId>;
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {CompilerError} from '..';
|
||||
import {assertNonNull} from './CollectHoistablePropertyLoads';
|
||||
import {
|
||||
|
||||
@@ -9,6 +9,7 @@ import {Effect, ValueKind, ValueReason} from './HIR';
|
||||
import {
|
||||
BUILTIN_SHAPES,
|
||||
BuiltInArrayId,
|
||||
BuiltInFireFunctionId,
|
||||
BuiltInFireId,
|
||||
BuiltInMapId,
|
||||
BuiltInMixedReadonlyId,
|
||||
@@ -674,7 +675,12 @@ const REACT_APIS: Array<[string, BuiltInType]> = [
|
||||
{
|
||||
positionalParams: [],
|
||||
restParam: null,
|
||||
returnType: {kind: 'Primitive'},
|
||||
returnType: {
|
||||
kind: 'Function',
|
||||
return: {kind: 'Poly'},
|
||||
shapeId: BuiltInFireFunctionId,
|
||||
isConstructor: false,
|
||||
},
|
||||
calleeEffect: Effect.Read,
|
||||
returnValueKind: ValueKind.Frozen,
|
||||
},
|
||||
|
||||
@@ -910,13 +910,21 @@ export type InstructionValue =
|
||||
value: Place;
|
||||
loc: SourceLocation;
|
||||
}
|
||||
| {
|
||||
| ({
|
||||
kind: 'TypeCastExpression';
|
||||
value: Place;
|
||||
typeAnnotation: t.FlowType | t.TSType;
|
||||
type: Type;
|
||||
loc: SourceLocation;
|
||||
}
|
||||
} & (
|
||||
| {
|
||||
typeAnnotation: t.FlowType;
|
||||
typeAnnotationKind: 'cast';
|
||||
}
|
||||
| {
|
||||
typeAnnotation: t.TSType;
|
||||
typeAnnotationKind: 'as' | 'satisfies';
|
||||
}
|
||||
))
|
||||
| JsxExpression
|
||||
| {
|
||||
kind: 'ObjectExpression';
|
||||
@@ -1714,6 +1722,12 @@ export function isDispatcherType(id: Identifier): boolean {
|
||||
return id.type.kind === 'Function' && id.type.shapeId === 'BuiltInDispatch';
|
||||
}
|
||||
|
||||
export function isFireFunctionType(id: Identifier): boolean {
|
||||
return (
|
||||
id.type.kind === 'Function' && id.type.shapeId === 'BuiltInFireFunction'
|
||||
);
|
||||
}
|
||||
|
||||
export function isStableType(id: Identifier): boolean {
|
||||
return (
|
||||
isSetStateType(id) ||
|
||||
@@ -1724,6 +1738,40 @@ export function isStableType(id: Identifier): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
export function isStableTypeContainer(id: Identifier): boolean {
|
||||
const type_ = id.type;
|
||||
if (type_.kind !== 'Object') {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
isUseStateType(id) || // setState
|
||||
type_.shapeId === 'BuiltInUseActionState' || // setActionState
|
||||
isUseReducerType(id) || // dispatcher
|
||||
type_.shapeId === 'BuiltInUseTransition' // startTransition
|
||||
);
|
||||
}
|
||||
|
||||
export function evaluatesToStableTypeOrContainer(
|
||||
env: Environment,
|
||||
{value}: Instruction,
|
||||
): boolean {
|
||||
if (value.kind === 'CallExpression' || value.kind === 'MethodCall') {
|
||||
const callee =
|
||||
value.kind === 'CallExpression' ? value.callee : value.property;
|
||||
|
||||
const calleeHookKind = getHookKind(env, callee.identifier);
|
||||
switch (calleeHookKind) {
|
||||
case 'useState':
|
||||
case 'useReducer':
|
||||
case 'useActionState':
|
||||
case 'useRef':
|
||||
case 'useTransition':
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function isUseEffectHookType(id: Identifier): boolean {
|
||||
return (
|
||||
id.type.kind === 'Function' && id.type.shapeId === 'BuiltInUseEffectHook'
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {
|
||||
HIRFunction,
|
||||
InstructionId,
|
||||
|
||||
@@ -223,6 +223,7 @@ export const BuiltInUseContextHookId = 'BuiltInUseContextHook';
|
||||
export const BuiltInUseTransitionId = 'BuiltInUseTransition';
|
||||
export const BuiltInStartTransitionId = 'BuiltInStartTransition';
|
||||
export const BuiltInFireId = 'BuiltInFire';
|
||||
export const BuiltInFireFunctionId = 'BuiltInFireFunction';
|
||||
|
||||
// ShapeRegistry with default definitions for built-ins.
|
||||
export const BUILTIN_SHAPES: ShapeRegistry = new Map();
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {
|
||||
ScopeId,
|
||||
HIRFunction,
|
||||
@@ -109,7 +116,7 @@ export function propagateScopeDependenciesHIR(fn: HIRFunction): void {
|
||||
}
|
||||
}
|
||||
|
||||
function findTemporariesUsedOutsideDeclaringScope(
|
||||
export function findTemporariesUsedOutsideDeclaringScope(
|
||||
fn: HIRFunction,
|
||||
): ReadonlySet<DeclarationId> {
|
||||
/*
|
||||
@@ -371,7 +378,7 @@ type Decl = {
|
||||
scope: Stack<ReactiveScope>;
|
||||
};
|
||||
|
||||
class Context {
|
||||
export class DependencyCollectionContext {
|
||||
#declarations: Map<DeclarationId, Decl> = new Map();
|
||||
#reassignments: Map<Identifier, Decl> = new Map();
|
||||
|
||||
@@ -638,7 +645,10 @@ enum HIRValue {
|
||||
Terminal,
|
||||
}
|
||||
|
||||
function handleInstruction(instr: Instruction, context: Context): void {
|
||||
export function handleInstruction(
|
||||
instr: Instruction,
|
||||
context: DependencyCollectionContext,
|
||||
): void {
|
||||
const {id, value, lvalue} = instr;
|
||||
context.declare(lvalue.identifier, {
|
||||
id,
|
||||
@@ -701,7 +711,7 @@ function collectDependencies(
|
||||
temporaries: ReadonlyMap<IdentifierId, ReactiveScopeDependency>,
|
||||
processedInstrsInOptional: ReadonlySet<Instruction | Terminal>,
|
||||
): Map<ReactiveScope, Array<ReactiveScopeDependency>> {
|
||||
const context = new Context(
|
||||
const context = new DependencyCollectionContext(
|
||||
usedOutsideDeclaringScope,
|
||||
temporaries,
|
||||
processedInstrsInOptional,
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {CompilerError} from '..';
|
||||
import {BlockId, GotoVariant, HIRFunction} from './HIR';
|
||||
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
/**
|
||||
* 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 * as t from '@babel/types';
|
||||
import {CompilerError, SourceLocation} from '..';
|
||||
import {
|
||||
ArrayExpression,
|
||||
@@ -14,17 +22,30 @@ import {
|
||||
ScopeId,
|
||||
ReactiveScopeDependency,
|
||||
Place,
|
||||
ReactiveScope,
|
||||
ReactiveScopeDependencies,
|
||||
Terminal,
|
||||
isUseRefType,
|
||||
isSetStateType,
|
||||
isFireFunctionType,
|
||||
makeScopeId,
|
||||
} from '../HIR';
|
||||
import {collectHoistablePropertyLoadsInInnerFn} from '../HIR/CollectHoistablePropertyLoads';
|
||||
import {collectOptionalChainSidemap} from '../HIR/CollectOptionalChainDependencies';
|
||||
import {ReactiveScopeDependencyTreeHIR} from '../HIR/DeriveMinimalDependenciesHIR';
|
||||
import {DEFAULT_EXPORT} from '../HIR/Environment';
|
||||
import {
|
||||
createTemporaryPlace,
|
||||
fixScopeAndIdentifierRanges,
|
||||
markInstructionIds,
|
||||
} from '../HIR/HIRBuilder';
|
||||
import {
|
||||
collectTemporariesSidemap,
|
||||
DependencyCollectionContext,
|
||||
handleInstruction,
|
||||
} from '../HIR/PropagateScopeDependenciesHIR';
|
||||
import {eachInstructionOperand, eachTerminalOperand} from '../HIR/visitors';
|
||||
import {empty} from '../Utils/Stack';
|
||||
import {getOrInsertWith} from '../Utils/utils';
|
||||
|
||||
/**
|
||||
@@ -53,10 +74,7 @@ export function inferEffectDependencies(fn: HIRFunction): void {
|
||||
const autodepFnLoads = new Map<IdentifierId, number>();
|
||||
const autodepModuleLoads = new Map<IdentifierId, Map<string, number>>();
|
||||
|
||||
const scopeInfos = new Map<
|
||||
ScopeId,
|
||||
{pruned: boolean; deps: ReactiveScopeDependencies; hasSingleInstr: boolean}
|
||||
>();
|
||||
const scopeInfos = new Map<ScopeId, ReactiveScopeDependencies>();
|
||||
|
||||
const loadGlobals = new Set<IdentifierId>();
|
||||
|
||||
@@ -70,19 +88,18 @@ export function inferEffectDependencies(fn: HIRFunction): void {
|
||||
const reactiveIds = inferReactiveIdentifiers(fn);
|
||||
|
||||
for (const [, block] of fn.body.blocks) {
|
||||
if (
|
||||
block.terminal.kind === 'scope' ||
|
||||
block.terminal.kind === 'pruned-scope'
|
||||
) {
|
||||
if (block.terminal.kind === 'scope') {
|
||||
const scopeBlock = fn.body.blocks.get(block.terminal.block)!;
|
||||
scopeInfos.set(block.terminal.scope.id, {
|
||||
pruned: block.terminal.kind === 'pruned-scope',
|
||||
deps: block.terminal.scope.dependencies,
|
||||
hasSingleInstr:
|
||||
scopeBlock.instructions.length === 1 &&
|
||||
scopeBlock.terminal.kind === 'goto' &&
|
||||
scopeBlock.terminal.block === block.terminal.fallthrough,
|
||||
});
|
||||
if (
|
||||
scopeBlock.instructions.length === 1 &&
|
||||
scopeBlock.terminal.kind === 'goto' &&
|
||||
scopeBlock.terminal.block === block.terminal.fallthrough
|
||||
) {
|
||||
scopeInfos.set(
|
||||
block.terminal.scope.id,
|
||||
block.terminal.scope.dependencies,
|
||||
);
|
||||
}
|
||||
}
|
||||
const rewriteInstrs = new Map<InstructionId, Array<Instruction>>();
|
||||
for (const instr of block.instructions) {
|
||||
@@ -164,22 +181,12 @@ export function inferEffectDependencies(fn: HIRFunction): void {
|
||||
fnExpr.lvalue.identifier.scope != null
|
||||
? scopeInfos.get(fnExpr.lvalue.identifier.scope.id)
|
||||
: null;
|
||||
CompilerError.invariant(scopeInfo != null, {
|
||||
reason: 'Expected function expression scope to exist',
|
||||
loc: value.loc,
|
||||
});
|
||||
if (scopeInfo.pruned || !scopeInfo.hasSingleInstr) {
|
||||
/**
|
||||
* TODO: retry pipeline that ensures effect function expressions
|
||||
* are placed into their own scope
|
||||
*/
|
||||
CompilerError.throwTodo({
|
||||
reason:
|
||||
'[InferEffectDependencies] Expected effect function to have non-pruned scope and its scope to have exactly one instruction',
|
||||
loc: fnExpr.loc,
|
||||
});
|
||||
let minimalDeps: Set<ReactiveScopeDependency>;
|
||||
if (scopeInfo != null) {
|
||||
minimalDeps = new Set(scopeInfo);
|
||||
} else {
|
||||
minimalDeps = inferMinimalDependencies(fnExpr);
|
||||
}
|
||||
|
||||
/**
|
||||
* Step 1: push dependencies to the effect deps array
|
||||
*
|
||||
@@ -187,11 +194,14 @@ export function inferEffectDependencies(fn: HIRFunction): void {
|
||||
* the `infer-effect-deps/pruned-nonreactive-obj` fixture for an
|
||||
* explanation.
|
||||
*/
|
||||
for (const dep of scopeInfo.deps) {
|
||||
|
||||
const usedDeps = [];
|
||||
for (const dep of minimalDeps) {
|
||||
if (
|
||||
(isUseRefType(dep.identifier) ||
|
||||
((isUseRefType(dep.identifier) ||
|
||||
isSetStateType(dep.identifier)) &&
|
||||
!reactiveIds.has(dep.identifier.id)
|
||||
!reactiveIds.has(dep.identifier.id)) ||
|
||||
isFireFunctionType(dep.identifier)
|
||||
) {
|
||||
// exclude non-reactive hook results, which will never be in a memo block
|
||||
continue;
|
||||
@@ -205,6 +215,23 @@ export function inferEffectDependencies(fn: HIRFunction): void {
|
||||
);
|
||||
newInstructions.push(...instructions);
|
||||
effectDeps.push(place);
|
||||
usedDeps.push(dep);
|
||||
}
|
||||
|
||||
// For LSP autodeps feature.
|
||||
const decorations: Array<t.SourceLocation> = [];
|
||||
for (const loc of collectDepUsages(usedDeps, fnExpr.value)) {
|
||||
if (typeof loc === 'symbol') {
|
||||
continue;
|
||||
}
|
||||
decorations.push(loc);
|
||||
}
|
||||
if (typeof value.loc !== 'symbol') {
|
||||
fn.env.logger?.logEvent(fn.env.filename, {
|
||||
kind: 'AutoDepsDecorations',
|
||||
fnLoc: value.loc,
|
||||
decorations,
|
||||
});
|
||||
}
|
||||
|
||||
newInstructions.push({
|
||||
@@ -230,6 +257,31 @@ export function inferEffectDependencies(fn: HIRFunction): void {
|
||||
rewriteInstrs.set(instr.id, newInstructions);
|
||||
fn.env.inferredEffectLocations.add(callee.loc);
|
||||
}
|
||||
} else if (
|
||||
value.args.length >= 2 &&
|
||||
value.args.length - 1 === autodepFnLoads.get(callee.identifier.id) &&
|
||||
value.args[0] != null &&
|
||||
value.args[0].kind === 'Identifier'
|
||||
) {
|
||||
const penultimateArg = value.args[value.args.length - 2];
|
||||
const depArrayArg = value.args[value.args.length - 1];
|
||||
if (
|
||||
depArrayArg.kind !== 'Spread' &&
|
||||
penultimateArg.kind !== 'Spread' &&
|
||||
typeof depArrayArg.loc !== 'symbol' &&
|
||||
typeof penultimateArg.loc !== 'symbol' &&
|
||||
typeof value.loc !== 'symbol'
|
||||
) {
|
||||
fn.env.logger?.logEvent(fn.env.filename, {
|
||||
kind: 'AutoDepsEligible',
|
||||
fnLoc: value.loc,
|
||||
depArrayLoc: {
|
||||
...depArrayArg.loc,
|
||||
start: penultimateArg.loc.end,
|
||||
end: depArrayArg.loc.end,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -338,3 +390,163 @@ function inferReactiveIdentifiers(fn: HIRFunction): Set<IdentifierId> {
|
||||
}
|
||||
return reactiveIds;
|
||||
}
|
||||
|
||||
function collectDepUsages(
|
||||
deps: Array<ReactiveScopeDependency>,
|
||||
fnExpr: FunctionExpression,
|
||||
): Array<SourceLocation> {
|
||||
const identifiers: Map<IdentifierId, ReactiveScopeDependency> = new Map();
|
||||
const loadedDeps: Set<IdentifierId> = new Set();
|
||||
const sourceLocations = [];
|
||||
for (const dep of deps) {
|
||||
identifiers.set(dep.identifier.id, dep);
|
||||
}
|
||||
|
||||
for (const [, block] of fnExpr.loweredFunc.func.body.blocks) {
|
||||
for (const instr of block.instructions) {
|
||||
if (
|
||||
instr.value.kind === 'LoadLocal' &&
|
||||
identifiers.has(instr.value.place.identifier.id)
|
||||
) {
|
||||
loadedDeps.add(instr.lvalue.identifier.id);
|
||||
}
|
||||
for (const place of eachInstructionOperand(instr)) {
|
||||
if (loadedDeps.has(place.identifier.id)) {
|
||||
// TODO(@jbrown215): handle member exprs!!
|
||||
sourceLocations.push(place.identifier.loc);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return sourceLocations;
|
||||
}
|
||||
|
||||
function inferMinimalDependencies(
|
||||
fnInstr: TInstruction<FunctionExpression>,
|
||||
): Set<ReactiveScopeDependency> {
|
||||
const fn = fnInstr.value.loweredFunc.func;
|
||||
|
||||
const temporaries = collectTemporariesSidemap(fn, new Set());
|
||||
const {
|
||||
hoistableObjects,
|
||||
processedInstrsInOptional,
|
||||
temporariesReadInOptional,
|
||||
} = collectOptionalChainSidemap(fn);
|
||||
|
||||
const hoistablePropertyLoads = collectHoistablePropertyLoadsInInnerFn(
|
||||
fnInstr,
|
||||
temporaries,
|
||||
hoistableObjects,
|
||||
);
|
||||
const hoistableToFnEntry = hoistablePropertyLoads.get(fn.body.entry);
|
||||
CompilerError.invariant(hoistableToFnEntry != null, {
|
||||
reason:
|
||||
'[InferEffectDependencies] Internal invariant broken: missing entry block',
|
||||
loc: fnInstr.loc,
|
||||
});
|
||||
|
||||
const dependencies = inferDependencies(
|
||||
fnInstr,
|
||||
new Map([...temporaries, ...temporariesReadInOptional]),
|
||||
processedInstrsInOptional,
|
||||
);
|
||||
|
||||
const tree = new ReactiveScopeDependencyTreeHIR(
|
||||
[...hoistableToFnEntry.assumedNonNullObjects].map(o => o.fullPath),
|
||||
);
|
||||
for (const dep of dependencies) {
|
||||
tree.addDependency({...dep});
|
||||
}
|
||||
|
||||
return tree.deriveMinimalDependencies();
|
||||
}
|
||||
|
||||
function inferDependencies(
|
||||
fnInstr: TInstruction<FunctionExpression>,
|
||||
temporaries: ReadonlyMap<IdentifierId, ReactiveScopeDependency>,
|
||||
processedInstrsInOptional: ReadonlySet<Instruction | Terminal>,
|
||||
): Set<ReactiveScopeDependency> {
|
||||
const fn = fnInstr.value.loweredFunc.func;
|
||||
const context = new DependencyCollectionContext(
|
||||
new Set(),
|
||||
temporaries,
|
||||
processedInstrsInOptional,
|
||||
);
|
||||
for (const dep of fn.context) {
|
||||
context.declare(dep.identifier, {
|
||||
id: makeInstructionId(0),
|
||||
scope: empty(),
|
||||
});
|
||||
}
|
||||
const placeholderScope: ReactiveScope = {
|
||||
id: makeScopeId(0),
|
||||
range: {
|
||||
start: fnInstr.id,
|
||||
end: makeInstructionId(fnInstr.id + 1),
|
||||
},
|
||||
dependencies: new Set(),
|
||||
reassignments: new Set(),
|
||||
declarations: new Map(),
|
||||
earlyReturnValue: null,
|
||||
merged: new Set(),
|
||||
loc: GeneratedSource,
|
||||
};
|
||||
context.enterScope(placeholderScope);
|
||||
inferDependenciesInFn(fn, context, temporaries);
|
||||
context.exitScope(placeholderScope, false);
|
||||
const resultUnfiltered = context.deps.get(placeholderScope);
|
||||
CompilerError.invariant(resultUnfiltered != null, {
|
||||
reason:
|
||||
'[InferEffectDependencies] Internal invariant broken: missing scope dependencies',
|
||||
loc: fn.loc,
|
||||
});
|
||||
|
||||
const fnContext = new Set(fn.context.map(dep => dep.identifier.id));
|
||||
const result = new Set<ReactiveScopeDependency>();
|
||||
for (const dep of resultUnfiltered) {
|
||||
if (fnContext.has(dep.identifier.id)) {
|
||||
result.add(dep);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function inferDependenciesInFn(
|
||||
fn: HIRFunction,
|
||||
context: DependencyCollectionContext,
|
||||
temporaries: ReadonlyMap<IdentifierId, ReactiveScopeDependency>,
|
||||
): void {
|
||||
for (const [, block] of fn.body.blocks) {
|
||||
// Record referenced optional chains in phis
|
||||
for (const phi of block.phis) {
|
||||
for (const operand of phi.operands) {
|
||||
const maybeOptionalChain = temporaries.get(operand[1].identifier.id);
|
||||
if (maybeOptionalChain) {
|
||||
context.visitDependency(maybeOptionalChain);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const instr of block.instructions) {
|
||||
if (
|
||||
instr.value.kind === 'FunctionExpression' ||
|
||||
instr.value.kind === 'ObjectMethod'
|
||||
) {
|
||||
context.declare(instr.lvalue.identifier, {
|
||||
id: instr.id,
|
||||
scope: context.currentScope,
|
||||
});
|
||||
/**
|
||||
* Recursively visit the inner function to extract dependencies
|
||||
*/
|
||||
const innerFn = instr.value.loweredFunc.func;
|
||||
context.enterInnerFn(instr as TInstruction<FunctionExpression>, () => {
|
||||
inferDependenciesInFn(innerFn, context, temporaries);
|
||||
});
|
||||
} else {
|
||||
handleInstruction(instr, context);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,14 +9,19 @@ import {CompilerError} from '..';
|
||||
import {
|
||||
BlockId,
|
||||
Effect,
|
||||
Environment,
|
||||
HIRFunction,
|
||||
Identifier,
|
||||
IdentifierId,
|
||||
Instruction,
|
||||
Place,
|
||||
computePostDominatorTree,
|
||||
evaluatesToStableTypeOrContainer,
|
||||
getHookKind,
|
||||
isStableType,
|
||||
isStableTypeContainer,
|
||||
isUseOperator,
|
||||
isUseRefType,
|
||||
} from '../HIR';
|
||||
import {PostDominator} from '../HIR/Dominator';
|
||||
import {
|
||||
@@ -31,6 +36,103 @@ import {
|
||||
import DisjointSet from '../Utils/DisjointSet';
|
||||
import {assertExhaustive} from '../Utils/utils';
|
||||
|
||||
/**
|
||||
* Side map to track and propagate sources of stability (i.e. hook calls such as
|
||||
* `useRef()` and property reads such as `useState()[1]). Note that this
|
||||
* requires forward data flow analysis since stability is not part of React
|
||||
* Compiler's type system.
|
||||
*/
|
||||
class StableSidemap {
|
||||
map: Map<IdentifierId, {isStable: boolean}> = new Map();
|
||||
env: Environment;
|
||||
|
||||
constructor(env: Environment) {
|
||||
this.env = env;
|
||||
}
|
||||
|
||||
handleInstruction(instr: Instruction): void {
|
||||
const {value, lvalue} = instr;
|
||||
|
||||
switch (value.kind) {
|
||||
case 'CallExpression':
|
||||
case 'MethodCall': {
|
||||
/**
|
||||
* Sources of stability are known hook calls
|
||||
*/
|
||||
if (evaluatesToStableTypeOrContainer(this.env, instr)) {
|
||||
if (isStableType(lvalue.identifier)) {
|
||||
this.map.set(lvalue.identifier.id, {
|
||||
isStable: true,
|
||||
});
|
||||
} else {
|
||||
this.map.set(lvalue.identifier.id, {
|
||||
isStable: false,
|
||||
});
|
||||
}
|
||||
} else if (
|
||||
this.env.config.enableTreatRefLikeIdentifiersAsRefs &&
|
||||
isUseRefType(lvalue.identifier)
|
||||
) {
|
||||
this.map.set(lvalue.identifier.id, {
|
||||
isStable: true,
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'Destructure':
|
||||
case 'PropertyLoad': {
|
||||
/**
|
||||
* PropertyLoads may from stable containers may also produce stable
|
||||
* values. ComputedLoads are technically safe for now (as all stable
|
||||
* containers have differently-typed elements), but are not handled as
|
||||
* they should be rare anyways.
|
||||
*/
|
||||
const source =
|
||||
value.kind === 'Destructure'
|
||||
? value.value.identifier.id
|
||||
: value.object.identifier.id;
|
||||
const entry = this.map.get(source);
|
||||
if (entry) {
|
||||
for (const lvalue of eachInstructionLValue(instr)) {
|
||||
if (isStableTypeContainer(lvalue.identifier)) {
|
||||
this.map.set(lvalue.identifier.id, {
|
||||
isStable: false,
|
||||
});
|
||||
} else if (isStableType(lvalue.identifier)) {
|
||||
this.map.set(lvalue.identifier.id, {
|
||||
isStable: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'StoreLocal': {
|
||||
const entry = this.map.get(value.value.identifier.id);
|
||||
if (entry) {
|
||||
this.map.set(lvalue.identifier.id, entry);
|
||||
this.map.set(value.lvalue.place.identifier.id, entry);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'LoadLocal': {
|
||||
const entry = this.map.get(value.place.identifier.id);
|
||||
if (entry) {
|
||||
this.map.set(lvalue.identifier.id, entry);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
isStable(id: IdentifierId): boolean {
|
||||
const entry = this.map.get(id);
|
||||
return entry != null ? entry.isStable : false;
|
||||
}
|
||||
}
|
||||
/*
|
||||
* Infers which `Place`s are reactive, ie may *semantically* change
|
||||
* over the course of the component/hook's lifetime. Places are reactive
|
||||
@@ -111,6 +213,7 @@ import {assertExhaustive} from '../Utils/utils';
|
||||
*/
|
||||
export function inferReactivePlaces(fn: HIRFunction): void {
|
||||
const reactiveIdentifiers = new ReactivityMap(findDisjointMutableValues(fn));
|
||||
const stableIdentifierSources = new StableSidemap(fn.env);
|
||||
for (const param of fn.params) {
|
||||
const place = param.kind === 'Identifier' ? param : param.place;
|
||||
reactiveIdentifiers.markReactive(place);
|
||||
@@ -184,6 +287,7 @@ export function inferReactivePlaces(fn: HIRFunction): void {
|
||||
}
|
||||
}
|
||||
for (const instruction of block.instructions) {
|
||||
stableIdentifierSources.handleInstruction(instruction);
|
||||
const {value} = instruction;
|
||||
let hasReactiveInput = false;
|
||||
/*
|
||||
@@ -218,7 +322,13 @@ export function inferReactivePlaces(fn: HIRFunction): void {
|
||||
|
||||
if (hasReactiveInput) {
|
||||
for (const lvalue of eachInstructionLValue(instruction)) {
|
||||
if (isStableType(lvalue.identifier)) {
|
||||
/**
|
||||
* Note that it's not correct to mark all stable-typed identifiers
|
||||
* as non-reactive, since ternaries and other value blocks can
|
||||
* produce reactive identifiers typed as these.
|
||||
* (e.g. `props.cond ? setState1 : setState2`)
|
||||
*/
|
||||
if (stableIdentifierSources.isStable(lvalue.identifier.id)) {
|
||||
continue;
|
||||
}
|
||||
reactiveIdentifiers.markReactive(lvalue);
|
||||
|
||||
@@ -48,7 +48,7 @@ import {printIdentifier, printPlace} from '../HIR/PrintHIR';
|
||||
import {eachPatternOperand} from '../HIR/visitors';
|
||||
import {Err, Ok, Result} from '../Utils/Result';
|
||||
import {GuardKind} from '../Utils/RuntimeDiagnosticConstants';
|
||||
import {assertExhaustive} from '../Utils/utils';
|
||||
import {assertExhaustive, hasOwnProperty} from '../Utils/utils';
|
||||
import {buildReactiveFunction} from './BuildReactiveFunction';
|
||||
import {SINGLE_CHILD_FBT_TAGS} from './MemoizeFbtAndMacroOperandsInSameScope';
|
||||
import {ReactiveFunctionVisitor, visitReactiveFunction} from './visitors';
|
||||
@@ -374,6 +374,8 @@ function codegenReactiveFunction(
|
||||
const countMemoBlockVisitor = new CountMemoBlockVisitor(fn.env);
|
||||
visitReactiveFunction(fn, countMemoBlockVisitor, undefined);
|
||||
|
||||
setMissingLocationsToNull(body);
|
||||
|
||||
return Ok({
|
||||
type: 'CodegenFunction',
|
||||
loc: fn.loc,
|
||||
@@ -2115,10 +2117,17 @@ function codegenInstructionValue(
|
||||
}
|
||||
case 'TypeCastExpression': {
|
||||
if (t.isTSType(instrValue.typeAnnotation)) {
|
||||
value = t.tsAsExpression(
|
||||
codegenPlaceToExpression(cx, instrValue.value),
|
||||
instrValue.typeAnnotation,
|
||||
);
|
||||
if (instrValue.typeAnnotationKind === 'satisfies') {
|
||||
value = t.tsSatisfiesExpression(
|
||||
codegenPlaceToExpression(cx, instrValue.value),
|
||||
instrValue.typeAnnotation,
|
||||
);
|
||||
} else {
|
||||
value = t.tsAsExpression(
|
||||
codegenPlaceToExpression(cx, instrValue.value),
|
||||
instrValue.typeAnnotation,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
value = t.typeCastExpression(
|
||||
codegenPlaceToExpression(cx, instrValue.value),
|
||||
@@ -2658,3 +2667,38 @@ function compareScopeDeclaration(
|
||||
else if (aName > bName) return 1;
|
||||
else return 0;
|
||||
}
|
||||
|
||||
function setMissingLocationsToNull(ast: any): void {
|
||||
if (Array.isArray(ast)) {
|
||||
ast.forEach(item => setMissingLocationsToNull(item));
|
||||
return;
|
||||
} else if (
|
||||
ast == null ||
|
||||
typeof ast !== 'object' ||
|
||||
typeof ast['type'] !== 'string'
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (ast['loc'] == null) {
|
||||
ast['loc'] = {
|
||||
start: {line: null, column: null, index: null},
|
||||
end: {line: null, column: null, index: null},
|
||||
filename: null,
|
||||
identifierName: null,
|
||||
};
|
||||
}
|
||||
for (const key in ast) {
|
||||
if (!hasOwnProperty(ast, key)) {
|
||||
continue;
|
||||
}
|
||||
const value = ast[key];
|
||||
if (typeof value !== 'object') {
|
||||
/*
|
||||
* We handle this above too, but avoid extra function calls in the majority of
|
||||
* cases where we're traversing an AST node's properties
|
||||
*/
|
||||
continue;
|
||||
}
|
||||
setMissingLocationsToNull(ast[key]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {
|
||||
BlockId,
|
||||
ReactiveFunction,
|
||||
|
||||
@@ -34,7 +34,11 @@ import {
|
||||
} from '../HIR';
|
||||
import {createTemporaryPlace, markInstructionIds} from '../HIR/HIRBuilder';
|
||||
import {getOrInsertWith} from '../Utils/utils';
|
||||
import {BuiltInFireId, DefaultNonmutatingHook} from '../HIR/ObjectShape';
|
||||
import {
|
||||
BuiltInFireFunctionId,
|
||||
BuiltInFireId,
|
||||
DefaultNonmutatingHook,
|
||||
} from '../HIR/ObjectShape';
|
||||
import {eachInstructionOperand} from '../HIR/visitors';
|
||||
import {printSourceLocationLine} from '../HIR/PrintHIR';
|
||||
import {USE_FIRE_FUNCTION_NAME} from '../HIR/Environment';
|
||||
@@ -633,6 +637,13 @@ class Context {
|
||||
() => createTemporaryPlace(this.#env, GeneratedSource),
|
||||
);
|
||||
|
||||
fireFunctionBinding.identifier.type = {
|
||||
kind: 'Function',
|
||||
shapeId: BuiltInFireFunctionId,
|
||||
return: {kind: 'Poly'},
|
||||
isConstructor: false,
|
||||
};
|
||||
|
||||
this.#capturedCalleeIdentifierIds.set(callee.identifier.id, {
|
||||
fireFunctionBinding,
|
||||
capturedCalleeIdentifier: callee.identifier,
|
||||
|
||||
@@ -4,4 +4,5 @@
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
export {transformFire} from './TransformFire';
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {CompilerError, EnvironmentConfig, ErrorSeverity} from '..';
|
||||
import {HIRFunction, IdentifierId} from '../HIR';
|
||||
import {DEFAULT_GLOBALS} from '../HIR/Globals';
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @inferEffectDependencies @panicThreshold(none)
|
||||
import {useEffect} from 'react';
|
||||
import {print} from 'shared-runtime';
|
||||
|
||||
function Component({foo}) {
|
||||
const arr = [];
|
||||
// Taking either arr[0].value or arr as a dependency is reasonable
|
||||
// as long as developers know what to expect.
|
||||
useEffect(() => print(arr[0].value));
|
||||
arr.push({value: foo});
|
||||
return arr;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
// @inferEffectDependencies @panicThreshold(none)
|
||||
import { useEffect } from "react";
|
||||
import { print } from "shared-runtime";
|
||||
|
||||
function Component(t0) {
|
||||
const { foo } = t0;
|
||||
const arr = [];
|
||||
|
||||
useEffect(() => print(arr[0].value), [arr[0].value]);
|
||||
arr.push({ value: foo });
|
||||
return arr;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: exception) Fixture not implemented
|
||||
@@ -0,0 +1,12 @@
|
||||
// @inferEffectDependencies @panicThreshold(none)
|
||||
import {useEffect} from 'react';
|
||||
import {print} from 'shared-runtime';
|
||||
|
||||
function Component({foo}) {
|
||||
const arr = [];
|
||||
// Taking either arr[0].value or arr as a dependency is reasonable
|
||||
// as long as developers know what to expect.
|
||||
useEffect(() => print(arr[0].value));
|
||||
arr.push({value: foo});
|
||||
return arr;
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @inferEffectDependencies @panicThreshold(none)
|
||||
|
||||
import {useEffect, useRef} from 'react';
|
||||
import {print} from 'shared-runtime';
|
||||
|
||||
function Component({arrRef}) {
|
||||
// Avoid taking arr.current as a dependency
|
||||
useEffect(() => print(arrRef.current));
|
||||
arrRef.current.val = 2;
|
||||
return arrRef;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
// @inferEffectDependencies @panicThreshold(none)
|
||||
|
||||
import { useEffect, useRef } from "react";
|
||||
import { print } from "shared-runtime";
|
||||
|
||||
function Component(t0) {
|
||||
const { arrRef } = t0;
|
||||
|
||||
useEffect(() => print(arrRef.current), [arrRef]);
|
||||
arrRef.current.val = 2;
|
||||
return arrRef;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: exception) Fixture not implemented
|
||||
@@ -0,0 +1,11 @@
|
||||
// @inferEffectDependencies @panicThreshold(none)
|
||||
|
||||
import {useEffect, useRef} from 'react';
|
||||
import {print} from 'shared-runtime';
|
||||
|
||||
function Component({arrRef}) {
|
||||
// Avoid taking arr.current as a dependency
|
||||
useEffect(() => print(arrRef.current));
|
||||
arrRef.current.val = 2;
|
||||
return arrRef;
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @inferEffectDependencies @panicThreshold(none)
|
||||
import {useEffect} from 'react';
|
||||
|
||||
function Component({foo}) {
|
||||
const arr = [];
|
||||
useEffect(() => arr.push(foo));
|
||||
arr.push(2);
|
||||
return arr;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
// @inferEffectDependencies @panicThreshold(none)
|
||||
import { useEffect } from "react";
|
||||
|
||||
function Component(t0) {
|
||||
const { foo } = t0;
|
||||
const arr = [];
|
||||
useEffect(() => arr.push(foo), [arr, foo]);
|
||||
arr.push(2);
|
||||
return arr;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: exception) Fixture not implemented
|
||||
@@ -0,0 +1,9 @@
|
||||
// @inferEffectDependencies @panicThreshold(none)
|
||||
import {useEffect} from 'react';
|
||||
|
||||
function Component({foo}) {
|
||||
const arr = [];
|
||||
useEffect(() => arr.push(foo));
|
||||
arr.push(2);
|
||||
return arr;
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @inferEffectDependencies
|
||||
import {useRef, useEffect} from 'react';
|
||||
import {print, mutate} from 'shared-runtime';
|
||||
|
||||
function Component({cond}) {
|
||||
const arr = useRef([]);
|
||||
const other = useRef([]);
|
||||
// Although arr and other are both stable, derived is not
|
||||
const derived = cond ? arr : other;
|
||||
useEffect(() => {
|
||||
mutate(derived.current);
|
||||
print(derived.current);
|
||||
});
|
||||
return arr;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @inferEffectDependencies
|
||||
import { useRef, useEffect } from "react";
|
||||
import { print, mutate } from "shared-runtime";
|
||||
|
||||
function Component(t0) {
|
||||
const $ = _c(4);
|
||||
const { cond } = t0;
|
||||
let t1;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = [];
|
||||
$[0] = t1;
|
||||
} else {
|
||||
t1 = $[0];
|
||||
}
|
||||
const arr = useRef(t1);
|
||||
let t2;
|
||||
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t2 = [];
|
||||
$[1] = t2;
|
||||
} else {
|
||||
t2 = $[1];
|
||||
}
|
||||
const other = useRef(t2);
|
||||
|
||||
const derived = cond ? arr : other;
|
||||
let t3;
|
||||
if ($[2] !== derived) {
|
||||
t3 = () => {
|
||||
mutate(derived.current);
|
||||
print(derived.current);
|
||||
};
|
||||
$[2] = derived;
|
||||
$[3] = t3;
|
||||
} else {
|
||||
t3 = $[3];
|
||||
}
|
||||
useEffect(t3, [derived]);
|
||||
return arr;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: exception) Fixture not implemented
|
||||
@@ -0,0 +1,15 @@
|
||||
// @inferEffectDependencies
|
||||
import {useRef, useEffect} from 'react';
|
||||
import {print, mutate} from 'shared-runtime';
|
||||
|
||||
function Component({cond}) {
|
||||
const arr = useRef([]);
|
||||
const other = useRef([]);
|
||||
// Although arr and other are both stable, derived is not
|
||||
const derived = cond ? arr : other;
|
||||
useEffect(() => {
|
||||
mutate(derived.current);
|
||||
print(derived.current);
|
||||
});
|
||||
return arr;
|
||||
}
|
||||
@@ -83,10 +83,10 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
import { c as _c2 } from "react/compiler-runtime"; // @inlineJsxTransform
|
||||
|
||||
function Parent(t0) {
|
||||
const $ = _c2(2);
|
||||
const $ = _c2(3);
|
||||
const { children, ref } = t0;
|
||||
let t1;
|
||||
if ($[0] !== children) {
|
||||
if ($[0] !== children || $[1] !== ref) {
|
||||
if (DEV) {
|
||||
t1 = <div ref={ref}>{children}</div>;
|
||||
} else {
|
||||
@@ -99,9 +99,10 @@ function Parent(t0) {
|
||||
};
|
||||
}
|
||||
$[0] = children;
|
||||
$[1] = t1;
|
||||
$[1] = ref;
|
||||
$[2] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
t1 = $[2];
|
||||
}
|
||||
return t1;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @sourceMaps
|
||||
export const Button = () => {
|
||||
return <button>Click me</button>;
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @sourceMaps
|
||||
export const Button = () => {
|
||||
const $ = _c(1);
|
||||
let t0;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t0 = <button>Click me</button>;
|
||||
$[0] = t0;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
}
|
||||
return t0;
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Source Map
|
||||
|
||||
```
|
||||
{
|
||||
"version": 3,
|
||||
"names": [
|
||||
"Button",
|
||||
"t0"
|
||||
],
|
||||
"sources": [
|
||||
"sourcemaps-simple.ts"
|
||||
],
|
||||
"sourcesContent": [
|
||||
"// @sourceMaps\nexport const Button = () => {\n return <button>Click me</button>;\n};\n"
|
||||
],
|
||||
"mappings": "kDAAA;AACA,OAAO,MAAMA,MAAM,GAAGA,CAAA,K;SACb,OAAyB,CAAjB,QAAQ,EAAhB,MAAyB,C,qCAAzBC,EAAyB,C,CACjC",
|
||||
"ignoreList": []
|
||||
}
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: exception) Fixture not implemented
|
||||
@@ -0,0 +1,4 @@
|
||||
// @sourceMaps
|
||||
export const Button = () => {
|
||||
return <button>Click me</button>;
|
||||
};
|
||||
@@ -1,42 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @inferEffectDependencies @panicThreshold(none)
|
||||
import {useRef} from 'react';
|
||||
import {useSpecialEffect} from 'shared-runtime';
|
||||
|
||||
/**
|
||||
* The retry pipeline disables memoization features, which means we need to
|
||||
* provide an alternate implementation of effect dependencies which does not
|
||||
* rely on memoization.
|
||||
*/
|
||||
function useFoo({cond}) {
|
||||
const ref = useRef();
|
||||
const derived = cond ? ref.current : makeObject();
|
||||
useSpecialEffect(() => {
|
||||
log(derived);
|
||||
}, [derived]);
|
||||
return ref;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
11 | const ref = useRef();
|
||||
12 | const derived = cond ? ref.current : makeObject();
|
||||
> 13 | useSpecialEffect(() => {
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
> 14 | log(derived);
|
||||
| ^^^^^^^^^^^^^^^^^
|
||||
> 15 | }, [derived]);
|
||||
| ^^^^^^^^^^^^^^^^ InvalidReact: [InferEffectDependencies] React Compiler is unable to infer dependencies of this effect. This will break your build! To resolve, either pass your own dependency array or fix reported compiler bailout diagnostics.. (Bailout reason: Invariant: Expected function expression scope to exist (13:15)) (13:15)
|
||||
16 | return ref;
|
||||
17 | }
|
||||
18 |
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @inferEffectDependencies @panicThreshold(none)
|
||||
import {useRef} from 'react';
|
||||
import {useSpecialEffect} from 'shared-runtime';
|
||||
|
||||
/**
|
||||
* The retry pipeline disables memoization features, which means we need to
|
||||
* provide an alternate implementation of effect dependencies which does not
|
||||
* rely on memoization.
|
||||
*/
|
||||
function useFoo({cond}) {
|
||||
const ref = useRef();
|
||||
const derived = cond ? ref.current : makeObject();
|
||||
useSpecialEffect(() => {
|
||||
log(derived);
|
||||
}, [derived]);
|
||||
return ref;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
// @inferEffectDependencies @panicThreshold(none)
|
||||
import { useRef } from "react";
|
||||
import { useSpecialEffect } from "shared-runtime";
|
||||
|
||||
/**
|
||||
* The retry pipeline disables memoization features, which means we need to
|
||||
* provide an alternate implementation of effect dependencies which does not
|
||||
* rely on memoization.
|
||||
*/
|
||||
function useFoo(t0) {
|
||||
const { cond } = t0;
|
||||
const ref = useRef();
|
||||
const derived = cond ? ref.current : makeObject();
|
||||
useSpecialEffect(
|
||||
() => {
|
||||
log(derived);
|
||||
},
|
||||
[derived],
|
||||
[derived],
|
||||
);
|
||||
return ref;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: exception) Fixture not implemented
|
||||
@@ -49,7 +49,7 @@ function Component(props) {
|
||||
} else {
|
||||
t2 = $[4];
|
||||
}
|
||||
useEffect(t2, [t1, props]);
|
||||
useEffect(t2, [props]);
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @enableUseTypeAnnotations
|
||||
function Component(props: {id: number}) {
|
||||
const x = makeArray(props.id) satisfies number[];
|
||||
const y = x.at(0);
|
||||
return y;
|
||||
}
|
||||
|
||||
function makeArray<T>(x: T): Array<T> {
|
||||
return [x];
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{id: 42}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @enableUseTypeAnnotations
|
||||
function Component(props) {
|
||||
const $ = _c(4);
|
||||
let t0;
|
||||
if ($[0] !== props.id) {
|
||||
t0 = makeArray(props.id);
|
||||
$[0] = props.id;
|
||||
$[1] = t0;
|
||||
} else {
|
||||
t0 = $[1];
|
||||
}
|
||||
const x = t0 satisfies number[];
|
||||
let t1;
|
||||
if ($[2] !== x) {
|
||||
t1 = x.at(0);
|
||||
$[2] = x;
|
||||
$[3] = t1;
|
||||
} else {
|
||||
t1 = $[3];
|
||||
}
|
||||
const y = t1;
|
||||
return y;
|
||||
}
|
||||
|
||||
function makeArray(x) {
|
||||
const $ = _c(2);
|
||||
let t0;
|
||||
if ($[0] !== x) {
|
||||
t0 = [x];
|
||||
$[0] = x;
|
||||
$[1] = t0;
|
||||
} else {
|
||||
t0 = $[1];
|
||||
}
|
||||
return t0;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ id: 42 }],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) 42
|
||||
@@ -0,0 +1,15 @@
|
||||
// @enableUseTypeAnnotations
|
||||
function Component(props: {id: number}) {
|
||||
const x = makeArray(props.id) satisfies number[];
|
||||
const y = x.at(0);
|
||||
return y;
|
||||
}
|
||||
|
||||
function makeArray<T>(x: T): Array<T> {
|
||||
return [x];
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{id: 42}],
|
||||
};
|
||||
@@ -0,0 +1,41 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @enableUseTypeAnnotations
|
||||
import {identity} from 'shared-runtime';
|
||||
|
||||
function Component(props: {id: number}) {
|
||||
const x = identity(props.id);
|
||||
const y = x satisfies number;
|
||||
return y;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{id: 42}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
// @enableUseTypeAnnotations
|
||||
import { identity } from "shared-runtime";
|
||||
|
||||
function Component(props) {
|
||||
const x = identity(props.id);
|
||||
const y = x satisfies number;
|
||||
return y;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ id: 42 }],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) 42
|
||||
@@ -0,0 +1,13 @@
|
||||
// @enableUseTypeAnnotations
|
||||
import {identity} from 'shared-runtime';
|
||||
|
||||
function Component(props: {id: number}) {
|
||||
const x = identity(props.id);
|
||||
const y = x satisfies number;
|
||||
return y;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{id: 42}],
|
||||
};
|
||||
@@ -1,3 +1,10 @@
|
||||
/**
|
||||
* 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 {defineConfig} from 'tsup';
|
||||
|
||||
export default defineConfig({
|
||||
|
||||
@@ -92,36 +92,8 @@ const tests: CompilerTestCases = {
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
// Don't report the issue if Flow already has
|
||||
name: '[InvalidInput] Ref access during render',
|
||||
code: normalizeIndent`
|
||||
function Component(props) {
|
||||
const ref = useRef(null);
|
||||
// $FlowFixMe[react-rule-unsafe-ref]
|
||||
const value = ref.current;
|
||||
return value;
|
||||
}
|
||||
`,
|
||||
},
|
||||
],
|
||||
invalid: [
|
||||
{
|
||||
name: '[InvalidInput] Ref access during render',
|
||||
code: normalizeIndent`
|
||||
function Component(props) {
|
||||
const ref = useRef(null);
|
||||
const value = ref.current;
|
||||
return value;
|
||||
}
|
||||
`,
|
||||
errors: [
|
||||
{
|
||||
message:
|
||||
'Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Reportable levels can be configured',
|
||||
options: [{reportableLevels: new Set([ErrorSeverity.Todo])}],
|
||||
|
||||
@@ -105,6 +105,9 @@ const COMPILER_OPTIONS: Partial<PluginOptions> = {
|
||||
panicThreshold: 'none',
|
||||
// Don't emit errors on Flow suppressions--Flow already gave a signal
|
||||
flowSuppressions: false,
|
||||
environment: validateEnvironmentConfig({
|
||||
validateRefAccessDuringRender: false,
|
||||
}),
|
||||
};
|
||||
|
||||
const rule: Rule.RuleModule = {
|
||||
@@ -149,10 +152,14 @@ const rule: Rule.RuleModule = {
|
||||
}
|
||||
|
||||
let shouldReportUnusedOptOutDirective = true;
|
||||
const options: PluginOptions = {
|
||||
...parsePluginOptions(userOpts),
|
||||
const options: PluginOptions = parsePluginOptions({
|
||||
...COMPILER_OPTIONS,
|
||||
};
|
||||
...userOpts,
|
||||
environment: {
|
||||
...COMPILER_OPTIONS.environment,
|
||||
...userOpts.environment,
|
||||
},
|
||||
});
|
||||
const userLogger: Logger | null = options.logger;
|
||||
options.logger = {
|
||||
logEvent: (filename, event): void => {
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
/**
|
||||
* 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 {defineConfig} from 'tsup';
|
||||
|
||||
export default defineConfig({
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
/**
|
||||
* 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 {defineConfig} from 'tsup';
|
||||
|
||||
export default defineConfig({
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
/**
|
||||
* 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 = {
|
||||
knownIncompatibleLibraries: [
|
||||
'mobx-react',
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
/**
|
||||
* 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 {defineConfig} from 'tsup';
|
||||
|
||||
export default defineConfig({
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
/**
|
||||
* 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 {defineConfig} from 'tsup';
|
||||
|
||||
export default defineConfig({
|
||||
|
||||
106
compiler/packages/react-forgive/client/src/autodeps.ts
Normal file
106
compiler/packages/react-forgive/client/src/autodeps.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
/**
|
||||
* 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 * as vscode from 'vscode';
|
||||
import {
|
||||
LanguageClient,
|
||||
RequestType,
|
||||
type Position,
|
||||
} from 'vscode-languageclient/node';
|
||||
import {positionLiteralToVSCodePosition, positionsToRange} from './mapping';
|
||||
|
||||
export type AutoDepsDecorationsLSPEvent = {
|
||||
useEffectCallExpr: [Position, Position];
|
||||
decorations: Array<[Position, Position]>;
|
||||
};
|
||||
|
||||
export interface AutoDepsDecorationsParams {
|
||||
position: Position;
|
||||
}
|
||||
|
||||
export namespace AutoDepsDecorationsRequest {
|
||||
export const type = new RequestType<
|
||||
AutoDepsDecorationsParams,
|
||||
AutoDepsDecorationsLSPEvent | null,
|
||||
void
|
||||
>('react/autodeps_decorations');
|
||||
}
|
||||
|
||||
const inferredEffectDepDecoration =
|
||||
vscode.window.createTextEditorDecorationType({
|
||||
// TODO: make configurable?
|
||||
borderColor: new vscode.ThemeColor('diffEditor.move.border'),
|
||||
borderStyle: 'solid',
|
||||
borderWidth: '0 0 4px 0',
|
||||
});
|
||||
|
||||
let currentlyDecoratedAutoDepFnLoc: vscode.Range | null = null;
|
||||
export function getCurrentlyDecoratedAutoDepFnLoc(): vscode.Range | null {
|
||||
return currentlyDecoratedAutoDepFnLoc;
|
||||
}
|
||||
export function setCurrentlyDecoratedAutoDepFnLoc(range: vscode.Range): void {
|
||||
currentlyDecoratedAutoDepFnLoc = range;
|
||||
}
|
||||
export function clearCurrentlyDecoratedAutoDepFnLoc(): void {
|
||||
currentlyDecoratedAutoDepFnLoc = null;
|
||||
}
|
||||
|
||||
let decorationRequestId = 0;
|
||||
export type AutoDepsDecorationsOptions = {
|
||||
shouldUpdateCurrent: boolean;
|
||||
};
|
||||
export function requestAutoDepsDecorations(
|
||||
client: LanguageClient,
|
||||
position: vscode.Position,
|
||||
options: AutoDepsDecorationsOptions,
|
||||
) {
|
||||
const id = ++decorationRequestId;
|
||||
client
|
||||
.sendRequest(AutoDepsDecorationsRequest.type, {position})
|
||||
.then(response => {
|
||||
if (response !== null) {
|
||||
const {
|
||||
decorations,
|
||||
useEffectCallExpr: [start, end],
|
||||
} = response;
|
||||
// Maintain ordering
|
||||
if (decorationRequestId === id) {
|
||||
if (options.shouldUpdateCurrent) {
|
||||
setCurrentlyDecoratedAutoDepFnLoc(positionsToRange(start, end));
|
||||
}
|
||||
drawInferredEffectDepDecorations(decorations);
|
||||
}
|
||||
} else {
|
||||
clearCurrentlyDecoratedAutoDepFnLoc();
|
||||
clearDecorations(inferredEffectDepDecoration);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function drawInferredEffectDepDecorations(
|
||||
decorations: Array<[Position, Position]>,
|
||||
): void {
|
||||
const decorationOptions = decorations.map(([start, end]) => {
|
||||
return {
|
||||
range: new vscode.Range(
|
||||
positionLiteralToVSCodePosition(start),
|
||||
positionLiteralToVSCodePosition(end),
|
||||
),
|
||||
hoverMessage: 'Inferred as an effect dependency',
|
||||
};
|
||||
});
|
||||
vscode.window.activeTextEditor?.setDecorations(
|
||||
inferredEffectDepDecoration,
|
||||
decorationOptions,
|
||||
);
|
||||
}
|
||||
|
||||
export function clearDecorations(
|
||||
decorationType: vscode.TextEditorDecorationType,
|
||||
) {
|
||||
vscode.window.activeTextEditor?.setDecorations(decorationType, []);
|
||||
}
|
||||
80
compiler/packages/react-forgive/client/src/colors.ts
Normal file
80
compiler/packages/react-forgive/client/src/colors.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
type RGB = [number, number, number];
|
||||
|
||||
const int = Math.floor;
|
||||
|
||||
export class Color {
|
||||
constructor(
|
||||
private r: number,
|
||||
private g: number,
|
||||
private b: number,
|
||||
) {}
|
||||
|
||||
toAlphaString(a: number) {
|
||||
return this.toCssString(a);
|
||||
}
|
||||
toString() {
|
||||
return this.toCssString(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adjust the color by a multiplier to lighten (`> 1.0`) or darken (`< 1.0`) the color. Returns a new
|
||||
* instance.
|
||||
*/
|
||||
adjusted(mult: number) {
|
||||
const adjusted = Color.redistribute([
|
||||
this.r * mult,
|
||||
this.g * mult,
|
||||
this.b * mult,
|
||||
]);
|
||||
return new Color(...adjusted);
|
||||
}
|
||||
|
||||
private toCssString(a: number) {
|
||||
return `rgba(${this.r},${this.g},${this.b},${a})`;
|
||||
}
|
||||
/**
|
||||
* Redistributes rgb, maintaing hue until its clamped.
|
||||
* https://stackoverflow.com/a/141943
|
||||
*/
|
||||
private static redistribute([r, g, b]: RGB): RGB {
|
||||
const threshold = 255.999;
|
||||
const max = Math.max(r, g, b);
|
||||
if (max <= threshold) {
|
||||
return [int(r), int(g), int(b)];
|
||||
}
|
||||
const total = r + g + b;
|
||||
if (total >= 3 * threshold) {
|
||||
return [int(threshold), int(threshold), int(threshold)];
|
||||
}
|
||||
const x = (3 * threshold - total) / (3 * max - total);
|
||||
const gray = threshold - x * max;
|
||||
return [int(gray + x * r), int(gray + x * g), int(gray + x * b)];
|
||||
}
|
||||
}
|
||||
|
||||
export const BLACK = new Color(0, 0, 0);
|
||||
export const WHITE = new Color(255, 255, 255);
|
||||
|
||||
const COLOR_POOL = [
|
||||
new Color(249, 65, 68),
|
||||
new Color(243, 114, 44),
|
||||
new Color(248, 150, 30),
|
||||
new Color(249, 132, 74),
|
||||
new Color(249, 199, 79),
|
||||
new Color(144, 190, 109),
|
||||
new Color(67, 170, 139),
|
||||
new Color(77, 144, 142),
|
||||
new Color(87, 117, 144),
|
||||
new Color(39, 125, 161),
|
||||
];
|
||||
|
||||
export function getColorFor(index: number): Color {
|
||||
return COLOR_POOL[Math.abs(index) % COLOR_POOL.length]!;
|
||||
}
|
||||
@@ -1,17 +1,34 @@
|
||||
/**
|
||||
* 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 * as path from 'path';
|
||||
import {ExtensionContext, window as Window} from 'vscode';
|
||||
import * as vscode from 'vscode';
|
||||
|
||||
import {
|
||||
LanguageClient,
|
||||
LanguageClientOptions,
|
||||
type Position,
|
||||
ServerOptions,
|
||||
TransportKind,
|
||||
} from 'vscode-languageclient/node';
|
||||
import {positionLiteralToVSCodePosition} from './mapping';
|
||||
import {
|
||||
getCurrentlyDecoratedAutoDepFnLoc,
|
||||
requestAutoDepsDecorations,
|
||||
} from './autodeps';
|
||||
|
||||
let client: LanguageClient;
|
||||
|
||||
export function activate(context: ExtensionContext) {
|
||||
export function activate(context: vscode.ExtensionContext) {
|
||||
const serverModule = context.asAbsolutePath(path.join('dist', 'server.js'));
|
||||
const documentSelector = [
|
||||
{scheme: 'file', language: 'javascriptreact'},
|
||||
{scheme: 'file', language: 'typescriptreact'},
|
||||
];
|
||||
|
||||
// If the extension is launched in debug mode then the debug server options are used
|
||||
// Otherwise the run options are used
|
||||
@@ -27,10 +44,7 @@ export function activate(context: ExtensionContext) {
|
||||
};
|
||||
|
||||
const clientOptions: LanguageClientOptions = {
|
||||
documentSelector: [
|
||||
{scheme: 'file', language: 'javascriptreact'},
|
||||
{scheme: 'file', language: 'typescriptreact'},
|
||||
],
|
||||
documentSelector,
|
||||
progressOnInitialization: true,
|
||||
};
|
||||
|
||||
@@ -43,12 +57,39 @@ export function activate(context: ExtensionContext) {
|
||||
clientOptions,
|
||||
);
|
||||
} catch {
|
||||
Window.showErrorMessage(
|
||||
vscode.window.showErrorMessage(
|
||||
`React Analyzer couldn't be started. See the output channel for details.`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
vscode.languages.registerHoverProvider(documentSelector, {
|
||||
provideHover(_document, position, _token) {
|
||||
requestAutoDepsDecorations(client, position, {shouldUpdateCurrent: true});
|
||||
return null;
|
||||
},
|
||||
});
|
||||
|
||||
vscode.workspace.onDidChangeTextDocument(async _e => {
|
||||
const currentlyDecoratedAutoDepFnLoc = getCurrentlyDecoratedAutoDepFnLoc();
|
||||
if (currentlyDecoratedAutoDepFnLoc !== null) {
|
||||
requestAutoDepsDecorations(client, currentlyDecoratedAutoDepFnLoc.start, {
|
||||
shouldUpdateCurrent: false,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
vscode.commands.registerCommand(
|
||||
'react.requestAutoDepsDecorations',
|
||||
(position: Position) => {
|
||||
requestAutoDepsDecorations(
|
||||
client,
|
||||
positionLiteralToVSCodePosition(position),
|
||||
{shouldUpdateCurrent: true},
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
client.registerProposedFeatures();
|
||||
client.start();
|
||||
}
|
||||
@@ -57,4 +98,5 @@ export function deactivate(): Thenable<void> | undefined {
|
||||
if (client !== undefined) {
|
||||
return client.stop();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
22
compiler/packages/react-forgive/client/src/mapping.ts
Normal file
22
compiler/packages/react-forgive/client/src/mapping.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* 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 * as vscode from 'vscode';
|
||||
import {Position} from 'vscode-languageclient/node';
|
||||
|
||||
export function positionLiteralToVSCodePosition(
|
||||
position: Position,
|
||||
): vscode.Position {
|
||||
return new vscode.Position(position.line, position.character);
|
||||
}
|
||||
|
||||
export function positionsToRange(start: Position, end: Position): vscode.Range {
|
||||
return new vscode.Range(
|
||||
positionLiteralToVSCodePosition(start),
|
||||
positionLiteralToVSCodePosition(end),
|
||||
);
|
||||
}
|
||||
@@ -1,3 +1,10 @@
|
||||
/**
|
||||
* 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 {SourceLocation} from 'babel-plugin-react-compiler/src';
|
||||
import {type Range} from 'vscode-languageserver';
|
||||
|
||||
|
||||
@@ -33,6 +33,8 @@ export async function compile({
|
||||
plugins: ['typescript', 'jsx'],
|
||||
},
|
||||
sourceType: 'module',
|
||||
configFile: false,
|
||||
babelrc: false,
|
||||
});
|
||||
if (ast == null) {
|
||||
return null;
|
||||
@@ -48,6 +50,8 @@ export async function compile({
|
||||
plugins,
|
||||
sourceType: 'module',
|
||||
sourceFileName: file,
|
||||
configFile: false,
|
||||
babelrc: false,
|
||||
});
|
||||
if (result?.code == null) {
|
||||
throw new Error(
|
||||
|
||||
@@ -7,10 +7,14 @@
|
||||
|
||||
import {TextDocument} from 'vscode-languageserver-textdocument';
|
||||
import {
|
||||
CodeAction,
|
||||
CodeActionKind,
|
||||
CodeLens,
|
||||
Command,
|
||||
createConnection,
|
||||
type InitializeParams,
|
||||
type InitializeResult,
|
||||
Position,
|
||||
ProposedFeatures,
|
||||
TextDocuments,
|
||||
TextDocumentSyncKind,
|
||||
@@ -19,11 +23,22 @@ import {compile, lastResult} from './compiler';
|
||||
import {type PluginOptions} from 'babel-plugin-react-compiler/src';
|
||||
import {resolveReactConfig} from './compiler/options';
|
||||
import {
|
||||
CompileSuccessEvent,
|
||||
type CompileSuccessEvent,
|
||||
type LoggerEvent,
|
||||
defaultOptions,
|
||||
LoggerEvent,
|
||||
} from 'babel-plugin-react-compiler/src/Entrypoint/Options';
|
||||
import {babelLocationToRange, getRangeFirstCharacter} from './compiler/compat';
|
||||
import {
|
||||
type AutoDepsDecorationsLSPEvent,
|
||||
AutoDepsDecorationsRequest,
|
||||
mapCompilerEventToLSPEvent,
|
||||
} from './requests/autodepsdecorations';
|
||||
import {
|
||||
isPositionWithinRange,
|
||||
isRangeWithinRange,
|
||||
Range,
|
||||
sourceLocationToRange,
|
||||
} from './utils/range';
|
||||
|
||||
const SUPPORTED_LANGUAGE_IDS = new Set([
|
||||
'javascript',
|
||||
@@ -37,17 +52,68 @@ const documents = new TextDocuments(TextDocument);
|
||||
|
||||
let compilerOptions: PluginOptions | null = null;
|
||||
let compiledFns: Set<CompileSuccessEvent> = new Set();
|
||||
let autoDepsDecorations: Array<AutoDepsDecorationsLSPEvent> = [];
|
||||
let codeActionEvents: Array<CodeActionLSPEvent> = [];
|
||||
|
||||
type CodeActionLSPEvent = {
|
||||
title: string;
|
||||
kind: CodeActionKind;
|
||||
newText: string;
|
||||
anchorRange: Range;
|
||||
editRange: {start: Position; end: Position};
|
||||
};
|
||||
|
||||
connection.onInitialize((_params: InitializeParams) => {
|
||||
// TODO(@poteto) get config fr
|
||||
compilerOptions = resolveReactConfig('.') ?? defaultOptions;
|
||||
compilerOptions = {
|
||||
...compilerOptions,
|
||||
environment: {
|
||||
...compilerOptions.environment,
|
||||
inferEffectDependencies: [
|
||||
{
|
||||
function: {
|
||||
importSpecifierName: 'useEffect',
|
||||
source: 'react',
|
||||
},
|
||||
numRequiredArgs: 1,
|
||||
},
|
||||
{
|
||||
function: {
|
||||
importSpecifierName: 'useSpecialEffect',
|
||||
source: 'shared-runtime',
|
||||
},
|
||||
numRequiredArgs: 2,
|
||||
},
|
||||
{
|
||||
function: {
|
||||
importSpecifierName: 'default',
|
||||
source: 'useEffectWrapper',
|
||||
},
|
||||
numRequiredArgs: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
logger: {
|
||||
logEvent(_filename: string | null, event: LoggerEvent) {
|
||||
connection.console.info(`Received event: ${event.kind}`);
|
||||
connection.console.debug(JSON.stringify(event, null, 2));
|
||||
if (event.kind === 'CompileSuccess') {
|
||||
compiledFns.add(event);
|
||||
}
|
||||
if (event.kind === 'AutoDepsDecorations') {
|
||||
autoDepsDecorations.push(mapCompilerEventToLSPEvent(event));
|
||||
}
|
||||
if (event.kind === 'AutoDepsEligible') {
|
||||
const depArrayLoc = sourceLocationToRange(event.depArrayLoc);
|
||||
codeActionEvents.push({
|
||||
title: 'Use React Compiler inferred dependency array',
|
||||
kind: CodeActionKind.QuickFix,
|
||||
newText: '',
|
||||
anchorRange: sourceLocationToRange(event.fnLoc),
|
||||
editRange: {start: depArrayLoc[0], end: depArrayLoc[1]},
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -55,6 +121,7 @@ connection.onInitialize((_params: InitializeParams) => {
|
||||
capabilities: {
|
||||
textDocumentSync: TextDocumentSyncKind.Full,
|
||||
codeLensProvider: {resolveProvider: true},
|
||||
codeActionProvider: {resolveProvider: true},
|
||||
},
|
||||
};
|
||||
return result;
|
||||
@@ -65,20 +132,29 @@ connection.onInitialized(() => {
|
||||
});
|
||||
|
||||
documents.onDidChangeContent(async event => {
|
||||
connection.console.info(`Changed: ${event.document.uri}`);
|
||||
compiledFns.clear();
|
||||
connection.console.info(`Compiling: ${event.document.uri}`);
|
||||
resetState();
|
||||
if (SUPPORTED_LANGUAGE_IDS.has(event.document.languageId)) {
|
||||
const text = event.document.getText();
|
||||
await compile({
|
||||
text,
|
||||
file: event.document.uri,
|
||||
options: compilerOptions,
|
||||
});
|
||||
try {
|
||||
await compile({
|
||||
text,
|
||||
file: event.document.uri,
|
||||
options: compilerOptions,
|
||||
});
|
||||
} catch (err) {
|
||||
connection.console.error('Failed to compile');
|
||||
if (err instanceof Error) {
|
||||
connection.console.error(err.stack ?? err.message);
|
||||
} else {
|
||||
connection.console.error(JSON.stringify(err, null, 2));
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
connection.onDidChangeWatchedFiles(change => {
|
||||
compiledFns.clear();
|
||||
resetState();
|
||||
connection.console.log(
|
||||
change.changes.map(c => `File changed: ${c.uri}`).join('\n'),
|
||||
);
|
||||
@@ -118,6 +194,62 @@ connection.onCodeLensResolve(lens => {
|
||||
return lens;
|
||||
});
|
||||
|
||||
connection.onCodeAction(params => {
|
||||
const codeActions: Array<CodeAction> = [];
|
||||
for (const codeActionEvent of codeActionEvents) {
|
||||
if (
|
||||
isRangeWithinRange(
|
||||
[params.range.start, params.range.end],
|
||||
codeActionEvent.anchorRange,
|
||||
)
|
||||
) {
|
||||
const codeAction = CodeAction.create(
|
||||
codeActionEvent.title,
|
||||
{
|
||||
changes: {
|
||||
[params.textDocument.uri]: [
|
||||
{
|
||||
newText: codeActionEvent.newText,
|
||||
range: codeActionEvent.editRange,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
codeActionEvent.kind,
|
||||
);
|
||||
// After executing a codeaction, we want to draw autodep decorations again
|
||||
codeAction.command = Command.create(
|
||||
'Request autodeps decorations',
|
||||
'react.requestAutoDepsDecorations',
|
||||
codeActionEvent.anchorRange[0],
|
||||
);
|
||||
codeActions.push(codeAction);
|
||||
}
|
||||
}
|
||||
return codeActions;
|
||||
});
|
||||
|
||||
/**
|
||||
* The client can request the server to compute autodeps decorations based on a currently selected
|
||||
* position if the selected position is within an autodep eligible function call.
|
||||
*/
|
||||
connection.onRequest(AutoDepsDecorationsRequest.type, async params => {
|
||||
const position = params.position;
|
||||
for (const decoration of autoDepsDecorations) {
|
||||
if (isPositionWithinRange(position, decoration.useEffectCallExpr)) {
|
||||
return decoration;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
function resetState() {
|
||||
connection.console.debug('Clearing state');
|
||||
compiledFns.clear();
|
||||
autoDepsDecorations = [];
|
||||
codeActionEvents = [];
|
||||
}
|
||||
|
||||
documents.listen(connection);
|
||||
connection.listen();
|
||||
connection.console.info(`React Analyzer running in node ${process.version}`);
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {type AutoDepsDecorationsEvent} from 'babel-plugin-react-compiler/src/Entrypoint';
|
||||
import {type Position} from 'vscode-languageserver-textdocument';
|
||||
import {RequestType} from 'vscode-languageserver/node';
|
||||
import {type Range, sourceLocationToRange} from '../utils/range';
|
||||
|
||||
export type AutoDepsDecorationsLSPEvent = {
|
||||
useEffectCallExpr: Range;
|
||||
decorations: Array<Range>;
|
||||
};
|
||||
export interface AutoDepsDecorationsParams {
|
||||
position: Position;
|
||||
}
|
||||
export namespace AutoDepsDecorationsRequest {
|
||||
export const type = new RequestType<
|
||||
AutoDepsDecorationsParams,
|
||||
AutoDepsDecorationsLSPEvent,
|
||||
void
|
||||
>('react/autodeps_decorations');
|
||||
}
|
||||
|
||||
export function mapCompilerEventToLSPEvent(
|
||||
event: AutoDepsDecorationsEvent,
|
||||
): AutoDepsDecorationsLSPEvent {
|
||||
return {
|
||||
useEffectCallExpr: sourceLocationToRange(event.fnLoc),
|
||||
decorations: event.decorations.map(sourceLocationToRange),
|
||||
};
|
||||
}
|
||||
42
compiler/packages/react-forgive/server/src/utils/range.ts
Normal file
42
compiler/packages/react-forgive/server/src/utils/range.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* 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 * as t from '@babel/types';
|
||||
import {type Position} from 'vscode-languageserver/node';
|
||||
|
||||
export type Range = [Position, Position];
|
||||
|
||||
export function isPositionWithinRange(
|
||||
position: Position,
|
||||
[start, end]: Range,
|
||||
): boolean {
|
||||
return position.line >= start.line && position.line <= end.line;
|
||||
}
|
||||
|
||||
export function isRangeWithinRange(aRange: Range, bRange: Range): boolean {
|
||||
const startComparison = comparePositions(aRange[0], bRange[0]);
|
||||
const endComparison = comparePositions(aRange[1], bRange[1]);
|
||||
return startComparison >= 0 && endComparison <= 0;
|
||||
}
|
||||
|
||||
function comparePositions(a: Position, b: Position): number {
|
||||
const lineComparison = a.line - b.line;
|
||||
if (lineComparison === 0) {
|
||||
return a.character - b.character;
|
||||
} else {
|
||||
return lineComparison;
|
||||
}
|
||||
}
|
||||
|
||||
export function sourceLocationToRange(
|
||||
loc: t.SourceLocation,
|
||||
): [Position, Position] {
|
||||
return [
|
||||
{line: loc.start.line - 1, character: loc.start.column},
|
||||
{line: loc.end.line - 1, character: loc.end.column},
|
||||
];
|
||||
}
|
||||
16
compiler/packages/react-forgive/tsconfig.json
Normal file
16
compiler/packages/react-forgive/tsconfig.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"extends": "@tsconfig/strictest/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"module": "Node16",
|
||||
"moduleResolution": "Node16",
|
||||
"rootDir": "../",
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsxdev",
|
||||
"lib": ["ES2022"],
|
||||
|
||||
"target": "ES2022",
|
||||
"importsNotUsedAsValues": "remove",
|
||||
},
|
||||
"exclude": ["node_modules"],
|
||||
"include": ["server/src/**/*.ts", "client/src/**/*.ts"],
|
||||
}
|
||||
22
compiler/packages/react-mcp-server/README.md
Normal file
22
compiler/packages/react-mcp-server/README.md
Normal file
@@ -0,0 +1,22 @@
|
||||
# React MCP Server (experimental)
|
||||
|
||||
An experimental MCP Server for React.
|
||||
|
||||
## Development
|
||||
|
||||
First, add this file if you're using Claude Desktop: `code ~/Library/Application\ Support/Claude/claude_desktop_config.json`. Copy the absolute path from `which node` and from `react/compiler/react-mcp-server/dist/index.js` and paste, for example:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"react": {
|
||||
"command": "/Users/<username>/.asdf/shims/node",
|
||||
"args": [
|
||||
"/Users/<username>/code/react/compiler/packages/react-mcp-server/dist/index.js"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Next, run `yarn workspace react-mcp-server watch` from the `react/compiler` directory and make changes as needed. You will need to restart Claude everytime you want to try your changes.
|
||||
35
compiler/packages/react-mcp-server/package.json
Normal file
35
compiler/packages/react-mcp-server/package.json
Normal file
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"name": "react-mcp-server",
|
||||
"version": "0.0.0",
|
||||
"description": "React MCP Server (experimental)",
|
||||
"bin": {
|
||||
"react-mcp-server": "./dist/index.js"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "rimraf dist && tsup",
|
||||
"test": "echo 'no tests'",
|
||||
"dev": "concurrently --kill-others -n build,inspect \"yarn run watch\" \"wait-on dist/index.js && yarn run inspect\"",
|
||||
"inspect": "npx @modelcontextprotocol/inspector node dist/index.js",
|
||||
"watch": "yarn build --watch"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.26.0",
|
||||
"@babel/parser": "^7.26",
|
||||
"@babel/plugin-syntax-typescript": "^7.25.9",
|
||||
"@modelcontextprotocol/sdk": "^1.9.0",
|
||||
"algoliasearch": "^5.23.3",
|
||||
"cheerio": "^1.0.0",
|
||||
"html-to-text": "^9.0.5",
|
||||
"prettier": "^3.3.3",
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/html-to-text": "^9.0.4"
|
||||
},
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/facebook/react.git",
|
||||
"directory": "compiler/packages/react-mcp-server"
|
||||
}
|
||||
}
|
||||
77
compiler/packages/react-mcp-server/src/compiler/index.ts
Normal file
77
compiler/packages/react-mcp-server/src/compiler/index.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import type * as BabelCore from '@babel/core';
|
||||
import {parseAsync, transformFromAstAsync} from '@babel/core';
|
||||
import BabelPluginReactCompiler, {
|
||||
type PluginOptions,
|
||||
} from 'babel-plugin-react-compiler/src';
|
||||
import * as prettier from 'prettier';
|
||||
|
||||
export let lastResult: BabelCore.BabelFileResult | null = null;
|
||||
|
||||
export type PrintedCompilerPipelineValue =
|
||||
| {
|
||||
kind: 'hir';
|
||||
name: string;
|
||||
fnName: string | null;
|
||||
value: string;
|
||||
}
|
||||
| {kind: 'reactive'; name: string; fnName: string | null; value: string}
|
||||
| {kind: 'debug'; name: string; fnName: string | null; value: string};
|
||||
|
||||
type CompileOptions = {
|
||||
text: string;
|
||||
file: string;
|
||||
options: Partial<PluginOptions> | null;
|
||||
};
|
||||
export async function compile({
|
||||
text,
|
||||
file,
|
||||
options,
|
||||
}: CompileOptions): Promise<BabelCore.BabelFileResult> {
|
||||
const ast = await parseAsync(text, {
|
||||
sourceFileName: file,
|
||||
parserOpts: {
|
||||
plugins: ['typescript', 'jsx'],
|
||||
},
|
||||
sourceType: 'module',
|
||||
});
|
||||
if (ast == null) {
|
||||
throw new Error('Could not parse');
|
||||
}
|
||||
const plugins =
|
||||
options != null
|
||||
? [[BabelPluginReactCompiler, options]]
|
||||
: [[BabelPluginReactCompiler]];
|
||||
const result = await transformFromAstAsync(ast, text, {
|
||||
filename: file,
|
||||
highlightCode: false,
|
||||
retainLines: true,
|
||||
plugins,
|
||||
sourceType: 'module',
|
||||
sourceFileName: file,
|
||||
});
|
||||
if (result?.code == null) {
|
||||
throw new Error(
|
||||
`Expected BabelPluginReactCompiler to compile successfully, got ${result}`,
|
||||
);
|
||||
}
|
||||
try {
|
||||
result.code = await prettier.format(result.code, {
|
||||
semi: false,
|
||||
parser: 'babel-ts',
|
||||
});
|
||||
if (result.code != null) {
|
||||
lastResult = result;
|
||||
}
|
||||
} catch (err) {
|
||||
// If prettier failed just log, no need to crash
|
||||
console.error(err);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
365
compiler/packages/react-mcp-server/src/index.ts
Normal file
365
compiler/packages/react-mcp-server/src/index.ts
Normal file
@@ -0,0 +1,365 @@
|
||||
/**
|
||||
* 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 {McpServer} from '@modelcontextprotocol/sdk/server/mcp.js';
|
||||
import {StdioServerTransport} from '@modelcontextprotocol/sdk/server/stdio.js';
|
||||
import {z} from 'zod';
|
||||
import {compile, type PrintedCompilerPipelineValue} from './compiler';
|
||||
import {
|
||||
CompilerPipelineValue,
|
||||
printReactiveFunctionWithOutlined,
|
||||
printFunctionWithOutlined,
|
||||
PluginOptions,
|
||||
SourceLocation,
|
||||
} from 'babel-plugin-react-compiler/src';
|
||||
import * as cheerio from 'cheerio';
|
||||
import {queryAlgolia} from './utils/algolia';
|
||||
import assertExhaustive from './utils/assertExhaustive';
|
||||
import {convert} from 'html-to-text';
|
||||
|
||||
const server = new McpServer({
|
||||
name: 'React',
|
||||
version: '0.0.0',
|
||||
});
|
||||
|
||||
server.tool(
|
||||
'query-react-dev-docs',
|
||||
'Search/look up official docs from react.dev',
|
||||
{
|
||||
query: z.string(),
|
||||
},
|
||||
async ({query}) => {
|
||||
try {
|
||||
const pages = await queryAlgolia(query);
|
||||
if (pages.length === 0) {
|
||||
return {
|
||||
content: [{type: 'text' as const, text: `No results`}],
|
||||
};
|
||||
}
|
||||
const content = pages.map(html => {
|
||||
const $ = cheerio.load(html);
|
||||
// react.dev should always have at least one <article> with the main content
|
||||
const article = $('article').html();
|
||||
if (article != null) {
|
||||
return {
|
||||
type: 'text' as const,
|
||||
text: convert(article),
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
type: 'text' as const,
|
||||
// Fallback to converting the whole page to text.
|
||||
text: convert($.html()),
|
||||
};
|
||||
}
|
||||
});
|
||||
return {
|
||||
content,
|
||||
};
|
||||
} catch (err) {
|
||||
return {
|
||||
isError: true,
|
||||
content: [{type: 'text' as const, text: `Error: ${err.stack}`}],
|
||||
};
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
server.tool(
|
||||
'compile',
|
||||
'Compile code with React Compiler. Optionally, for debugging provide a pass name like "HIR" to see more information.',
|
||||
{
|
||||
text: z.string(),
|
||||
passName: z.enum(['HIR', 'ReactiveFunction', 'All', '@DEBUG']).optional(),
|
||||
},
|
||||
async ({text, passName}) => {
|
||||
const pipelinePasses = new Map<
|
||||
string,
|
||||
Array<PrintedCompilerPipelineValue>
|
||||
>();
|
||||
const recordPass: (
|
||||
result: PrintedCompilerPipelineValue,
|
||||
) => void = result => {
|
||||
const entry = pipelinePasses.get(result.name);
|
||||
if (Array.isArray(entry)) {
|
||||
entry.push(result);
|
||||
} else {
|
||||
pipelinePasses.set(result.name, [result]);
|
||||
}
|
||||
};
|
||||
const logIR = (result: CompilerPipelineValue): void => {
|
||||
switch (result.kind) {
|
||||
case 'ast': {
|
||||
break;
|
||||
}
|
||||
case 'hir': {
|
||||
recordPass({
|
||||
kind: 'hir',
|
||||
fnName: result.value.id,
|
||||
name: result.name,
|
||||
value: printFunctionWithOutlined(result.value),
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'reactive': {
|
||||
recordPass({
|
||||
kind: 'reactive',
|
||||
fnName: result.value.id,
|
||||
name: result.name,
|
||||
value: printReactiveFunctionWithOutlined(result.value),
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'debug': {
|
||||
recordPass({
|
||||
kind: 'debug',
|
||||
fnName: null,
|
||||
name: result.name,
|
||||
value: result.value,
|
||||
});
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
assertExhaustive(result, `Unhandled result ${result}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
const errors: Array<{message: string; loc: SourceLocation | null}> = [];
|
||||
const compilerOptions: Partial<PluginOptions> = {
|
||||
panicThreshold: 'none',
|
||||
logger: {
|
||||
debugLogIRs: logIR,
|
||||
logEvent: (_filename, event): void => {
|
||||
if (event.kind === 'CompileError') {
|
||||
const detail = event.detail;
|
||||
const loc =
|
||||
detail.loc == null || typeof detail.loc == 'symbol'
|
||||
? event.fnLoc
|
||||
: detail.loc;
|
||||
errors.push({
|
||||
message: detail.reason,
|
||||
loc,
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
try {
|
||||
const result = await compile({
|
||||
text,
|
||||
file: 'anonymous.tsx',
|
||||
options: compilerOptions,
|
||||
});
|
||||
if (result.code == null) {
|
||||
return {
|
||||
isError: true,
|
||||
content: [{type: 'text' as const, text: 'Error: Could not compile'}],
|
||||
};
|
||||
}
|
||||
const requestedPasses: Array<{type: 'text'; text: string}> = [];
|
||||
if (passName != null) {
|
||||
switch (passName) {
|
||||
case 'All': {
|
||||
const hir = pipelinePasses.get('PropagateScopeDependenciesHIR');
|
||||
if (hir !== undefined) {
|
||||
for (const pipelineValue of hir) {
|
||||
requestedPasses.push({
|
||||
type: 'text' as const,
|
||||
text: pipelineValue.value,
|
||||
});
|
||||
}
|
||||
}
|
||||
const reactiveFunc = pipelinePasses.get('PruneHoistedContexts');
|
||||
if (reactiveFunc !== undefined) {
|
||||
for (const pipelineValue of reactiveFunc) {
|
||||
requestedPasses.push({
|
||||
type: 'text' as const,
|
||||
text: pipelineValue.value,
|
||||
});
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'HIR': {
|
||||
// Last pass before HIR -> ReactiveFunction
|
||||
const requestedPass = pipelinePasses.get(
|
||||
'PropagateScopeDependenciesHIR',
|
||||
);
|
||||
if (requestedPass !== undefined) {
|
||||
for (const pipelineValue of requestedPass) {
|
||||
requestedPasses.push({
|
||||
type: 'text' as const,
|
||||
text: pipelineValue.value,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
console.error(`Could not find requested pass ${passName}`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'ReactiveFunction': {
|
||||
// Last pass
|
||||
const requestedPass = pipelinePasses.get('PruneHoistedContexts');
|
||||
if (requestedPass !== undefined) {
|
||||
for (const pipelineValue of requestedPass) {
|
||||
requestedPasses.push({
|
||||
type: 'text' as const,
|
||||
text: pipelineValue.value,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
console.error(`Could not find requested pass ${passName}`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case '@DEBUG': {
|
||||
for (const [, pipelinePass] of pipelinePasses) {
|
||||
for (const pass of pipelinePass) {
|
||||
requestedPasses.push({
|
||||
type: 'text' as const,
|
||||
text: `${pass.name}\n\n${pass.value}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
assertExhaustive(
|
||||
passName,
|
||||
`Unhandled passName option: ${passName}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
const requestedPass = pipelinePasses.get(passName);
|
||||
if (requestedPass !== undefined) {
|
||||
for (const pipelineValue of requestedPass) {
|
||||
if (pipelineValue.name === passName) {
|
||||
requestedPasses.push({
|
||||
type: 'text' as const,
|
||||
text: pipelineValue.value,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (errors.length > 0) {
|
||||
return {
|
||||
content: errors.map(err => {
|
||||
return {
|
||||
type: 'text' as const,
|
||||
text:
|
||||
err.loc === null || typeof err.loc === 'symbol'
|
||||
? `React Compiler bailed out:\n\n${err.message}`
|
||||
: `React Compiler bailed out:\n\n${err.message}@${err.loc.start.line}:${err.loc.end.line}`,
|
||||
};
|
||||
}),
|
||||
};
|
||||
}
|
||||
return {
|
||||
content: [
|
||||
{type: 'text' as const, text: result.code},
|
||||
...requestedPasses,
|
||||
],
|
||||
};
|
||||
} catch (err) {
|
||||
return {
|
||||
isError: true,
|
||||
content: [{type: 'text' as const, text: `Error: ${err.stack}`}],
|
||||
};
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
server.prompt('review-react-code', () => ({
|
||||
messages: [
|
||||
{
|
||||
role: 'assistant',
|
||||
content: {
|
||||
type: 'text',
|
||||
text: `
|
||||
## Role
|
||||
You are a React assistant that helps users write more efficient and optimizable React code. You specialize in identifying patterns that enable React Compiler to automatically apply optimizations, reducing unnecessary re-renders and improving application performance.
|
||||
|
||||
## Follow these guidelines in all code you produce and suggest
|
||||
Use functional components with Hooks: Do not generate class components or use old lifecycle methods. Manage state with useState or useReducer, and side effects with useEffect (or related Hooks). Always prefer functions and Hooks for any new component logic.
|
||||
|
||||
Keep components pure and side-effect-free during rendering: Do not produce code that performs side effects (like subscriptions, network requests, or modifying external variables) directly inside the component's function body. Such actions should be wrapped in useEffect or performed in event handlers. Ensure your render logic is a pure function of props and state.
|
||||
|
||||
Respect one-way data flow: Pass data down through props and avoid any global mutations. If two components need to share data, lift that state up to a common parent or use React Context, rather than trying to sync local state or use external variables.
|
||||
|
||||
Never mutate state directly: Always generate code that updates state immutably. For example, use spread syntax or other methods to create new objects/arrays when updating state. Do not use assignments like state.someValue = ... or array mutations like array.push() on state variables. Use the state setter (setState from useState, etc.) to update state.
|
||||
|
||||
Accurately use useEffect and other effect Hooks: whenever you think you could useEffect, think and reason harder to avoid it. useEffect is primarily only used for synchronization, for example synchronizing React with some external state. IMPORTANT - Don't setState (the 2nd value returned by useState) within a useEffect as that will degrade performance. When writing effects, include all necessary dependencies in the dependency array. Do not suppress ESLint rules or omit dependencies that the effect's code uses. Structure the effect callbacks to handle changing values properly (e.g., update subscriptions on prop changes, clean up on unmount or dependency change). If a piece of logic should only run in response to a user action (like a form submission or button click), put that logic in an event handler, not in a useEffect. Where possible, useEffects should return a cleanup function.
|
||||
|
||||
Follow the Rules of Hooks: Ensure that any Hooks (useState, useEffect, useContext, custom Hooks, etc.) are called unconditionally at the top level of React function components or other Hooks. Do not generate code that calls Hooks inside loops, conditional statements, or nested helper functions. Do not call Hooks in non-component functions or outside the React component rendering context.
|
||||
|
||||
Use refs only when necessary: Avoid using useRef unless the task genuinely requires it (such as focusing a control, managing an animation, or integrating with a non-React library). Do not use refs to store application state that should be reactive. If you do use refs, never write to or read from ref.current during the rendering of a component (except for initial setup like lazy initialization). Any ref usage should not affect the rendered output directly.
|
||||
|
||||
Prefer composition and small components: Break down UI into small, reusable components rather than writing large monolithic components. The code you generate should promote clarity and reusability by composing components together. Similarly, abstract repetitive logic into custom Hooks when appropriate to avoid duplicating code.
|
||||
|
||||
Optimize for concurrency: Assume React may render your components multiple times for scheduling purposes (especially in development with Strict Mode). Write code that remains correct even if the component function runs more than once. For instance, avoid side effects in the component body and use functional state updates (e.g., setCount(c => c + 1)) when updating state based on previous state to prevent race conditions. Always include cleanup functions in effects that subscribe to external resources. Don't write useEffects for "do this when this changes" side-effects. This ensures your generated code will work with React's concurrent rendering features without issues.
|
||||
|
||||
Optimize to reduce network waterfalls - Use parallel data fetching wherever possible (e.g., start multiple requests at once rather than one after another). Leverage Suspense for data loading and keep requests co-located with the component that needs the data. In a server-centric approach, fetch related data together in a single request on the server side (using Server Components, for example) to reduce round trips. Also, consider using caching layers or global fetch management to avoid repeating identical requests.
|
||||
|
||||
Rely on React Compiler - useMemo, useCallback, and React.memo can be omitted if React Compiler is enabled. Avoid premature optimization with manual memoization. Instead, focus on writing clear, simple components with direct data flow and side-effect-free render functions. Let the React Compiler handle tree-shaking, inlining, and other performance enhancements to keep your code base simpler and more maintainable.
|
||||
|
||||
Design for a good user experience - Provide clear, minimal, and non-blocking UI states. When data is loading, show lightweight placeholders (e.g., skeleton screens) rather than intrusive spinners everywhere. Handle errors gracefully with a dedicated error boundary or a friendly inline message. Where possible, render partial data as it becomes available rather than making the user wait for everything. Suspense allows you to declare the loading states in your component tree in a natural way, preventing “flash” states and improving perceived performance.
|
||||
|
||||
Server Components - Shift data-heavy logic to the server whenever possible. Break up the more static parts of the app into server components. Break up data fetching into server components. Only client components (denoted by the 'use client' top level directive) need interactivity. By rendering parts of your UI on the server, you reduce the client-side JavaScript needed and avoid sending unnecessary data over the wire. Use Server Components to prefetch and pre-render data, allowing faster initial loads and smaller bundle sizes. This also helps manage or eliminate certain waterfalls by resolving data on the server before streaming the HTML (and partial React tree) to the client.
|
||||
|
||||
## Available Tools
|
||||
- 'docs': Look up documentation from react.dev. Returns text as a string.
|
||||
- 'compile': Run the user's code through React Compiler. Returns optimized JS/TS code with potential diagnostics.
|
||||
|
||||
## Process
|
||||
1. Analyze the user's code for optimization opportunities:
|
||||
- Check for React anti-patterns that prevent compiler optimization
|
||||
- Identify unnecessary manual optimizations (useMemo, useCallback, React.memo) that the compiler can handle
|
||||
- Look for component structure issues that limit compiler effectiveness
|
||||
- Think about each suggestion you are making and consult React docs using the docs://{query} resource for best practices
|
||||
|
||||
2. Use React Compiler to verify optimization potential:
|
||||
- Run the code through the compiler and analyze the output
|
||||
- You can run the compiler multiple times to verify your work
|
||||
- Check for successful optimization by looking for const $ = _c(n) cache entries, where n is an integer
|
||||
- Identify bailout messages that indicate where code could be improved
|
||||
- Compare before/after optimization potential
|
||||
|
||||
3. Provide actionable guidance:
|
||||
- Explain specific code changes with clear reasoning
|
||||
- Show before/after examples when suggesting changes
|
||||
- Include compiler results to demonstrate the impact of optimizations
|
||||
- Only suggest changes that meaningfully improve optimization potential
|
||||
|
||||
## Optimization Guidelines
|
||||
- Avoid mutation of values that are memoized by the compiler
|
||||
- State updates should be structured to enable granular updates
|
||||
- Side effects should be isolated and dependencies clearly defined
|
||||
- The compiler automatically inserts memoization, so manually added useMemo/useCallback/React.memo can often be removed
|
||||
|
||||
## Understanding Compiler Output
|
||||
- Successful optimization adds import { c as _c } from "react/compiler-runtime";
|
||||
- Successful optimization initializes a constant sized cache with const $ = _c(n), where n is the size of the cache as an integer
|
||||
- When suggesting changes, try to increase or decrease the number of cached expressions (visible in const $ = _c(n))
|
||||
- Increase: more memoization coverage
|
||||
- Decrease: if there are unnecessary dependencies, less dependencies mean less re-rendering
|
||||
`,
|
||||
},
|
||||
},
|
||||
],
|
||||
}));
|
||||
|
||||
async function main() {
|
||||
const transport = new StdioServerTransport();
|
||||
await server.connect(transport);
|
||||
console.error('React Compiler MCP Server running on stdio');
|
||||
}
|
||||
|
||||
main().catch(error => {
|
||||
console.error('Fatal error in main():', error);
|
||||
process.exit(1);
|
||||
});
|
||||
100
compiler/packages/react-mcp-server/src/types/algolia.ts
Normal file
100
compiler/packages/react-mcp-server/src/types/algolia.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
// https://github.com/algolia/docsearch/blob/15ebcba606b281aa0dddc4ccb8feb19d396bf79e/packages/docsearch-react/src/types/DocSearchHit.ts
|
||||
type ContentType =
|
||||
| 'content'
|
||||
| 'lvl0'
|
||||
| 'lvl1'
|
||||
| 'lvl2'
|
||||
| 'lvl3'
|
||||
| 'lvl4'
|
||||
| 'lvl5'
|
||||
| 'lvl6';
|
||||
|
||||
interface DocSearchHitAttributeHighlightResult {
|
||||
value: string;
|
||||
matchLevel: 'full' | 'none' | 'partial';
|
||||
matchedWords: string[];
|
||||
fullyHighlighted?: boolean;
|
||||
}
|
||||
|
||||
interface DocSearchHitHighlightResultHierarchy {
|
||||
lvl0: DocSearchHitAttributeHighlightResult;
|
||||
lvl1: DocSearchHitAttributeHighlightResult;
|
||||
lvl2: DocSearchHitAttributeHighlightResult;
|
||||
lvl3: DocSearchHitAttributeHighlightResult;
|
||||
lvl4: DocSearchHitAttributeHighlightResult;
|
||||
lvl5: DocSearchHitAttributeHighlightResult;
|
||||
lvl6: DocSearchHitAttributeHighlightResult;
|
||||
}
|
||||
|
||||
interface DocSearchHitHighlightResult {
|
||||
content: DocSearchHitAttributeHighlightResult;
|
||||
hierarchy: DocSearchHitHighlightResultHierarchy;
|
||||
hierarchy_camel: DocSearchHitHighlightResultHierarchy[];
|
||||
}
|
||||
|
||||
interface DocSearchHitAttributeSnippetResult {
|
||||
value: string;
|
||||
matchLevel: 'full' | 'none' | 'partial';
|
||||
}
|
||||
|
||||
interface DocSearchHitSnippetResult {
|
||||
content: DocSearchHitAttributeSnippetResult;
|
||||
hierarchy: DocSearchHitHighlightResultHierarchy;
|
||||
hierarchy_camel: DocSearchHitHighlightResultHierarchy[];
|
||||
}
|
||||
|
||||
export declare type DocSearchHit = {
|
||||
objectID: string;
|
||||
content: string | null;
|
||||
url: string;
|
||||
url_without_anchor: string;
|
||||
type: ContentType;
|
||||
anchor: string | null;
|
||||
hierarchy: {
|
||||
lvl0: string;
|
||||
lvl1: string;
|
||||
lvl2: string | null;
|
||||
lvl3: string | null;
|
||||
lvl4: string | null;
|
||||
lvl5: string | null;
|
||||
lvl6: string | null;
|
||||
};
|
||||
_highlightResult: DocSearchHitHighlightResult;
|
||||
_snippetResult: DocSearchHitSnippetResult;
|
||||
_rankingInfo?: {
|
||||
promoted: boolean;
|
||||
nbTypos: number;
|
||||
firstMatchedWord: number;
|
||||
proximityDistance?: number;
|
||||
geoDistance: number;
|
||||
geoPrecision?: number;
|
||||
nbExactWords: number;
|
||||
words: number;
|
||||
filters: number;
|
||||
userScore: number;
|
||||
matchedGeoLocation?: {
|
||||
lat: number;
|
||||
lng: number;
|
||||
distance: number;
|
||||
};
|
||||
};
|
||||
_distinctSeqID?: number;
|
||||
__autocomplete_indexName?: string;
|
||||
__autocomplete_queryID?: string;
|
||||
__autocomplete_algoliaCredentials?: {
|
||||
appId: string;
|
||||
apiKey: string;
|
||||
};
|
||||
__autocomplete_id?: number;
|
||||
};
|
||||
|
||||
export type InternalDocSearchHit = DocSearchHit & {
|
||||
__docsearch_parent: InternalDocSearchHit | null;
|
||||
};
|
||||
119
compiler/packages/react-mcp-server/src/utils/algolia.ts
Normal file
119
compiler/packages/react-mcp-server/src/utils/algolia.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
/**
|
||||
* 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 {DocSearchHit, InternalDocSearchHit} from '../types/algolia';
|
||||
import {liteClient, type Hit, type SearchResponse} from 'algoliasearch/lite';
|
||||
|
||||
// https://github.com/reactjs/react.dev/blob/55986965fbf69c2584040039c9586a01bd54eba7/src/siteConfig.js#L15-L19
|
||||
const ALGOLIA_CONFIG = {
|
||||
appId: '1FCF9AYYAT',
|
||||
apiKey: '1b7ad4e1c89e645e351e59d40544eda1',
|
||||
indexName: 'beta-react',
|
||||
};
|
||||
|
||||
export const ALGOLIA_CLIENT = liteClient(
|
||||
ALGOLIA_CONFIG.appId,
|
||||
ALGOLIA_CONFIG.apiKey,
|
||||
);
|
||||
|
||||
export function printHierarchy(
|
||||
hit: DocSearchHit | InternalDocSearchHit,
|
||||
): string {
|
||||
let val = `${hit.hierarchy.lvl0} > ${hit.hierarchy.lvl1}`;
|
||||
if (hit.hierarchy.lvl2 != null) {
|
||||
val = val.concat(` > ${hit.hierarchy.lvl2}`);
|
||||
}
|
||||
if (hit.hierarchy.lvl3 != null) {
|
||||
val = val.concat(` > ${hit.hierarchy.lvl3}`);
|
||||
}
|
||||
if (hit.hierarchy.lvl4 != null) {
|
||||
val = val.concat(` > ${hit.hierarchy.lvl4}`);
|
||||
}
|
||||
if (hit.hierarchy.lvl5 != null) {
|
||||
val = val.concat(` > ${hit.hierarchy.lvl5}`);
|
||||
}
|
||||
if (hit.hierarchy.lvl6 != null) {
|
||||
val = val.concat(` > ${hit.hierarchy.lvl6}`);
|
||||
}
|
||||
return val;
|
||||
}
|
||||
|
||||
export async function queryAlgolia(
|
||||
message: string | Array<string>,
|
||||
): Promise<Array<string>> {
|
||||
const {results} = await ALGOLIA_CLIENT.search<DocSearchHit>({
|
||||
requests: [
|
||||
{
|
||||
query: Array.isArray(message) ? message.join('\n') : message,
|
||||
indexName: ALGOLIA_CONFIG.indexName,
|
||||
attributesToRetrieve: [
|
||||
'hierarchy.lvl0',
|
||||
'hierarchy.lvl1',
|
||||
'hierarchy.lvl2',
|
||||
'hierarchy.lvl3',
|
||||
'hierarchy.lvl4',
|
||||
'hierarchy.lvl5',
|
||||
'hierarchy.lvl6',
|
||||
'content',
|
||||
'url',
|
||||
],
|
||||
attributesToSnippet: [
|
||||
`hierarchy.lvl1:10`,
|
||||
`hierarchy.lvl2:10`,
|
||||
`hierarchy.lvl3:10`,
|
||||
`hierarchy.lvl4:10`,
|
||||
`hierarchy.lvl5:10`,
|
||||
`hierarchy.lvl6:10`,
|
||||
`content:10`,
|
||||
],
|
||||
snippetEllipsisText: '…',
|
||||
hitsPerPage: 30,
|
||||
attributesToHighlight: [
|
||||
'hierarchy.lvl0',
|
||||
'hierarchy.lvl1',
|
||||
'hierarchy.lvl2',
|
||||
'hierarchy.lvl3',
|
||||
'hierarchy.lvl4',
|
||||
'hierarchy.lvl5',
|
||||
'hierarchy.lvl6',
|
||||
'content',
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
const firstResult = results[0] as SearchResponse<DocSearchHit>;
|
||||
const {hits} = firstResult;
|
||||
const deduped = new Map();
|
||||
for (const hit of hits) {
|
||||
// drop hashes to dedupe properly
|
||||
const u = new URL(hit.url);
|
||||
if (deduped.has(u.pathname)) {
|
||||
continue;
|
||||
}
|
||||
deduped.set(u.pathname, hit);
|
||||
}
|
||||
const pages: Array<string | null> = await Promise.all(
|
||||
Array.from(deduped.values()).map(hit => {
|
||||
return fetch(hit.url, {
|
||||
headers: {
|
||||
'User-Agent':
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36',
|
||||
},
|
||||
}).then(res => {
|
||||
if (res.ok === true) {
|
||||
return res.text();
|
||||
} else {
|
||||
console.error(
|
||||
`Could not fetch docs: ${res.status} ${res.statusText}`,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
});
|
||||
}),
|
||||
);
|
||||
return pages.filter(page => page !== null);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Trigger an exhaustiveness check in TypeScript and throw at runtime.
|
||||
*/
|
||||
export default function assertExhaustive(_: never, errorMsg: string): never {
|
||||
throw new Error(errorMsg);
|
||||
}
|
||||
5
compiler/packages/react-mcp-server/todo.md
Normal file
5
compiler/packages/react-mcp-server/todo.md
Normal file
@@ -0,0 +1,5 @@
|
||||
TODO
|
||||
|
||||
- [ ] If code doesnt compile, read diagnostics and try again
|
||||
- [ ] Provide detailed examples in assistant prompt (use another LLM to generate good prompts, iterate from there)
|
||||
- [ ] Provide more tools for working with HIR/AST (eg so we can prompt it to try and optimize code via HIR, which it can then translate back into user code changes)
|
||||
22
compiler/packages/react-mcp-server/tsconfig.json
Normal file
22
compiler/packages/react-mcp-server/tsconfig.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"extends": "@tsconfig/strictest/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"module": "Node16",
|
||||
"moduleResolution": "Node16",
|
||||
"rootDir": "../",
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsxdev",
|
||||
"lib": ["ES2022"],
|
||||
|
||||
// weaken strictness from preset
|
||||
"importsNotUsedAsValues": "remove",
|
||||
"noUncheckedIndexedAccess": false,
|
||||
"noUnusedParameters": false,
|
||||
"useUnknownInCatchVariables": false,
|
||||
"target": "ES2022",
|
||||
// ideally turn off only during dev, or on a per-file basis
|
||||
"noUnusedLocals": false,
|
||||
},
|
||||
"exclude": ["node_modules"],
|
||||
"include": ["src/**/*.ts"],
|
||||
}
|
||||
37
compiler/packages/react-mcp-server/tsup.config.ts
Normal file
37
compiler/packages/react-mcp-server/tsup.config.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* 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 {defineConfig} from 'tsup';
|
||||
|
||||
export default defineConfig({
|
||||
entry: ['./src/index.ts'],
|
||||
outDir: './dist',
|
||||
external: [],
|
||||
splitting: false,
|
||||
sourcemap: false,
|
||||
dts: false,
|
||||
bundle: true,
|
||||
format: 'cjs',
|
||||
platform: 'node',
|
||||
target: 'es2022',
|
||||
banner: {
|
||||
js: `#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @lightSyntaxTransform
|
||||
* @noflow
|
||||
* @nolint
|
||||
* @preventMunge
|
||||
* @preserve-invariant-messages
|
||||
*/`,
|
||||
},
|
||||
});
|
||||
@@ -306,6 +306,7 @@ export type TransformResult = {
|
||||
original: string;
|
||||
forget: string;
|
||||
} | null;
|
||||
sourceMap: BabelCore.BabelFileResult['map'];
|
||||
};
|
||||
|
||||
export async function transformFixtureInput(
|
||||
@@ -331,6 +332,9 @@ export async function transformFixtureInput(
|
||||
// with `cwd`, which is different across machines
|
||||
const virtualFilepath = '/' + filename;
|
||||
|
||||
// Check if we should emit source maps in the test fixture
|
||||
const includeSourceMaps = firstLine.includes('@sourceMaps');
|
||||
|
||||
const presets =
|
||||
language === 'typescript'
|
||||
? TypescriptEvaluatorPresets
|
||||
@@ -357,6 +361,7 @@ export async function transformFixtureInput(
|
||||
'babel-plugin-idx',
|
||||
],
|
||||
sourceType: 'module',
|
||||
sourceMaps: includeSourceMaps,
|
||||
ast: includeEvaluator,
|
||||
cloneInputAst: includeEvaluator,
|
||||
configFile: false,
|
||||
@@ -447,6 +452,7 @@ export async function transformFixtureInput(
|
||||
forgetOutput,
|
||||
logs: formattedLogs,
|
||||
evaluatorCode,
|
||||
sourceMap: includeSourceMaps ? forgetResult.map : null,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {BabelFileResult} from '@babel/core';
|
||||
import chalk from 'chalk';
|
||||
import fs from 'fs';
|
||||
import invariant from 'invariant';
|
||||
@@ -24,6 +25,7 @@ export function writeOutputToString(
|
||||
evaluatorOutput: string | null,
|
||||
logs: string | null,
|
||||
errorMessage: string | null,
|
||||
sourceMap: BabelFileResult['map'] | null,
|
||||
) {
|
||||
// leading newline intentional
|
||||
let result = `
|
||||
@@ -42,6 +44,14 @@ ${wrapWithTripleBackticks(compilerOutput, 'javascript')}
|
||||
result += '\n';
|
||||
}
|
||||
|
||||
if (sourceMap != null) {
|
||||
result += `
|
||||
## Source Map
|
||||
|
||||
${wrapWithTripleBackticks(JSON.stringify(sourceMap, null, 2))}
|
||||
`;
|
||||
}
|
||||
|
||||
if (logs != null) {
|
||||
result += `
|
||||
## Logs
|
||||
|
||||
@@ -146,7 +146,7 @@ function subscribeFilterFile(
|
||||
state: RunnerState,
|
||||
onChange: (state: RunnerState) => void,
|
||||
) {
|
||||
watcher.subscribe(process.cwd(), async (err, events) => {
|
||||
watcher.subscribe(PROJECT_ROOT, async (err, events) => {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
|
||||
@@ -245,6 +245,7 @@ export async function transformFixture(
|
||||
sproutOutput,
|
||||
compileResult?.logs ?? null,
|
||||
error,
|
||||
compileResult?.sourceMap ?? null,
|
||||
);
|
||||
|
||||
return {
|
||||
|
||||
@@ -51,6 +51,9 @@ if (hasErrors) {
|
||||
}
|
||||
|
||||
function processFile(file) {
|
||||
if (fs.lstatSync(file).isDirectory()) {
|
||||
return;
|
||||
}
|
||||
let source = fs.readFileSync(file, 'utf8');
|
||||
|
||||
if (source.indexOf(META_COPYRIGHT_COMMENT_BLOCK) === 0) {
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
const prompt = require('prompt-promise');
|
||||
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* 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 strict';
|
||||
|
||||
@@ -62,9 +68,15 @@ async function main() {
|
||||
.option('tag', {
|
||||
description: 'Tag to publish to npm',
|
||||
type: 'choices',
|
||||
choices: ['experimental', 'beta'],
|
||||
choices: ['experimental', 'beta', 'rc'],
|
||||
default: 'experimental',
|
||||
})
|
||||
.option('tag-version', {
|
||||
description:
|
||||
'Optional tag version to append to tag name, eg `1` becomes 0.0.0-rc.1',
|
||||
type: 'number',
|
||||
default: null,
|
||||
})
|
||||
.option('version-name', {
|
||||
description: 'Version name',
|
||||
type: 'string',
|
||||
@@ -133,7 +145,13 @@ async function main() {
|
||||
files: {exclude: ['.DS_Store']},
|
||||
});
|
||||
const truncatedHash = hash.slice(0, 7);
|
||||
const newVersion = `${argv.versionName}-${argv.tag}-${truncatedHash}-${dateString}`;
|
||||
let newVersion =
|
||||
argv.tagVersion == null || argv.tagVersion === ''
|
||||
? `${argv.versionName}-${argv.tag}`
|
||||
: `${argv.versionName}-${argv.tag}.${argv.tagVersion}`;
|
||||
if (argv.tag === 'experimental' || argv.tag === 'beta') {
|
||||
newVersion = `${newVersion}-${truncatedHash}-${dateString}`;
|
||||
}
|
||||
|
||||
for (const pkgName of pkgNames) {
|
||||
const pkgDir = path.resolve(__dirname, `../../packages/${pkgName}`);
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
const ora = require('ora');
|
||||
const {execHelper} = require('./utils');
|
||||
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
const PUBLISHABLE_PACKAGES = [
|
||||
'babel-plugin-react-compiler',
|
||||
'eslint-plugin-react-compiler',
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
const cp = require('child_process');
|
||||
const util = require('util');
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user