Compare commits
59 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 |
@@ -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"],
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
@@ -33,7 +33,7 @@ const canaryChannelLabel = 'canary';
|
||||
const rcNumber = 0;
|
||||
|
||||
const stablePackages = {
|
||||
'eslint-plugin-react-hooks': '6.0.0',
|
||||
'eslint-plugin-react-hooks': '6.1.0',
|
||||
'jest-react': '0.17.0',
|
||||
react: ReactVersion,
|
||||
'react-art': ReactVersion,
|
||||
|
||||
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,
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -1722,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) ||
|
||||
@@ -1732,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,
|
||||
@@ -2665,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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
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
|
||||
|
||||
@@ -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
@@ -66,7 +66,7 @@
|
||||
"webpack-manifest-plugin": "^4.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.49.1"
|
||||
"@playwright/test": "^1.51.1"
|
||||
},
|
||||
"scripts": {
|
||||
"predev": "cp -r ../../build/oss-experimental/* ./node_modules/",
|
||||
|
||||
@@ -2748,12 +2748,12 @@
|
||||
resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33"
|
||||
integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==
|
||||
|
||||
"@playwright/test@^1.49.1":
|
||||
version "1.49.1"
|
||||
resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.49.1.tgz#55fa360658b3187bfb6371e2f8a64f50ef80c827"
|
||||
integrity sha512-Ky+BVzPz8pL6PQxHqNRW1k3mIyv933LML7HktS8uik0bUXNCdPhoS/kLihiO1tMf/egaJb4IutXd7UywvXEW+g==
|
||||
"@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.49.1"
|
||||
playwright "1.51.1"
|
||||
|
||||
"@pmmmwh/react-refresh-webpack-plugin@0.5.15":
|
||||
version "0.5.15"
|
||||
@@ -7284,17 +7284,17 @@ pkg-up@^3.1.0:
|
||||
dependencies:
|
||||
find-up "^3.0.0"
|
||||
|
||||
playwright-core@1.49.1:
|
||||
version "1.49.1"
|
||||
resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.49.1.tgz#32c62f046e950f586ff9e35ed490a424f2248015"
|
||||
integrity sha512-BzmpVcs4kE2CH15rWfzpjzVGhWERJfmnXmniSyKeRZUs9Ws65m+RGIi7mjJK/euCegfn3i7jvqWeWyHe9y3Vgg==
|
||||
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.49.1:
|
||||
version "1.49.1"
|
||||
resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.49.1.tgz#830266dbca3008022afa7b4783565db9944ded7c"
|
||||
integrity sha512-VYL8zLoNTBxVOrJBbDuRgDWa3i+mfQgDTrL8Ah9QXZ7ax4Dsj0MSq5bYgytRnDVVe+njoKnfsYkH3HzqVj5UZA==
|
||||
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.49.1"
|
||||
playwright-core "1.51.1"
|
||||
optionalDependencies:
|
||||
fsevents "2.3.2"
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
import {renderToPipeableStream} from 'react-dom/server';
|
||||
import {Writable} from 'stream';
|
||||
|
||||
import App from '../src/components/App';
|
||||
|
||||
@@ -14,11 +15,41 @@ if (process.env.NODE_ENV === 'development') {
|
||||
assets = require('../build/asset-manifest.json');
|
||||
}
|
||||
|
||||
class ThrottledWritable extends Writable {
|
||||
constructor(destination) {
|
||||
super();
|
||||
this.destination = destination;
|
||||
this.delay = 150;
|
||||
}
|
||||
|
||||
_write(chunk, encoding, callback) {
|
||||
let o = 0;
|
||||
const write = () => {
|
||||
this.destination.write(chunk.slice(o, o + 100), encoding, x => {
|
||||
o += 100;
|
||||
if (o < chunk.length) {
|
||||
setTimeout(write, this.delay);
|
||||
} else {
|
||||
callback(x);
|
||||
}
|
||||
});
|
||||
};
|
||||
setTimeout(write, this.delay);
|
||||
}
|
||||
|
||||
_final(callback) {
|
||||
setTimeout(() => {
|
||||
this.destination.end(callback);
|
||||
}, this.delay);
|
||||
}
|
||||
}
|
||||
|
||||
export default function render(url, res) {
|
||||
res.socket.on('error', error => {
|
||||
// Log fatal errors
|
||||
console.error('Fatal', error);
|
||||
});
|
||||
console.log('hello');
|
||||
let didError = false;
|
||||
const {pipe, abort} = renderToPipeableStream(<App assets={assets} />, {
|
||||
bootstrapScripts: [assets['main.js']],
|
||||
@@ -26,7 +57,10 @@ export default function render(url, res) {
|
||||
// If something errored before we started streaming, we set the error code appropriately.
|
||||
res.statusCode = didError ? 500 : 200;
|
||||
res.setHeader('Content-type', 'text/html');
|
||||
pipe(res);
|
||||
// To test the actual chunks taking time to load over the network, we throttle
|
||||
// the stream a bit.
|
||||
const throttledResponse = new ThrottledWritable(res);
|
||||
pipe(throttledResponse);
|
||||
},
|
||||
onShellError(x) {
|
||||
// Something errored before we could complete the shell so we emit an alternative shell.
|
||||
|
||||
@@ -4,6 +4,8 @@ import Theme, {ThemeToggleButton} from './Theme';
|
||||
|
||||
import './Chrome.css';
|
||||
|
||||
import LargeContent from './LargeContent';
|
||||
|
||||
export default class Chrome extends Component {
|
||||
state = {theme: 'light'};
|
||||
render() {
|
||||
@@ -25,7 +27,6 @@ export default class Chrome extends Component {
|
||||
/>
|
||||
<Suspense fallback="Loading...">
|
||||
<Theme.Provider value={this.state.theme}>
|
||||
{this.props.children}
|
||||
<div>
|
||||
<ThemeToggleButton
|
||||
onChange={theme => {
|
||||
@@ -35,8 +36,14 @@ export default class Chrome extends Component {
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{this.props.children}
|
||||
</Theme.Provider>
|
||||
</Suspense>
|
||||
<p>This should appear in the first paint.</p>
|
||||
<Suspense fallback="Loading...">
|
||||
<p>This content should not block paint.</p>
|
||||
<LargeContent />
|
||||
</Suspense>
|
||||
<script
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `assetManifest = ${JSON.stringify(assets)};`,
|
||||
|
||||
243
fixtures/ssr/src/components/LargeContent.js
Normal file
243
fixtures/ssr/src/components/LargeContent.js
Normal file
@@ -0,0 +1,243 @@
|
||||
import React, {Fragment} from 'react';
|
||||
|
||||
export default function LargeContent() {
|
||||
return (
|
||||
<Fragment>
|
||||
<p>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris
|
||||
porttitor tortor ac lectus faucibus, eget eleifend elit hendrerit.
|
||||
Integer porttitor nisi in leo congue rutrum. Morbi sed ante posuere,
|
||||
aliquam lorem ac, imperdiet orci. Duis malesuada gravida pharetra. Cras
|
||||
facilisis arcu diam, id dictum lorem imperdiet a. Suspendisse aliquet
|
||||
tempus tortor et ultricies. Aliquam libero velit, posuere tempus ante
|
||||
sed, pellentesque tincidunt lorem. Nullam iaculis, eros a varius
|
||||
aliquet, tortor felis tempor metus, nec cursus felis eros aliquam nulla.
|
||||
Vivamus ut orci sed mauris congue lacinia. Cras eget blandit neque.
|
||||
Pellentesque a massa in turpis ullamcorper volutpat vel at massa. Sed
|
||||
ante est, auctor non diam non, vulputate ultrices metus. Maecenas dictum
|
||||
fermentum quam id aliquam. Donec porta risus vitae pretium posuere.
|
||||
Fusce facilisis eros in lacus tincidunt congue.
|
||||
</p>
|
||||
<p>
|
||||
Pellentesque habitant morbi tristique senectus et netus et malesuada
|
||||
fames ac turpis egestas. Phasellus dolor ante, iaculis vel nisl vitae,
|
||||
ornare ornare orci. Praesent sit amet lobortis sapien. Suspendisse
|
||||
pharetra posuere libero ut dapibus. Donec condimentum ante urna. Aliquam
|
||||
laoreet tincidunt lacus, sed interdum tortor dapibus elementum. Nam sed
|
||||
faucibus lorem. Suspendisse finibus, velit sed molestie finibus, risus
|
||||
purus mollis ante, sit amet aliquet sapien nulla ut nibh. In eget ligula
|
||||
metus. Duis in purus mattis, blandit magna nec, dictum nunc.
|
||||
</p>
|
||||
<p>
|
||||
Sed convallis magna id tortor blandit dictum. Suspendisse in porttitor
|
||||
neque. Integer quis metus consequat, rutrum est sit amet, finibus justo.
|
||||
In hac habitasse platea dictumst. Nullam sagittis, risus sed vehicula
|
||||
porta, sapien elit ultrices nibh, vel luctus odio tortor et ante. Sed
|
||||
porta enim in hendrerit tristique. Pellentesque id feugiat libero, sit
|
||||
amet tempor enim. Proin gravida nisl justo, vel ornare dolor bibendum
|
||||
ac. Mauris scelerisque mattis facilisis. Praesent sodales augue mollis
|
||||
orci vulputate aliquet. Mauris molestie luctus neque, sed congue elit
|
||||
congue ut. Cras quis tortor augue. In auctor nulla vel turpis dapibus
|
||||
egestas. Phasellus consequat rhoncus nisi sed dignissim. Quisque varius
|
||||
justo non ex lobortis finibus cursus nec justo. Nulla erat neque,
|
||||
commodo et sem convallis, tristique faucibus odio.
|
||||
</p>
|
||||
<p>
|
||||
Ut condimentum volutpat sem, id accumsan augue placerat vel. Donec ac
|
||||
efficitur turpis. Suspendisse pretium odio euismod sapien bibendum, sed
|
||||
tempus est condimentum. Etiam nisl magna, consequat at ullamcorper at,
|
||||
sollicitudin eu eros. In mattis ligula arcu. Sed eu consectetur turpis,
|
||||
id molestie ligula. Vestibulum et venenatis enim. Donec condimentum
|
||||
vitae nisi et placerat. Sed fringilla vehicula egestas. Proin
|
||||
consectetur, nibh non ornare scelerisque, diam lorem cursus lectus, ut
|
||||
mattis mauris purus id mi. Curabitur non ligula sit amet augue molestie
|
||||
vulputate. Donec maximus magna at volutpat aliquet. Pellentesque
|
||||
dignissim nulla eget odio eleifend tincidunt. Etiam diam lorem, ornare
|
||||
vel scelerisque vel, iaculis id risus. Donec aliquet aliquam felis, ac
|
||||
vehicula lacus suscipit vitae. Morbi eu ligula elit.
|
||||
</p>
|
||||
<p>
|
||||
Praesent pellentesque, libero ut faucibus tempor, purus elit consequat
|
||||
metus, in ornare nulla lectus at erat. Duis quis blandit turpis. Fusce
|
||||
at ligula rutrum metus molestie tempor sit amet eu justo. Maecenas
|
||||
tincidunt nisl nunc. Morbi ac metus tempor, pretium arcu vel, dapibus
|
||||
velit. Nulla convallis ligula at porta mollis. Duis magna ante, mollis
|
||||
eget nibh in, congue tempor dolor. Sed tincidunt sagittis arcu, in
|
||||
ultricies neque tempor non. Suspendisse eget nunc neque. Nulla sit amet
|
||||
odio volutpat, maximus purus id, dictum metus. Integer consequat, orci
|
||||
nec ullamcorper porta, mauris libero vestibulum ipsum, nec tempor tellus
|
||||
enim non nunc. Quisque nisl risus, dapibus sit amet purus nec, aliquam
|
||||
finibus metus. Nullam condimentum urna viverra finibus cursus. Proin et
|
||||
sollicitudin tellus, porta fermentum felis. Maecenas ac turpis sed dui
|
||||
condimentum interdum sed sed erat. Mauris ut dignissim erat.
|
||||
</p>
|
||||
<p>
|
||||
Proin varius porta dui, id fringilla elit lobortis eget. Integer at
|
||||
metus elementum, efficitur eros id, euismod est. Morbi vestibulum nibh
|
||||
ac leo luctus sagittis. Praesent rhoncus, risus sit amet mattis dictum,
|
||||
diam sapien tempor neque, vel dignissim nulla neque eget ex. Nam
|
||||
sollicitudin metus quis ullamcorper dapibus. Nam tristique euismod
|
||||
efficitur. Pellentesque rhoncus vel sem eget lacinia. Pellentesque
|
||||
volutpat velit ac dignissim luctus. Vivamus euismod tortor at ligula
|
||||
mattis porta. Vestibulum ante ipsum primis in faucibus orci luctus et
|
||||
ultrices posuere cubilia curae;
|
||||
</p>
|
||||
<p>
|
||||
Proin blandit vulputate efficitur. Pellentesque sit amet porta odio.
|
||||
Nunc pulvinar varius rhoncus. Mauris fermentum leo a imperdiet pretium.
|
||||
Mauris scelerisque justo vel ante egestas, eget tempus neque malesuada.
|
||||
Sed dictum ex vel justo dignissim, aliquam commodo diam rutrum. Integer
|
||||
dignissim est ullamcorper augue laoreet consectetur id at diam. Vivamus
|
||||
molestie blandit urna, eget pulvinar augue dictum vestibulum. Duis
|
||||
maximus bibendum mauris, ut ultricies elit rhoncus eu. Praesent gravida
|
||||
placerat mauris. Praesent tempor ipsum at nibh rhoncus sagittis. Duis
|
||||
non sem turpis. Quisque et metus leo. Sed eu purus lorem. Pellentesque
|
||||
dictum metus sed leo viverra interdum. Maecenas vel tincidunt mi.
|
||||
</p>
|
||||
<p>
|
||||
Praesent consequat dapibus pellentesque. Fusce at enim id mauris laoreet
|
||||
commodo. Nullam ut mauris euismod, rhoncus tellus vel, facilisis diam.
|
||||
Aenean porta faucibus augue, a iaculis massa iaculis in. Praesent vel
|
||||
metus purus. Etiam quis augue eget orci lobortis eleifend ac ut lorem.
|
||||
Aenean non orci quis nisi molestie maximus. Mauris interdum, eros et
|
||||
aliquam aliquam, lectus diam pharetra velit, in condimentum odio eros
|
||||
non quam. Praesent bibendum pretium turpis vitae tristique. Mauris
|
||||
convallis, massa ut fermentum fermentum, libero orci tempus ipsum,
|
||||
malesuada ultrices metus sapien placerat lectus. Ut fringilla arcu nec
|
||||
lorem ultrices mattis. Etiam id tortor feugiat magna gravida gravida.
|
||||
Morbi aliquam, mi ac pellentesque mattis, erat ex venenatis erat, a
|
||||
vestibulum eros turpis quis metus. Pellentesque tempus justo in ligula
|
||||
ultricies porta. Phasellus congue felis sit amet dolor tristique
|
||||
finibus. Nunc eget eros non est ultricies vestibulum.
|
||||
</p>
|
||||
<p>
|
||||
Donec efficitur ligula quis odio tincidunt tristique. Duis urna dolor,
|
||||
hendrerit quis enim at, accumsan auctor turpis. Vivamus ante lorem,
|
||||
maximus vitae suscipit ut, congue eget velit. Maecenas sed ligula erat.
|
||||
Aliquam mollis purus at nisi porta suscipit in ut magna. Vivamus a
|
||||
turpis nec tellus egestas suscipit nec ornare nisi. Donec vestibulum
|
||||
libero quis ex suscipit, sit amet luctus leo gravida.
|
||||
</p>
|
||||
<p>
|
||||
Praesent pharetra dolor elit, sed volutpat lorem rhoncus non. Etiam a
|
||||
neque ut velit dignissim sodales. Vestibulum neque risus, condimentum
|
||||
nec consectetur vitae, ultricies ut sapien. Integer iaculis at urna sit
|
||||
amet malesuada. Integer tincidunt, felis ac vulputate semper, velit leo
|
||||
facilisis lorem, quis aliquet leo dui id lorem. Morbi non quam quis nisl
|
||||
sagittis consequat nec vitae libero. Nunc molestie pretium libero, eu
|
||||
eleifend nibh feugiat sed. Ut in bibendum diam, sit amet vehicula risus.
|
||||
Nam ornare ac nisi ac euismod. Nullam id egestas nulla. Etiam porta
|
||||
commodo ante sit amet pellentesque. Suspendisse eleifend purus in urna
|
||||
euismod auctor non vel nisi. Suspendisse rutrum est nunc, sit amet
|
||||
lacinia lacus dictum eget. Pellentesque habitant morbi tristique
|
||||
senectus et netus et malesuada fames ac turpis egestas. Morbi a blandit
|
||||
diam.
|
||||
</p>
|
||||
<p>
|
||||
Donec eget efficitur sapien. Suspendisse diam lacus, varius eu interdum
|
||||
et, congue ac justo. Proin ipsum odio, suscipit elementum mauris sed,
|
||||
porttitor congue est. Cras dapibus dictum ante, vitae gravida elit
|
||||
venenatis sed. Sed massa sem, posuere ut enim sit amet, vestibulum
|
||||
condimentum nibh. Pellentesque pulvinar sodales lacinia. Proin id
|
||||
pretium sapien, non convallis nulla. In mollis tincidunt sem et
|
||||
porttitor.
|
||||
</p>
|
||||
<p>
|
||||
Integer at sollicitudin sem. Suspendisse sed semper orci. Nulla at nibh
|
||||
nec risus suscipit posuere egestas vitae enim. Nullam mauris justo,
|
||||
mattis vel laoreet non, finibus nec nisl. Cras iaculis ultrices nibh,
|
||||
non commodo eros aliquam non. Sed vitae mollis dui, at maximus metus. Ut
|
||||
vestibulum, enim ut lobortis vulputate, lorem urna congue elit, non
|
||||
dictum odio lorem eget velit. Morbi eleifend id ligula vitae vulputate.
|
||||
Suspendisse ac laoreet justo. Proin eu mattis diam.
|
||||
</p>
|
||||
<p>
|
||||
Nunc in ex quis enim ullamcorper scelerisque eget ac eros. Class aptent
|
||||
taciti sociosqu ad litora torquent per conubia nostra, per inceptos
|
||||
himenaeos. Aliquam turpis dui, egestas a rhoncus non, fermentum in
|
||||
tellus. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices
|
||||
posuere cubilia curae; Aenean non risus arcu. Nam ultricies lacinia
|
||||
volutpat. Class aptent taciti sociosqu ad litora torquent per conubia
|
||||
nostra, per inceptos himenaeos. Lorem ipsum dolor sit amet, consectetur
|
||||
adipiscing elit.
|
||||
</p>
|
||||
<p>
|
||||
Aliquam a felis leo. Proin lorem ipsum, congue eu cursus in, rhoncus ut
|
||||
libero. Vestibulum sit amet consequat nunc. Ut eleifend lobortis lacus,
|
||||
vel molestie metus viverra eget. Nullam suscipit eu magna scelerisque
|
||||
suscipit. Donec dictum in diam nec lacinia. Mauris pellentesque ex ut
|
||||
purus facilisis, eget placerat turpis semper. Sed dapibus lorem ante, et
|
||||
malesuada dui eleifend ac. Sed diam felis, semper ac nulla vel, posuere
|
||||
ultricies ante.
|
||||
</p>
|
||||
<p>
|
||||
Nunc elementum odio sapien, sit amet vulputate lorem varius at. Fusce
|
||||
non sapien vitae lorem aliquam pretium sit amet congue dolor. Nunc quis
|
||||
tortor luctus, pretium ex a, tincidunt urna. Aliquam fermentum massa a
|
||||
erat pharetra varius. Curabitur at auctor dui. Sed posuere pellentesque
|
||||
massa, vel bibendum urna dictum non. Fusce eget rhoncus urna. Maecenas
|
||||
sed lectus tellus. Pellentesque convallis dapibus nisl vitae venenatis.
|
||||
Quisque ornare a dolor ac pharetra. Nam cursus, mi a lacinia accumsan,
|
||||
felis erat fringilla magna, ac mattis nunc ante a orci.
|
||||
</p>
|
||||
<p>
|
||||
Nunc vel tortor euismod, commodo tortor non, aliquam nisi. Maecenas
|
||||
tempus mollis velit non suscipit. Mauris sit amet dolor sed ex fringilla
|
||||
varius. Suspendisse vel cursus risus. Vivamus pharetra massa nec dolor
|
||||
aliquam feugiat. Fusce finibus enim commodo, scelerisque ante eu,
|
||||
laoreet ex. Curabitur placerat magna quis imperdiet lacinia. Etiam
|
||||
lectus mauris, porttitor ac lacinia sed, posuere eget lacus. Mauris
|
||||
vulputate mattis imperdiet. Nunc id aliquet libero, vitae hendrerit
|
||||
purus. Praesent vestibulum urna ac egestas tempor. In molestie, nunc sit
|
||||
amet sagittis dapibus, ligula enim fermentum mi, lacinia molestie eros
|
||||
dui in tortor. Mauris fermentum pulvinar faucibus. Curabitur laoreet
|
||||
eleifend purus, non tincidunt tortor gravida nec. Nam eu lectus congue,
|
||||
commodo libero et, porttitor est. Nullam tincidunt, nisi eu congue
|
||||
congue, magna justo commodo massa, nec efficitur dui lectus non sem.
|
||||
</p>
|
||||
<p>
|
||||
Nullam vehicula, ipsum quis lacinia tristique, elit nulla dignissim
|
||||
augue, at pulvinar metus justo ac magna. Nullam nec nunc ac sapien
|
||||
mollis cursus eu ac enim. Pellentesque a pharetra erat. Ut tempor magna
|
||||
nisi, accumsan blandit lectus volutpat nec. Vivamus vel lorem nec eros
|
||||
blandit dictum eget ac diam. Nulla nec turpis dolor. Morbi eu euismod
|
||||
libero. Nam ut tortor at arcu porta tincidunt. In gravida ligula
|
||||
fringilla ornare imperdiet. Nulla scelerisque ante erat, efficitur
|
||||
dictum metus ullamcorper vel. Nam ac purus metus. Maecenas eget tempus
|
||||
nulla. Ut magna lorem, efficitur ut ex a, semper aliquam magna. Praesent
|
||||
lobortis, velit ac posuere mattis, justo est accumsan turpis, id
|
||||
sagittis felis mi in lacus.
|
||||
</p>
|
||||
<p>
|
||||
Aenean est mi, semper nec sem at, malesuada consectetur nunc. Aenean
|
||||
consequat sem quis sem consequat, non aliquam est placerat. Cras
|
||||
malesuada magna neque, et pellentesque nibh consequat at. Sed interdum
|
||||
velit et ex interdum, vel lobortis ante vestibulum. Nam placerat lectus
|
||||
eu commodo efficitur. Pellentesque in nunc ac massa porttitor eleifend
|
||||
ut efficitur sem. Aenean at magna auctor, posuere augue in, ultrices
|
||||
arcu. Praesent dignissim augue ex, malesuada maximus metus interdum a.
|
||||
Proin nec odio in nulla vestibulum.
|
||||
</p>
|
||||
<p>
|
||||
Aenean est mi, semper nec sem at, malesuada consectetur nunc. Aenean
|
||||
consequat sem quis sem consequat, non aliquam est placerat. Cras
|
||||
malesuada magna neque, et pellentesque nibh consequat at. Sed interdum
|
||||
velit et ex interdum, vel lobortis ante vestibulum. Nam placerat lectus
|
||||
eu commodo efficitur. Pellentesque in nunc ac massa porttitor eleifend
|
||||
ut efficitur sem. Aenean at magna auctor, posuere augue in, ultrices
|
||||
arcu. Praesent dignissim augue ex, malesuada maximus metus interdum a.
|
||||
Proin nec odio in nulla vestibulum.
|
||||
</p>
|
||||
<p>
|
||||
Aenean est mi, semper nec sem at, malesuada consectetur nunc. Aenean
|
||||
consequat sem quis sem consequat, non aliquam est placerat. Cras
|
||||
malesuada magna neque, et pellentesque nibh consequat at. Sed interdum
|
||||
velit et ex interdum, vel lobortis ante vestibulum. Nam placerat lectus
|
||||
eu commodo efficitur. Pellentesque in nunc ac massa porttitor eleifend
|
||||
ut efficitur sem. Aenean at magna auctor, posuere augue in, ultrices
|
||||
arcu. Praesent dignissim augue ex, malesuada maximus metus interdum a.
|
||||
Proin nec odio in nulla vestibulum.
|
||||
</p>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
@@ -146,7 +146,7 @@
|
||||
"publish-prereleases": "echo 'This command has been deprecated. Please refer to https://github.com/facebook/react/tree/main/scripts/release#trigger-an-automated-prerelease'",
|
||||
"download-build": "node ./scripts/release/download-experimental-build.js",
|
||||
"download-build-for-head": "node ./scripts/release/download-experimental-build.js --commit=$(git rev-parse HEAD)",
|
||||
"download-build-in-codesandbox-ci": "yarn build --type=node react/index react-dom/index react-dom/client react-dom/src/server react-dom/test-utils scheduler/index react/jsx-runtime react/jsx-dev-runtime",
|
||||
"download-build-in-codesandbox-ci": "yarn build --type=node react/index react.react-server react-dom/index react-dom/client react-dom/src/server react-dom/test-utils react-dom.react-server scheduler/index react/jsx-runtime react/jsx-dev-runtime react-server-dom-webpack",
|
||||
"check-release-dependencies": "node ./scripts/release/check-release-dependencies",
|
||||
"generate-inline-fizz-runtime": "node ./scripts/rollup/generate-inline-fizz-runtime.js",
|
||||
"flags": "node ./scripts/flags/flags.js"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* global chrome */
|
||||
|
||||
import {normalizeUrl} from 'react-devtools-shared/src/utils';
|
||||
import {normalizeUrlIfValid} from 'react-devtools-shared/src/utils';
|
||||
import {__DEBUG__} from 'react-devtools-shared/src/constants';
|
||||
|
||||
let debugIDCounter = 0;
|
||||
@@ -117,7 +117,7 @@ async function fetchFileWithCaching(url: string): Promise<string> {
|
||||
chrome.devtools.inspectedWindow.getResources(r => resolve(r)),
|
||||
);
|
||||
|
||||
const normalizedReferenceURL = normalizeUrl(url);
|
||||
const normalizedReferenceURL = normalizeUrlIfValid(url);
|
||||
const resource = resources.find(r => r.url === normalizedReferenceURL);
|
||||
|
||||
if (resource != null) {
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
LOCAL_STORAGE_TRACE_UPDATES_ENABLED_KEY,
|
||||
} from 'react-devtools-shared/src/constants';
|
||||
import {logEvent} from 'react-devtools-shared/src/Logger';
|
||||
import {normalizeUrlIfValid} from 'react-devtools-shared/src/utils';
|
||||
|
||||
import {
|
||||
setBrowserSelectionFromReact,
|
||||
@@ -128,7 +129,11 @@ function createBridgeAndStore() {
|
||||
: source;
|
||||
|
||||
// We use 1-based line and column, Chrome expects them 0-based.
|
||||
chrome.devtools.panels.openResource(sourceURL, line - 1, column - 1);
|
||||
chrome.devtools.panels.openResource(
|
||||
normalizeUrlIfValid(sourceURL),
|
||||
line - 1,
|
||||
column - 1,
|
||||
);
|
||||
};
|
||||
|
||||
// TODO (Webpack 5) Hopefully we can remove this prop after the Webpack 5 migration.
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
"@babel/preset-env": "^7.11.0",
|
||||
"@babel/preset-flow": "^7.10.4",
|
||||
"@babel/preset-react": "^7.10.4",
|
||||
"@playwright/test": "^1.16.3",
|
||||
"@playwright/test": "^1.51.1",
|
||||
"babel-core": "^7.0.0-bridge",
|
||||
"babel-eslint": "^9.0.0",
|
||||
"babel-loader": "^8.0.4",
|
||||
|
||||
@@ -815,6 +815,130 @@ describe('InspectedElement', () => {
|
||||
`);
|
||||
});
|
||||
|
||||
it('should support Thenables in React 19', async () => {
|
||||
const Example = () => null;
|
||||
|
||||
class SubclassedPromise extends Promise {}
|
||||
|
||||
const plainThenable = {then() {}};
|
||||
const subclassedPromise = new SubclassedPromise(() => {});
|
||||
const unusedPromise = Promise.resolve();
|
||||
const usedFulfilledPromise = Promise.resolve();
|
||||
const usedFulfilledRichPromise = Promise.resolve({
|
||||
some: {
|
||||
deeply: {
|
||||
nested: {
|
||||
object: {
|
||||
string: 'test',
|
||||
fn: () => {},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const usedPendingPromise = new Promise(resolve => {});
|
||||
const usedRejectedPromise = Promise.reject(
|
||||
new Error('test-error-do-not-surface'),
|
||||
);
|
||||
|
||||
function Use({value}) {
|
||||
React.use(value);
|
||||
}
|
||||
|
||||
await utils.actAsync(() =>
|
||||
render(
|
||||
<>
|
||||
<Example
|
||||
plainThenable={plainThenable}
|
||||
subclassedPromise={subclassedPromise}
|
||||
unusedPromise={unusedPromise}
|
||||
usedFulfilledPromise={usedFulfilledPromise}
|
||||
usedFulfilledRichPromise={usedFulfilledRichPromise}
|
||||
usedPendingPromise={usedPendingPromise}
|
||||
usedRejectedPromise={usedRejectedPromise}
|
||||
/>
|
||||
<React.Suspense>
|
||||
<Use value={usedPendingPromise} />
|
||||
</React.Suspense>
|
||||
<React.Suspense>
|
||||
<Use value={usedFulfilledPromise} />
|
||||
</React.Suspense>
|
||||
<React.Suspense>
|
||||
<Use value={usedFulfilledRichPromise} />
|
||||
</React.Suspense>
|
||||
<ErrorBoundary>
|
||||
<React.Suspense>
|
||||
<Use value={usedRejectedPromise} />
|
||||
</React.Suspense>
|
||||
</ErrorBoundary>
|
||||
</>,
|
||||
),
|
||||
);
|
||||
|
||||
const inspectedElement = await inspectElementAtIndex(0);
|
||||
|
||||
expect(inspectedElement.props).toMatchInlineSnapshot(`
|
||||
{
|
||||
"plainThenable": Dehydrated {
|
||||
"preview_short": Thenable,
|
||||
"preview_long": Thenable,
|
||||
},
|
||||
"subclassedPromise": Dehydrated {
|
||||
"preview_short": SubclassedPromise,
|
||||
"preview_long": SubclassedPromise,
|
||||
},
|
||||
"unusedPromise": Dehydrated {
|
||||
"preview_short": Promise,
|
||||
"preview_long": Promise,
|
||||
},
|
||||
"usedFulfilledPromise": {
|
||||
"value": undefined,
|
||||
},
|
||||
"usedFulfilledRichPromise": {
|
||||
"value": Dehydrated {
|
||||
"preview_short": {…},
|
||||
"preview_long": {some: {…}},
|
||||
},
|
||||
},
|
||||
"usedPendingPromise": Dehydrated {
|
||||
"preview_short": pending Promise,
|
||||
"preview_long": pending Promise,
|
||||
},
|
||||
"usedRejectedPromise": {
|
||||
"reason": Dehydrated {
|
||||
"preview_short": Error: test-error-do-not-surface,
|
||||
"preview_long": Error: test-error-do-not-surface,
|
||||
},
|
||||
},
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('should support Promises in React 18', async () => {
|
||||
const Example = () => null;
|
||||
|
||||
const unusedPromise = Promise.resolve();
|
||||
|
||||
await utils.actAsync(() =>
|
||||
render(
|
||||
<>
|
||||
<Example unusedPromise={unusedPromise} />
|
||||
</>,
|
||||
),
|
||||
);
|
||||
|
||||
const inspectedElement = await inspectElementAtIndex(0);
|
||||
|
||||
expect(inspectedElement.props).toMatchInlineSnapshot(`
|
||||
{
|
||||
"unusedPromise": Dehydrated {
|
||||
"preview_short": Promise,
|
||||
"preview_long": Promise,
|
||||
},
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('should not consume iterables while inspecting', async () => {
|
||||
const Example = () => null;
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ export const THEME_STYLES: {[style: Theme | DisplayDensity]: any, ...} = {
|
||||
'--color-background-selected': '#0088fa',
|
||||
'--color-button-background': '#ffffff',
|
||||
'--color-button-background-focus': '#ededed',
|
||||
'--color-button-background-hover': 'rgba(0, 0, 0, 0.2)',
|
||||
'--color-button': '#5f6673',
|
||||
'--color-button-disabled': '#cfd1d5',
|
||||
'--color-button-active': '#0088fa',
|
||||
@@ -174,6 +175,7 @@ export const THEME_STYLES: {[style: Theme | DisplayDensity]: any, ...} = {
|
||||
'--color-background-selected': '#178fb9',
|
||||
'--color-button-background': '#282c34',
|
||||
'--color-button-background-focus': '#3d424a',
|
||||
'--color-button-background-hover': 'rgba(255, 255, 255, 0.2)',
|
||||
'--color-button': '#afb3b9',
|
||||
'--color-button-active': '#61dafb',
|
||||
'--color-button-disabled': '#4f5766',
|
||||
|
||||
@@ -19,7 +19,6 @@ import {
|
||||
} from 'react-devtools-shared/src/storage';
|
||||
import InspectedElementErrorBoundary from './InspectedElementErrorBoundary';
|
||||
import InspectedElement from './InspectedElement';
|
||||
import {InspectedElementContextController} from './InspectedElementContext';
|
||||
import {ModalDialog} from '../ModalDialog';
|
||||
import SettingsModal from 'react-devtools-shared/src/devtools/views/Settings/SettingsModal';
|
||||
import {NativeStyleContextController} from './NativeStyleEditor/context';
|
||||
@@ -162,9 +161,7 @@ function Components(_: {}) {
|
||||
<div className={styles.InspectedElementWrapper}>
|
||||
<NativeStyleContextController>
|
||||
<InspectedElementErrorBoundary>
|
||||
<InspectedElementContextController>
|
||||
<InspectedElement />
|
||||
</InspectedElementContextController>
|
||||
<InspectedElement />
|
||||
</InspectedElementErrorBoundary>
|
||||
</NativeStyleContextController>
|
||||
</div>
|
||||
|
||||
@@ -28,6 +28,7 @@ import {SettingsContextController} from './Settings/SettingsContext';
|
||||
import {TreeContextController} from './Components/TreeContext';
|
||||
import ViewElementSourceContext from './Components/ViewElementSourceContext';
|
||||
import FetchFileWithCachingContext from './Components/FetchFileWithCachingContext';
|
||||
import {InspectedElementContextController} from './Components/InspectedElementContext';
|
||||
import HookNamesModuleLoaderContext from 'react-devtools-shared/src/devtools/views/Components/HookNamesModuleLoaderContext';
|
||||
import {ProfilerContextController} from './Profiler/ProfilerContext';
|
||||
import {TimelineContextController} from 'react-devtools-timeline/src/TimelineContext';
|
||||
@@ -276,43 +277,47 @@ export default function DevTools({
|
||||
<TreeContextController>
|
||||
<ProfilerContextController>
|
||||
<TimelineContextController>
|
||||
<ThemeProvider>
|
||||
<div
|
||||
className={styles.DevTools}
|
||||
ref={devToolsRef}
|
||||
data-react-devtools-portal-root={true}>
|
||||
{showTabBar && (
|
||||
<div className={styles.TabBar}>
|
||||
<ReactLogo />
|
||||
<span className={styles.DevToolsVersion}>
|
||||
{process.env.DEVTOOLS_VERSION}
|
||||
</span>
|
||||
<div className={styles.Spacer} />
|
||||
<TabBar
|
||||
currentTab={tab}
|
||||
id="DevTools"
|
||||
selectTab={selectTab}
|
||||
tabs={tabs}
|
||||
type="navigation"
|
||||
<InspectedElementContextController>
|
||||
<ThemeProvider>
|
||||
<div
|
||||
className={styles.DevTools}
|
||||
ref={devToolsRef}
|
||||
data-react-devtools-portal-root={true}>
|
||||
{showTabBar && (
|
||||
<div className={styles.TabBar}>
|
||||
<ReactLogo />
|
||||
<span className={styles.DevToolsVersion}>
|
||||
{process.env.DEVTOOLS_VERSION}
|
||||
</span>
|
||||
<div className={styles.Spacer} />
|
||||
<TabBar
|
||||
currentTab={tab}
|
||||
id="DevTools"
|
||||
selectTab={selectTab}
|
||||
tabs={tabs}
|
||||
type="navigation"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={styles.TabContent}
|
||||
hidden={tab !== 'components'}>
|
||||
<Components
|
||||
portalContainer={
|
||||
componentsPortalContainer
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className={styles.TabContent}
|
||||
hidden={tab !== 'profiler'}>
|
||||
<Profiler
|
||||
portalContainer={profilerPortalContainer}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={styles.TabContent}
|
||||
hidden={tab !== 'components'}>
|
||||
<Components
|
||||
portalContainer={componentsPortalContainer}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className={styles.TabContent}
|
||||
hidden={tab !== 'profiler'}>
|
||||
<Profiler
|
||||
portalContainer={profilerPortalContainer}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</ThemeProvider>
|
||||
</ThemeProvider>
|
||||
</InspectedElementContextController>
|
||||
</TimelineContextController>
|
||||
</ProfilerContextController>
|
||||
</TreeContextController>
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
.LoadHookNamesToggle,
|
||||
.ToggleError {
|
||||
padding: 2px;
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
bottom: -0.2em;
|
||||
margin-block: -1em;
|
||||
}
|
||||
|
||||
.ToggleError {
|
||||
color: var(--color-error-text);
|
||||
}
|
||||
|
||||
.Hook {
|
||||
list-style-type: none;
|
||||
margin: 0;
|
||||
padding-left: 0.5rem;
|
||||
line-height: 1.125rem;
|
||||
|
||||
font-family: var(--font-family-monospace);
|
||||
font-size: var(--font-size-monospace-normal);
|
||||
}
|
||||
|
||||
.Hook .Hook {
|
||||
padding-left: 1rem;
|
||||
}
|
||||
|
||||
.Name {
|
||||
color: var(--color-dim);
|
||||
flex: 0 0 auto;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.PrimitiveHookName {
|
||||
color: var(--color-text);
|
||||
flex: 0 0 auto;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.Name:after {
|
||||
color: var(--color-text);
|
||||
content: ': ';
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
.PrimitiveHookNumber {
|
||||
background-color: var(--color-primitive-hook-badge-background);
|
||||
color: var(--color-primitive-hook-badge-text);
|
||||
font-size: var(--font-size-monospace-small);
|
||||
margin-right: 0.25rem;
|
||||
border-radius: 0.125rem;
|
||||
padding: 0.125rem 0.25rem;
|
||||
}
|
||||
|
||||
.HookName {
|
||||
color: var(--color-component-name);
|
||||
}
|
||||
207
packages/react-devtools-shared/src/devtools/views/Profiler/HookChangeSummary.js
vendored
Normal file
207
packages/react-devtools-shared/src/devtools/views/Profiler/HookChangeSummary.js
vendored
Normal file
@@ -0,0 +1,207 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import {
|
||||
useContext,
|
||||
useMemo,
|
||||
useCallback,
|
||||
memo,
|
||||
useState,
|
||||
useEffect,
|
||||
} from 'react';
|
||||
import styles from './HookChangeSummary.css';
|
||||
import ButtonIcon from '../ButtonIcon';
|
||||
import {InspectedElementContext} from '../Components/InspectedElementContext';
|
||||
import {StoreContext} from '../context';
|
||||
|
||||
import {
|
||||
getAlreadyLoadedHookNames,
|
||||
getHookSourceLocationKey,
|
||||
} from 'react-devtools-shared/src/hookNamesCache';
|
||||
import Toggle from '../Toggle';
|
||||
import type {HooksNode} from 'react-debug-tools/src/ReactDebugHooks';
|
||||
import type {ChangeDescription} from './types';
|
||||
|
||||
// $FlowFixMe: Flow doesn't know about Intl.ListFormat
|
||||
const hookListFormatter = new Intl.ListFormat('en', {
|
||||
style: 'long',
|
||||
type: 'conjunction',
|
||||
});
|
||||
|
||||
type HookProps = {
|
||||
hook: HooksNode,
|
||||
hookNames: Map<string, string> | null,
|
||||
};
|
||||
|
||||
const Hook: React.AbstractComponent<HookProps> = memo(({hook, hookNames}) => {
|
||||
const hookSource = hook.hookSource;
|
||||
const hookName = useMemo(() => {
|
||||
if (!hookSource || !hookNames) return null;
|
||||
const key = getHookSourceLocationKey(hookSource);
|
||||
return hookNames.get(key) || null;
|
||||
}, [hookSource, hookNames]);
|
||||
|
||||
return (
|
||||
<ul className={styles.Hook}>
|
||||
<li>
|
||||
{hook.id !== null && (
|
||||
<span className={styles.PrimitiveHookNumber}>
|
||||
{String(hook.id + 1)}
|
||||
</span>
|
||||
)}
|
||||
<span
|
||||
className={hook.id !== null ? styles.PrimitiveHookName : styles.Name}>
|
||||
{hook.name}
|
||||
{hookName && <span className={styles.HookName}>({hookName})</span>}
|
||||
</span>
|
||||
{hook.subHooks?.map((subHook, index) => (
|
||||
<Hook key={hook.id} hook={subHook} hookNames={hookNames} />
|
||||
))}
|
||||
</li>
|
||||
</ul>
|
||||
);
|
||||
});
|
||||
|
||||
const shouldKeepHook = (
|
||||
hook: HooksNode,
|
||||
hooksArray: Array<number>,
|
||||
): boolean => {
|
||||
if (hook.id !== null && hooksArray.includes(hook.id)) {
|
||||
return true;
|
||||
}
|
||||
const subHooks = hook.subHooks;
|
||||
if (subHooks == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return subHooks.some(subHook => shouldKeepHook(subHook, hooksArray));
|
||||
};
|
||||
|
||||
const filterHooks = (
|
||||
hook: HooksNode,
|
||||
hooksArray: Array<number>,
|
||||
): HooksNode | null => {
|
||||
if (!shouldKeepHook(hook, hooksArray)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const subHooks = hook.subHooks;
|
||||
if (subHooks == null) {
|
||||
return hook;
|
||||
}
|
||||
|
||||
const filteredSubHooks = subHooks
|
||||
.map(subHook => filterHooks(subHook, hooksArray))
|
||||
.filter(Boolean);
|
||||
return filteredSubHooks.length > 0
|
||||
? {...hook, subHooks: filteredSubHooks}
|
||||
: hook;
|
||||
};
|
||||
|
||||
type Props = {|
|
||||
fiberID: number,
|
||||
hooks: $PropertyType<ChangeDescription, 'hooks'>,
|
||||
state: $PropertyType<ChangeDescription, 'state'>,
|
||||
displayMode?: 'detailed' | 'compact',
|
||||
|};
|
||||
|
||||
const HookChangeSummary: React.AbstractComponent<Props> = memo(
|
||||
({hooks, fiberID, state, displayMode = 'detailed'}: Props) => {
|
||||
const {parseHookNames, toggleParseHookNames, inspectedElement} = useContext(
|
||||
InspectedElementContext,
|
||||
);
|
||||
const store = useContext(StoreContext);
|
||||
|
||||
const [parseHookNamesOptimistic, setParseHookNamesOptimistic] =
|
||||
useState<boolean>(parseHookNames);
|
||||
|
||||
useEffect(() => {
|
||||
setParseHookNamesOptimistic(parseHookNames);
|
||||
}, [inspectedElement?.id, parseHookNames]);
|
||||
|
||||
const handleOnChange = useCallback(() => {
|
||||
setParseHookNamesOptimistic(!parseHookNames);
|
||||
toggleParseHookNames();
|
||||
}, [toggleParseHookNames, parseHookNames]);
|
||||
|
||||
const element = fiberID !== null ? store.getElementByID(fiberID) : null;
|
||||
const hookNames =
|
||||
element != null ? getAlreadyLoadedHookNames(element) : null;
|
||||
|
||||
const filteredHooks = useMemo(() => {
|
||||
if (!hooks || !inspectedElement?.hooks) return null;
|
||||
return inspectedElement.hooks
|
||||
.map(hook => filterHooks(hook, hooks))
|
||||
.filter(Boolean);
|
||||
}, [inspectedElement?.hooks, hooks]);
|
||||
|
||||
const hookParsingFailed = parseHookNames && hookNames === null;
|
||||
|
||||
if (!hooks?.length) {
|
||||
return <span>No hooks changed</span>;
|
||||
}
|
||||
|
||||
if (
|
||||
inspectedElement?.id !== element?.id ||
|
||||
filteredHooks?.length !== hooks.length ||
|
||||
displayMode === 'compact'
|
||||
) {
|
||||
const hookIds = hooks.map(hookId => String(hookId + 1));
|
||||
const hookWord = hookIds.length === 1 ? '• Hook' : '• Hooks';
|
||||
return (
|
||||
<span>
|
||||
{hookWord} {hookListFormatter.format(hookIds)} changed
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
let toggleTitle: string;
|
||||
if (hookParsingFailed) {
|
||||
toggleTitle = 'Hook parsing failed';
|
||||
} else if (parseHookNamesOptimistic) {
|
||||
toggleTitle = 'Parsing hook names ...';
|
||||
} else {
|
||||
toggleTitle = 'Parse hook names (may be slow)';
|
||||
}
|
||||
|
||||
if (filteredHooks == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{filteredHooks.length > 1 ? '• Hooks changed:' : '• Hook changed:'}
|
||||
{(!parseHookNames || hookParsingFailed) && (
|
||||
<Toggle
|
||||
className={
|
||||
hookParsingFailed
|
||||
? styles.ToggleError
|
||||
: styles.LoadHookNamesToggle
|
||||
}
|
||||
isChecked={parseHookNamesOptimistic}
|
||||
isDisabled={parseHookNamesOptimistic || hookParsingFailed}
|
||||
onChange={handleOnChange}
|
||||
title={toggleTitle}>
|
||||
<ButtonIcon type="parse-hook-names" />
|
||||
</Toggle>
|
||||
)}
|
||||
{filteredHooks.map(hook => (
|
||||
<Hook
|
||||
key={`${inspectedElement?.id ?? 'unknown'}-${hook.id}`}
|
||||
hook={hook}
|
||||
hookNames={hookNames}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
export default HookChangeSummary;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user