Compare commits
52 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6cfde32738 | ||
|
|
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"
|
||||
|
||||
|
||||
@@ -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) ||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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.
|
||||
@@ -8,19 +8,24 @@
|
||||
"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",
|
||||
"@babel/types": "^7.26.0",
|
||||
"@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": {},
|
||||
"devDependencies": {
|
||||
"@types/html-to-text": "^9.0.4"
|
||||
},
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -14,6 +14,16 @@ 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;
|
||||
@@ -51,12 +61,17 @@ export async function compile({
|
||||
`Expected BabelPluginReactCompiler to compile successfully, got ${result}`,
|
||||
);
|
||||
}
|
||||
result.code = await prettier.format(result.code, {
|
||||
semi: false,
|
||||
parser: 'babel-ts',
|
||||
});
|
||||
if (result.code != null) {
|
||||
lastResult = 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;
|
||||
}
|
||||
|
||||
@@ -5,110 +5,76 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {
|
||||
McpServer,
|
||||
ResourceTemplate,
|
||||
} from '@modelcontextprotocol/sdk/server/mcp.js';
|
||||
import {McpServer} from '@modelcontextprotocol/sdk/server/mcp.js';
|
||||
import {StdioServerTransport} from '@modelcontextprotocol/sdk/server/stdio.js';
|
||||
import {z} from 'zod';
|
||||
import {compile} from './compiler';
|
||||
import {compile, type PrintedCompilerPipelineValue} from './compiler';
|
||||
import {
|
||||
CompilerPipelineValue,
|
||||
printReactiveFunctionWithOutlined,
|
||||
printFunctionWithOutlined,
|
||||
PluginOptions,
|
||||
SourceLocation,
|
||||
} from 'babel-plugin-react-compiler/src';
|
||||
import {liteClient, type SearchResponse} from 'algoliasearch/lite';
|
||||
import {DocSearchHit} from './types/algolia';
|
||||
import {printHierarchy} from './utils/algolia';
|
||||
|
||||
// https://github.com/reactjs/react.dev/blob/55986965fbf69c2584040039c9586a01bd54eba7/src/siteConfig.js#L15-L19
|
||||
const ALGOLIA_CONFIG = {
|
||||
appId: '1FCF9AYYAT',
|
||||
apiKey: '1b7ad4e1c89e645e351e59d40544eda1',
|
||||
indexName: 'beta-react',
|
||||
};
|
||||
|
||||
const client = liteClient(ALGOLIA_CONFIG.appId, ALGOLIA_CONFIG.apiKey);
|
||||
|
||||
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};
|
||||
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',
|
||||
});
|
||||
|
||||
// TODO: how to verify this works?
|
||||
server.resource(
|
||||
'docs',
|
||||
new ResourceTemplate('docs://{message}', {list: undefined}),
|
||||
async (uri, {message}) => {
|
||||
const {results} = await 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;
|
||||
return {
|
||||
contents: hits.map(hit => {
|
||||
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 {
|
||||
uri: uri.href,
|
||||
text: hit.url,
|
||||
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(
|
||||
'optimize',
|
||||
'Use React Compiler to optimize React code. Optionally, for debugging provide a pass name like "HIR" to see more information.',
|
||||
'compile',
|
||||
'Compile code with React Compiler. Optionally, for debugging provide a pass name like "HIR" to see more information.',
|
||||
{
|
||||
text: z.string(),
|
||||
passName: z.string().optional(),
|
||||
passName: z.enum(['HIR', 'ReactiveFunction', 'All', '@DEBUG']).optional(),
|
||||
},
|
||||
async ({text, passName}) => {
|
||||
const pipelinePasses = new Map<
|
||||
@@ -158,15 +124,28 @@ server.tool(
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
const _: never = result;
|
||||
throw new Error(`Unhandled result ${result}`);
|
||||
assertExhaustive(result, `Unhandled result ${result}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
const compilerOptions = {
|
||||
const errors: Array<{message: string; loc: SourceLocation | null}> = [];
|
||||
const compilerOptions: Partial<PluginOptions> = {
|
||||
panicThreshold: 'none',
|
||||
logger: {
|
||||
debugLogIRs: logIR,
|
||||
logEvent: () => {},
|
||||
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 {
|
||||
@@ -178,73 +157,196 @@ server.tool(
|
||||
if (result.code == null) {
|
||||
return {
|
||||
isError: true,
|
||||
content: [{type: 'text', text: 'Error: Could not compile'}],
|
||||
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',
|
||||
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', text: result.code}, ...requestedPasses],
|
||||
content: [
|
||||
{type: 'text' as const, text: result.code},
|
||||
...requestedPasses,
|
||||
],
|
||||
};
|
||||
} catch (err) {
|
||||
return {
|
||||
isError: true,
|
||||
content: [{type: 'text', text: `Error: ${err.stack}`}],
|
||||
content: [{type: 'text' as const, text: `Error: ${err.stack}`}],
|
||||
};
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
server.prompt('review-code', {code: z.string()}, ({code}) => ({
|
||||
server.prompt('review-react-code', () => ({
|
||||
messages: [
|
||||
{
|
||||
role: 'assistant',
|
||||
content: {
|
||||
type: 'text',
|
||||
text: `# React Expert Assistant
|
||||
|
||||
text: `
|
||||
## Role
|
||||
You are a React assistant that helps users write better React, following the rules of React in the react.dev docs.
|
||||
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.
|
||||
|
||||
## Available Resources
|
||||
- 'docs': Look up documentation from React.dev. Returns urls that you must retrieve so you can view its content.
|
||||
## 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
|
||||
- 'optimize': Run the users code through React Compiler
|
||||
- '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. Check if the users code follows the rules of React
|
||||
- Point out issues in the users code if it does not
|
||||
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. Run the compiler on the users code and see if it can successfully optimize the code
|
||||
- If the same code is returned by the compiler, it has bailed out or there is nothing to optimize
|
||||
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. Iterate
|
||||
- Guide the user on making adjustments to their code so that it can be successfully optimized.
|
||||
- If it was already successfully optimized, check how many items were cached previously and compare them to each new attempt. For example, you can refer to the cache size const $ = _c(n); as a rough heuristic, where n is the size of the cache as an integer. Higher is better.
|
||||
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
|
||||
|
||||
## Special Instructions
|
||||
Make sure to use information from react.dev as the main reference for your React knowledge. Information from unofficial sources such as blogs and articles can also be used but may sometimes be outdated or contain poor advice.
|
||||
## 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
|
||||
|
||||
## Example 1: <todo>
|
||||
|
||||
## Example 2: <todo>
|
||||
|
||||
Review the following code:
|
||||
|
||||
${code}
|
||||
## 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
|
||||
`,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
// https://github.com/algolia/docsearch/blob/15ebcba606b281aa0dddc4ccb8feb19d396bf79e/packages/docsearch-react/src/types/DocSearchHit.ts
|
||||
type ContentType =
|
||||
| 'content'
|
||||
|
||||
@@ -6,6 +6,19 @@
|
||||
*/
|
||||
|
||||
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,
|
||||
@@ -28,3 +41,79 @@ export function printHierarchy(
|
||||
}
|
||||
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)
|
||||
@@ -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({
|
||||
|
||||
@@ -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');
|
||||
|
||||
|
||||
@@ -7,121 +7,121 @@
|
||||
resolved "https://registry.yarnpkg.com/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz#bd9154aec9983f77b3a034ecaa015c2e4201f6cf"
|
||||
integrity sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==
|
||||
|
||||
"@algolia/client-abtesting@5.23.3":
|
||||
version "5.23.3"
|
||||
resolved "https://registry.yarnpkg.com/@algolia/client-abtesting/-/client-abtesting-5.23.3.tgz#efc2ad31792675a26cfac12cc0ef3adbd4766a11"
|
||||
integrity sha512-yHI0hBwYcNPc+nJoHPTmmlP8pG6nstCEhpHaZQCDwLZhdMtNhd1hliZMCtLgNnvd1yKEgTt/ZDnTSdZLehfKdA==
|
||||
"@algolia/client-abtesting@5.23.4":
|
||||
version "5.23.4"
|
||||
resolved "https://registry.yarnpkg.com/@algolia/client-abtesting/-/client-abtesting-5.23.4.tgz#de89e757ca26e003dc4dbd7e7fac35c3071caaa4"
|
||||
integrity sha512-WIMT2Kxy+FFWXWQxIU8QgbTioL+SGE24zhpj0kipG4uQbzXwONaWt7ffaYLjfge3gcGSgJVv+1VlahVckafluQ==
|
||||
dependencies:
|
||||
"@algolia/client-common" "5.23.3"
|
||||
"@algolia/requester-browser-xhr" "5.23.3"
|
||||
"@algolia/requester-fetch" "5.23.3"
|
||||
"@algolia/requester-node-http" "5.23.3"
|
||||
"@algolia/client-common" "5.23.4"
|
||||
"@algolia/requester-browser-xhr" "5.23.4"
|
||||
"@algolia/requester-fetch" "5.23.4"
|
||||
"@algolia/requester-node-http" "5.23.4"
|
||||
|
||||
"@algolia/client-analytics@5.23.3":
|
||||
version "5.23.3"
|
||||
resolved "https://registry.yarnpkg.com/@algolia/client-analytics/-/client-analytics-5.23.3.tgz#ebc613413f7ebad5b0a2631d7a72ca436109b239"
|
||||
integrity sha512-/70Ey+nZm4bRr2DcNrGU251YIn9lDu0g8xeP4jTCyunGRNFZ/d8hQAw9El34pcTpO1QDojJWAi6ywKIrUaks9w==
|
||||
"@algolia/client-analytics@5.23.4":
|
||||
version "5.23.4"
|
||||
resolved "https://registry.yarnpkg.com/@algolia/client-analytics/-/client-analytics-5.23.4.tgz#4a918a775db1c596773a34414f9d4203a50b4291"
|
||||
integrity sha512-4B9gChENsQA9kFmFlb+x3YhBz2Gx3vSsm81FHI1yJ3fn2zlxREHmfrjyqYoMunsU7BybT/o5Nb7ccCbm/vfseA==
|
||||
dependencies:
|
||||
"@algolia/client-common" "5.23.3"
|
||||
"@algolia/requester-browser-xhr" "5.23.3"
|
||||
"@algolia/requester-fetch" "5.23.3"
|
||||
"@algolia/requester-node-http" "5.23.3"
|
||||
"@algolia/client-common" "5.23.4"
|
||||
"@algolia/requester-browser-xhr" "5.23.4"
|
||||
"@algolia/requester-fetch" "5.23.4"
|
||||
"@algolia/requester-node-http" "5.23.4"
|
||||
|
||||
"@algolia/client-common@5.23.3":
|
||||
version "5.23.3"
|
||||
resolved "https://registry.yarnpkg.com/@algolia/client-common/-/client-common-5.23.3.tgz#c5eb2256d6fe1390cb2bf545b52ea78ecae472e7"
|
||||
integrity sha512-fkpbPclIvaiyw3ADKRBCxMZhrNx/8//6DClfWGxeEiTJ0HEEYtHlqE6GjAkEJubz4v1ioCQkhZwMoFfFct2/vQ==
|
||||
"@algolia/client-common@5.23.4":
|
||||
version "5.23.4"
|
||||
resolved "https://registry.yarnpkg.com/@algolia/client-common/-/client-common-5.23.4.tgz#651506d080fd1feda1175c89ffb83fd7a2af20c2"
|
||||
integrity sha512-bsj0lwU2ytiWLtl7sPunr+oLe+0YJql9FozJln5BnIiqfKOaseSDdV42060vUy+D4373f2XBI009K/rm2IXYMA==
|
||||
|
||||
"@algolia/client-insights@5.23.3":
|
||||
version "5.23.3"
|
||||
resolved "https://registry.yarnpkg.com/@algolia/client-insights/-/client-insights-5.23.3.tgz#312add9292887d3e41c0161028b27ee54adef9c3"
|
||||
integrity sha512-TXc5Ve6QOCihWCTWY9N56CZxF1iovzpBWBUhQhy6JSiUfX3MXceV3saV+sXHQ1NVt2NKkyUfEspYHBsTrYzIDg==
|
||||
"@algolia/client-insights@5.23.4":
|
||||
version "5.23.4"
|
||||
resolved "https://registry.yarnpkg.com/@algolia/client-insights/-/client-insights-5.23.4.tgz#a901e2dda6a7a8e6d8879b66e5776d22d1e95a04"
|
||||
integrity sha512-XSCtAYvJ/hnfDHfRVMbBH0dayR+2ofVZy3jf5qyifjguC6rwxDsSdQvXpT0QFVyG+h8UPGtDhMPoUIng4wIcZA==
|
||||
dependencies:
|
||||
"@algolia/client-common" "5.23.3"
|
||||
"@algolia/requester-browser-xhr" "5.23.3"
|
||||
"@algolia/requester-fetch" "5.23.3"
|
||||
"@algolia/requester-node-http" "5.23.3"
|
||||
"@algolia/client-common" "5.23.4"
|
||||
"@algolia/requester-browser-xhr" "5.23.4"
|
||||
"@algolia/requester-fetch" "5.23.4"
|
||||
"@algolia/requester-node-http" "5.23.4"
|
||||
|
||||
"@algolia/client-personalization@5.23.3":
|
||||
version "5.23.3"
|
||||
resolved "https://registry.yarnpkg.com/@algolia/client-personalization/-/client-personalization-5.23.3.tgz#d5be045bd93b9896f9e65d17af8ece5d89507e95"
|
||||
integrity sha512-JlReruxxiw9LB53jF/BmvVV+c0thiWQUHRdgtbVIEusvRaiX1IdpWJSPQExEtBQ7VFg89nP8niCzWtA34ktKSA==
|
||||
"@algolia/client-personalization@5.23.4":
|
||||
version "5.23.4"
|
||||
resolved "https://registry.yarnpkg.com/@algolia/client-personalization/-/client-personalization-5.23.4.tgz#d236f3ef648976307ca119899ad1459d40db93a6"
|
||||
integrity sha512-l/0QvqgRFFOf7BnKSJ3myd1WbDr86ftVaa3PQwlsNh7IpIHmvVcT83Bi5zlORozVGMwaKfyPZo6O48PZELsOeA==
|
||||
dependencies:
|
||||
"@algolia/client-common" "5.23.3"
|
||||
"@algolia/requester-browser-xhr" "5.23.3"
|
||||
"@algolia/requester-fetch" "5.23.3"
|
||||
"@algolia/requester-node-http" "5.23.3"
|
||||
"@algolia/client-common" "5.23.4"
|
||||
"@algolia/requester-browser-xhr" "5.23.4"
|
||||
"@algolia/requester-fetch" "5.23.4"
|
||||
"@algolia/requester-node-http" "5.23.4"
|
||||
|
||||
"@algolia/client-query-suggestions@5.23.3":
|
||||
version "5.23.3"
|
||||
resolved "https://registry.yarnpkg.com/@algolia/client-query-suggestions/-/client-query-suggestions-5.23.3.tgz#d47a6288dc8ea64083f30a2aa71c3044d2887bb0"
|
||||
integrity sha512-GDEExFMXwx0ScE0AZUA4F6ssztdJvGcXUkdWmWyt2hbYz43ukqmlVJqPaYgGmWdjJjvTx+dNF/hcinwWuXbCug==
|
||||
"@algolia/client-query-suggestions@5.23.4":
|
||||
version "5.23.4"
|
||||
resolved "https://registry.yarnpkg.com/@algolia/client-query-suggestions/-/client-query-suggestions-5.23.4.tgz#79579f525510bcc3aacc289040d9c2536e65f945"
|
||||
integrity sha512-TB0htrDgVacVGtPDyENoM6VIeYqR+pMsDovW94dfi2JoaRxfqu/tYmLpvgWcOknP6wLbr8bA+G7t/NiGksNAwQ==
|
||||
dependencies:
|
||||
"@algolia/client-common" "5.23.3"
|
||||
"@algolia/requester-browser-xhr" "5.23.3"
|
||||
"@algolia/requester-fetch" "5.23.3"
|
||||
"@algolia/requester-node-http" "5.23.3"
|
||||
"@algolia/client-common" "5.23.4"
|
||||
"@algolia/requester-browser-xhr" "5.23.4"
|
||||
"@algolia/requester-fetch" "5.23.4"
|
||||
"@algolia/requester-node-http" "5.23.4"
|
||||
|
||||
"@algolia/client-search@5.23.3":
|
||||
version "5.23.3"
|
||||
resolved "https://registry.yarnpkg.com/@algolia/client-search/-/client-search-5.23.3.tgz#e8df14c9aa257c81b8aeaa3cb80cb2af484b9c61"
|
||||
integrity sha512-mwofV6tGo0oHt4BPi+S5eLC3wnhOa4A1OVgPxetTxZuetod+2W4cxKavUW2v/Ma5CABXPLooXX+g9E67umELZw==
|
||||
"@algolia/client-search@5.23.4":
|
||||
version "5.23.4"
|
||||
resolved "https://registry.yarnpkg.com/@algolia/client-search/-/client-search-5.23.4.tgz#7906ab4b704edd1ba2ac39100bf37e0279b4ebdc"
|
||||
integrity sha512-uBGo6KwUP6z+u6HZWRui8UJClS7fgUIAiYd1prUqCbkzDiCngTOzxaJbEvrdkK0hGCQtnPDiuNhC5MhtVNN4Eg==
|
||||
dependencies:
|
||||
"@algolia/client-common" "5.23.3"
|
||||
"@algolia/requester-browser-xhr" "5.23.3"
|
||||
"@algolia/requester-fetch" "5.23.3"
|
||||
"@algolia/requester-node-http" "5.23.3"
|
||||
"@algolia/client-common" "5.23.4"
|
||||
"@algolia/requester-browser-xhr" "5.23.4"
|
||||
"@algolia/requester-fetch" "5.23.4"
|
||||
"@algolia/requester-node-http" "5.23.4"
|
||||
|
||||
"@algolia/ingestion@1.23.3":
|
||||
version "1.23.3"
|
||||
resolved "https://registry.yarnpkg.com/@algolia/ingestion/-/ingestion-1.23.3.tgz#5ed0a38bfae72222b12579255cdca42bba3f62ce"
|
||||
integrity sha512-Zxgmi7Hk4lI52YFphzzJekUqWxYxVjY2GrCpOxV+QiojvUi8Ru+knq6REcwLHFSwpwaDh2Th5pOefMpn4EkQCw==
|
||||
"@algolia/ingestion@1.23.4":
|
||||
version "1.23.4"
|
||||
resolved "https://registry.yarnpkg.com/@algolia/ingestion/-/ingestion-1.23.4.tgz#f542907b13e7bb97dede32101cb86ce7e8482318"
|
||||
integrity sha512-Si6rFuGnSeEUPU9QchYvbknvEIyCRK7nkeaPVQdZpABU7m4V/tsiWdHmjVodtx3h20VZivJdHeQO9XbHxBOcCw==
|
||||
dependencies:
|
||||
"@algolia/client-common" "5.23.3"
|
||||
"@algolia/requester-browser-xhr" "5.23.3"
|
||||
"@algolia/requester-fetch" "5.23.3"
|
||||
"@algolia/requester-node-http" "5.23.3"
|
||||
"@algolia/client-common" "5.23.4"
|
||||
"@algolia/requester-browser-xhr" "5.23.4"
|
||||
"@algolia/requester-fetch" "5.23.4"
|
||||
"@algolia/requester-node-http" "5.23.4"
|
||||
|
||||
"@algolia/monitoring@1.23.3":
|
||||
version "1.23.3"
|
||||
resolved "https://registry.yarnpkg.com/@algolia/monitoring/-/monitoring-1.23.3.tgz#f4748e7ccdf4d84e5044f34e231f9b93fff526b1"
|
||||
integrity sha512-zi/IqvsmFW4E5gMaovAE4KRbXQ+LDYpPGG1nHtfuD5u3SSuQ31fT1vX2zqb6PbPTlgJMEmMk91Mbb7fIKmbQUw==
|
||||
"@algolia/monitoring@1.23.4":
|
||||
version "1.23.4"
|
||||
resolved "https://registry.yarnpkg.com/@algolia/monitoring/-/monitoring-1.23.4.tgz#be169ebdb56f3636c1428f4f20fb33c79d09160a"
|
||||
integrity sha512-EXGoVVTshraqPJgr5cMd1fq7Jm71Ew6MpGCEaxI5PErBpJAmKdtjRIzs6JOGKHRaWLi+jdbJPYc2y8RN4qcx5Q==
|
||||
dependencies:
|
||||
"@algolia/client-common" "5.23.3"
|
||||
"@algolia/requester-browser-xhr" "5.23.3"
|
||||
"@algolia/requester-fetch" "5.23.3"
|
||||
"@algolia/requester-node-http" "5.23.3"
|
||||
"@algolia/client-common" "5.23.4"
|
||||
"@algolia/requester-browser-xhr" "5.23.4"
|
||||
"@algolia/requester-fetch" "5.23.4"
|
||||
"@algolia/requester-node-http" "5.23.4"
|
||||
|
||||
"@algolia/recommend@5.23.3":
|
||||
version "5.23.3"
|
||||
resolved "https://registry.yarnpkg.com/@algolia/recommend/-/recommend-5.23.3.tgz#76b0d0df2e13a722512b75844e5dd954a370f182"
|
||||
integrity sha512-C9TwbT1zGwULLXGSUSB+G7o/30djacPmQcsTHepvT47PVfPr2ISK/5QVtUnjMU84LEP8uNjuPUeM4ZeWVJ2iuQ==
|
||||
"@algolia/recommend@5.23.4":
|
||||
version "5.23.4"
|
||||
resolved "https://registry.yarnpkg.com/@algolia/recommend/-/recommend-5.23.4.tgz#218ca0457d68045632953648b622047e0c57a338"
|
||||
integrity sha512-1t6glwKVCkjvBNlng2itTf8fwaLSqkL4JaMENgR3WTGR8mmW2akocUy/ZYSQcG4TcR7qu4zW2UMGAwLoWoflgQ==
|
||||
dependencies:
|
||||
"@algolia/client-common" "5.23.3"
|
||||
"@algolia/requester-browser-xhr" "5.23.3"
|
||||
"@algolia/requester-fetch" "5.23.3"
|
||||
"@algolia/requester-node-http" "5.23.3"
|
||||
"@algolia/client-common" "5.23.4"
|
||||
"@algolia/requester-browser-xhr" "5.23.4"
|
||||
"@algolia/requester-fetch" "5.23.4"
|
||||
"@algolia/requester-node-http" "5.23.4"
|
||||
|
||||
"@algolia/requester-browser-xhr@5.23.3":
|
||||
version "5.23.3"
|
||||
resolved "https://registry.yarnpkg.com/@algolia/requester-browser-xhr/-/requester-browser-xhr-5.23.3.tgz#a66b17495be4a4d3fff85efc9d2ec3589481b7d8"
|
||||
integrity sha512-/7oYeUhYzY0lls7WtkAURM6wy21/Wwmq9GdujW1MpoYVC0ATXXxwCiAfOpYL9xdWxLV0R3wjyD+yZEni+nboKg==
|
||||
"@algolia/requester-browser-xhr@5.23.4":
|
||||
version "5.23.4"
|
||||
resolved "https://registry.yarnpkg.com/@algolia/requester-browser-xhr/-/requester-browser-xhr-5.23.4.tgz#ee8c88094e904511024e3ba7749b85a85f8d31bd"
|
||||
integrity sha512-UUuizcgc5+VSY8hqzDFVdJ3Wcto03lpbFRGPgW12pHTlUQHUTADtIpIhkLLOZRCjXmCVhtr97Z+eR6LcRYXa3Q==
|
||||
dependencies:
|
||||
"@algolia/client-common" "5.23.3"
|
||||
"@algolia/client-common" "5.23.4"
|
||||
|
||||
"@algolia/requester-fetch@5.23.3":
|
||||
version "5.23.3"
|
||||
resolved "https://registry.yarnpkg.com/@algolia/requester-fetch/-/requester-fetch-5.23.3.tgz#85bb4a0894d4956122699cc541935a31d9de4be0"
|
||||
integrity sha512-r/4fKz4t+bSU1KdjRq+swdNvuGfJ0spV8aFTHPtcsF+1ZaN/VqmdXrTe5NkaZLSztFeMqKwZlJIVvE7VuGlFtw==
|
||||
"@algolia/requester-fetch@5.23.4":
|
||||
version "5.23.4"
|
||||
resolved "https://registry.yarnpkg.com/@algolia/requester-fetch/-/requester-fetch-5.23.4.tgz#138dab9f52771cdb90c64dabb01d1fec3614446b"
|
||||
integrity sha512-UhDg6elsek6NnV5z4VG1qMwR6vbp+rTMBEnl/v4hUyXQazU+CNdYkl++cpdmLwGI/7nXc28xtZiL90Es3I7viQ==
|
||||
dependencies:
|
||||
"@algolia/client-common" "5.23.3"
|
||||
"@algolia/client-common" "5.23.4"
|
||||
|
||||
"@algolia/requester-node-http@5.23.3":
|
||||
version "5.23.3"
|
||||
resolved "https://registry.yarnpkg.com/@algolia/requester-node-http/-/requester-node-http-5.23.3.tgz#67f9034a62a571f3fa9e840ed00f3e2cf9dd679b"
|
||||
integrity sha512-UZiTNmUBQFPl3tUKuXaDd8BxEC0t0ny86wwW6XgwfM9IQf4PrzuMpvuOGIJMcCGlrNolZDEI0mcbz/tqRdKW7A==
|
||||
"@algolia/requester-node-http@5.23.4":
|
||||
version "5.23.4"
|
||||
resolved "https://registry.yarnpkg.com/@algolia/requester-node-http/-/requester-node-http-5.23.4.tgz#8cc9439ef2f21f04cbea7ddeef712aa2b3d18f62"
|
||||
integrity sha512-jXGzGBRUS0oywQwnaCA6mMDJO7LoC3dYSLsyNfIqxDR4SNGLhtg3je0Y31lc24OA4nYyKAYgVLtjfrpcpsWShg==
|
||||
dependencies:
|
||||
"@algolia/client-common" "5.23.3"
|
||||
"@algolia/client-common" "5.23.4"
|
||||
|
||||
"@ampproject/remapping@^2.2.0":
|
||||
version "2.3.0"
|
||||
@@ -3056,6 +3056,14 @@
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.34.9.tgz#1973871850856ae72bc678aeb066ab952330e923"
|
||||
integrity sha512-AyleYRPU7+rgkMWbEh71fQlrzRfeP6SyMnRf9XX4fCdDPAJumdSBqYEcWPMzVQ4ScAl7E4oFfK0GUVn77xSwbw==
|
||||
|
||||
"@selderee/plugin-htmlparser2@^0.11.0":
|
||||
version "0.11.0"
|
||||
resolved "https://registry.yarnpkg.com/@selderee/plugin-htmlparser2/-/plugin-htmlparser2-0.11.0.tgz#d5b5e29a7ba6d3958a1972c7be16f4b2c188c517"
|
||||
integrity sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==
|
||||
dependencies:
|
||||
domhandler "^5.0.3"
|
||||
selderee "^0.11.0"
|
||||
|
||||
"@sideway/address@^4.1.5":
|
||||
version "4.1.5"
|
||||
resolved "https://registry.yarnpkg.com/@sideway/address/-/address-4.1.5.tgz#4bc149a0076623ced99ca8208ba780d65a99b9d5"
|
||||
@@ -3252,6 +3260,11 @@
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/html-to-text@^9.0.4":
|
||||
version "9.0.4"
|
||||
resolved "https://registry.yarnpkg.com/@types/html-to-text/-/html-to-text-9.0.4.tgz#4a83dd8ae8bfa91457d0b1ffc26f4d0537eff58c"
|
||||
integrity sha512-pUY3cKH/Nm2yYrEmDlPR1mR7yszjGx4DrwPjQ702C4/D5CwHuZTgZdIdwPkRbcuhs7BAh2L5rg3CL5cbRiGTCQ==
|
||||
|
||||
"@types/invariant@^2.2.35":
|
||||
version "2.2.35"
|
||||
resolved "https://registry.yarnpkg.com/@types/invariant/-/invariant-2.2.35.tgz#cd3ebf581a6557452735688d8daba6cf0bd5a3be"
|
||||
@@ -3774,23 +3787,23 @@ ajv@^6.12.4:
|
||||
uri-js "^4.2.2"
|
||||
|
||||
algoliasearch@^5.23.3:
|
||||
version "5.23.3"
|
||||
resolved "https://registry.yarnpkg.com/algoliasearch/-/algoliasearch-5.23.3.tgz#ac2a0541efac4dcd63be1ed98bfbd0583095dec2"
|
||||
integrity sha512-0JlUaY/hl3LrKvbidI5FysEi2ggAlcTHM8AHV2UsrJUXnNo8/lWBfhzc1b7o8bK3YZNiU26JtLyT9exoj5VBgA==
|
||||
version "5.23.4"
|
||||
resolved "https://registry.yarnpkg.com/algoliasearch/-/algoliasearch-5.23.4.tgz#2f8c6e6f540b0a73effa69cb05310f7843012e2d"
|
||||
integrity sha512-QzAKFHl3fm53s44VHrTdEo0TkpL3XVUYQpnZy1r6/EHvMAyIg+O4hwprzlsNmcCHTNyVcF2S13DAUn7XhkC6qg==
|
||||
dependencies:
|
||||
"@algolia/client-abtesting" "5.23.3"
|
||||
"@algolia/client-analytics" "5.23.3"
|
||||
"@algolia/client-common" "5.23.3"
|
||||
"@algolia/client-insights" "5.23.3"
|
||||
"@algolia/client-personalization" "5.23.3"
|
||||
"@algolia/client-query-suggestions" "5.23.3"
|
||||
"@algolia/client-search" "5.23.3"
|
||||
"@algolia/ingestion" "1.23.3"
|
||||
"@algolia/monitoring" "1.23.3"
|
||||
"@algolia/recommend" "5.23.3"
|
||||
"@algolia/requester-browser-xhr" "5.23.3"
|
||||
"@algolia/requester-fetch" "5.23.3"
|
||||
"@algolia/requester-node-http" "5.23.3"
|
||||
"@algolia/client-abtesting" "5.23.4"
|
||||
"@algolia/client-analytics" "5.23.4"
|
||||
"@algolia/client-common" "5.23.4"
|
||||
"@algolia/client-insights" "5.23.4"
|
||||
"@algolia/client-personalization" "5.23.4"
|
||||
"@algolia/client-query-suggestions" "5.23.4"
|
||||
"@algolia/client-search" "5.23.4"
|
||||
"@algolia/ingestion" "1.23.4"
|
||||
"@algolia/monitoring" "1.23.4"
|
||||
"@algolia/recommend" "5.23.4"
|
||||
"@algolia/requester-browser-xhr" "5.23.4"
|
||||
"@algolia/requester-fetch" "5.23.4"
|
||||
"@algolia/requester-node-http" "5.23.4"
|
||||
|
||||
ansi-colors@^4.1.3:
|
||||
version "4.1.3"
|
||||
@@ -4161,6 +4174,11 @@ body-parser@^2.2.0:
|
||||
raw-body "^3.0.0"
|
||||
type-is "^2.0.0"
|
||||
|
||||
boolbase@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e"
|
||||
integrity sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==
|
||||
|
||||
brace-expansion@^1.1.7:
|
||||
version "1.1.11"
|
||||
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
|
||||
@@ -4326,15 +4344,10 @@ camelcase@^6.0.0, camelcase@^6.2.0:
|
||||
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a"
|
||||
integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==
|
||||
|
||||
caniuse-lite@^1.0.30001489:
|
||||
version "1.0.30001581"
|
||||
resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001581.tgz"
|
||||
integrity sha512-whlTkwhqV2tUmP3oYhtNfaWGYHDdS3JYFQBKXxcUR9qqPWsRhFHhoISO2Xnl/g0xyKzht9mI1LZpiNWfMzHixQ==
|
||||
|
||||
caniuse-lite@^1.0.30001688:
|
||||
version "1.0.30001690"
|
||||
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001690.tgz#f2d15e3aaf8e18f76b2b8c1481abde063b8104c8"
|
||||
integrity sha512-5ExiE3qQN6oF8Clf8ifIDcMRCRE/dMGcETG/XGMD8/XiXm6HXQgQTh1yZYLXXpSOsEUlJm1Xr7kGULZTuGtP/w==
|
||||
caniuse-lite@^1.0.30001489, caniuse-lite@^1.0.30001688:
|
||||
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, chalk@^2.0.0, chalk@^2.4.2:
|
||||
version "2.4.2"
|
||||
@@ -4363,6 +4376,35 @@ char-regex@^1.0.2:
|
||||
resolved "https://registry.yarnpkg.com/char-regex/-/char-regex-1.0.2.tgz#d744358226217f981ed58f479b1d6bcc29545dcf"
|
||||
integrity sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==
|
||||
|
||||
cheerio-select@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/cheerio-select/-/cheerio-select-2.1.0.tgz#4d8673286b8126ca2a8e42740d5e3c4884ae21b4"
|
||||
integrity sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==
|
||||
dependencies:
|
||||
boolbase "^1.0.0"
|
||||
css-select "^5.1.0"
|
||||
css-what "^6.1.0"
|
||||
domelementtype "^2.3.0"
|
||||
domhandler "^5.0.3"
|
||||
domutils "^3.0.1"
|
||||
|
||||
cheerio@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-1.0.0.tgz#1ede4895a82f26e8af71009f961a9b8cb60d6a81"
|
||||
integrity sha512-quS9HgjQpdaXOvsZz82Oz7uxtXiy6UIsIQcpBj7HRw2M63Skasm9qlDocAM7jNuaxdhpPU7c4kJN+gA5MCu4ww==
|
||||
dependencies:
|
||||
cheerio-select "^2.1.0"
|
||||
dom-serializer "^2.0.0"
|
||||
domhandler "^5.0.3"
|
||||
domutils "^3.1.0"
|
||||
encoding-sniffer "^0.2.0"
|
||||
htmlparser2 "^9.1.0"
|
||||
parse5 "^7.1.2"
|
||||
parse5-htmlparser2-tree-adapter "^7.0.0"
|
||||
parse5-parser-stream "^7.1.2"
|
||||
undici "^6.19.5"
|
||||
whatwg-mimetype "^4.0.0"
|
||||
|
||||
chokidar@^3.5.3:
|
||||
version "3.6.0"
|
||||
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b"
|
||||
@@ -4644,6 +4686,22 @@ cross-spawn@^7.0.6:
|
||||
shebang-command "^2.0.0"
|
||||
which "^2.0.1"
|
||||
|
||||
css-select@^5.1.0:
|
||||
version "5.1.0"
|
||||
resolved "https://registry.yarnpkg.com/css-select/-/css-select-5.1.0.tgz#b8ebd6554c3637ccc76688804ad3f6a6fdaea8a6"
|
||||
integrity sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==
|
||||
dependencies:
|
||||
boolbase "^1.0.0"
|
||||
css-what "^6.1.0"
|
||||
domhandler "^5.0.2"
|
||||
domutils "^3.0.1"
|
||||
nth-check "^2.0.1"
|
||||
|
||||
css-what@^6.1.0:
|
||||
version "6.1.0"
|
||||
resolved "https://registry.yarnpkg.com/css-what/-/css-what-6.1.0.tgz#fb5effcf76f1ddea2c81bdfaa4de44e79bac70f4"
|
||||
integrity sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==
|
||||
|
||||
cssom@^0.5.0:
|
||||
version "0.5.0"
|
||||
resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.5.0.tgz#d254fa92cd8b6fbd83811b9fbaed34663cc17c36"
|
||||
@@ -4747,6 +4805,11 @@ deepmerge@^4.2.2:
|
||||
resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.2.2.tgz#44d2ea3679b8f4d4ffba33f03d865fc1e7bf4955"
|
||||
integrity sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==
|
||||
|
||||
deepmerge@^4.3.1:
|
||||
version "4.3.1"
|
||||
resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.3.1.tgz#44b5f2147cd3b00d4b56137685966f26fd25dd4a"
|
||||
integrity sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==
|
||||
|
||||
defaults@^1.0.3:
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/defaults/-/defaults-1.0.4.tgz#b0b02062c1e2aa62ff5d9528f0f98baa90978d7a"
|
||||
@@ -4833,6 +4896,20 @@ dom-accessibility-api@^0.5.9:
|
||||
resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.5.14.tgz#56082f71b1dc7aac69d83c4285eef39c15d93f56"
|
||||
integrity sha512-NMt+m9zFMPZe0JcY9gN224Qvk6qLIdqex29clBvc/y75ZBX9YA9wNK3frsYvu2DI1xcCIwxwnX+TlsJ2DSOADg==
|
||||
|
||||
dom-serializer@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-2.0.0.tgz#e41b802e1eedf9f6cae183ce5e622d789d7d8e53"
|
||||
integrity sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==
|
||||
dependencies:
|
||||
domelementtype "^2.3.0"
|
||||
domhandler "^5.0.2"
|
||||
entities "^4.2.0"
|
||||
|
||||
domelementtype@^2.3.0:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.3.0.tgz#5c45e8e869952626331d7aab326d01daf65d589d"
|
||||
integrity sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==
|
||||
|
||||
domexception@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/domexception/-/domexception-4.0.0.tgz#4ad1be56ccadc86fc76d033353999a8037d03673"
|
||||
@@ -4840,6 +4917,22 @@ domexception@^4.0.0:
|
||||
dependencies:
|
||||
webidl-conversions "^7.0.0"
|
||||
|
||||
domhandler@^5.0.2, domhandler@^5.0.3:
|
||||
version "5.0.3"
|
||||
resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-5.0.3.tgz#cc385f7f751f1d1fc650c21374804254538c7d31"
|
||||
integrity sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==
|
||||
dependencies:
|
||||
domelementtype "^2.3.0"
|
||||
|
||||
domutils@^3.0.1, domutils@^3.1.0:
|
||||
version "3.2.2"
|
||||
resolved "https://registry.yarnpkg.com/domutils/-/domutils-3.2.2.tgz#edbfe2b668b0c1d97c24baf0f1062b132221bc78"
|
||||
integrity sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==
|
||||
dependencies:
|
||||
dom-serializer "^2.0.0"
|
||||
domelementtype "^2.3.0"
|
||||
domhandler "^5.0.3"
|
||||
|
||||
dreamopt@~0.6.0:
|
||||
version "0.6.0"
|
||||
resolved "https://registry.yarnpkg.com/dreamopt/-/dreamopt-0.6.0.tgz#d813ccdac8d39d8ad526775514a13dda664d6b4b"
|
||||
@@ -4911,6 +5004,14 @@ encodeurl@^2.0.0:
|
||||
resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-2.0.0.tgz#7b8ea898077d7e409d3ac45474ea38eaf0857a58"
|
||||
integrity sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==
|
||||
|
||||
encoding-sniffer@^0.2.0:
|
||||
version "0.2.0"
|
||||
resolved "https://registry.yarnpkg.com/encoding-sniffer/-/encoding-sniffer-0.2.0.tgz#799569d66d443babe82af18c9f403498365ef1d5"
|
||||
integrity sha512-ju7Wq1kg04I3HtiYIOrUrdfdDvkyO9s5XM8QAj/bN61Yo/Vb4vgJxy5vi4Yxk01gWHbrofpPtpxM8bKger9jhg==
|
||||
dependencies:
|
||||
iconv-lite "^0.6.3"
|
||||
whatwg-encoding "^3.1.1"
|
||||
|
||||
enhanced-resolve@^5.15.0:
|
||||
version "5.18.0"
|
||||
resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.18.0.tgz#91eb1db193896b9801251eeff1c6980278b1e404"
|
||||
@@ -4919,6 +5020,11 @@ enhanced-resolve@^5.15.0:
|
||||
graceful-fs "^4.2.4"
|
||||
tapable "^2.2.0"
|
||||
|
||||
entities@^4.2.0, entities@^4.5.0:
|
||||
version "4.5.0"
|
||||
resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48"
|
||||
integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==
|
||||
|
||||
entities@^4.4.0:
|
||||
version "4.4.0"
|
||||
resolved "https://registry.yarnpkg.com/entities/-/entities-4.4.0.tgz#97bdaba170339446495e653cfd2db78962900174"
|
||||
@@ -5729,7 +5835,7 @@ glob@^10.3.10:
|
||||
minipass "^5.0.0 || ^6.0.2 || ^7.0.0"
|
||||
path-scurry "^1.10.1"
|
||||
|
||||
glob@^10.3.7, glob@^10.4.5:
|
||||
glob@^10.4.5:
|
||||
version "10.4.5"
|
||||
resolved "https://registry.yarnpkg.com/glob/-/glob-10.4.5.tgz#f4d9f0b90ffdbab09c9d77f5f29b4262517b0956"
|
||||
integrity sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==
|
||||
@@ -5948,6 +6054,37 @@ html-escaper@^2.0.0:
|
||||
resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453"
|
||||
integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==
|
||||
|
||||
html-to-text@^9.0.5:
|
||||
version "9.0.5"
|
||||
resolved "https://registry.yarnpkg.com/html-to-text/-/html-to-text-9.0.5.tgz#6149a0f618ae7a0db8085dca9bbf96d32bb8368d"
|
||||
integrity sha512-qY60FjREgVZL03vJU6IfMV4GDjGBIoOyvuFdpBDIX9yTlDw0TjxVBQp+P8NvpdIXNJvfWBTNul7fsAQJq2FNpg==
|
||||
dependencies:
|
||||
"@selderee/plugin-htmlparser2" "^0.11.0"
|
||||
deepmerge "^4.3.1"
|
||||
dom-serializer "^2.0.0"
|
||||
htmlparser2 "^8.0.2"
|
||||
selderee "^0.11.0"
|
||||
|
||||
htmlparser2@^8.0.2:
|
||||
version "8.0.2"
|
||||
resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-8.0.2.tgz#f002151705b383e62433b5cf466f5b716edaec21"
|
||||
integrity sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==
|
||||
dependencies:
|
||||
domelementtype "^2.3.0"
|
||||
domhandler "^5.0.3"
|
||||
domutils "^3.0.1"
|
||||
entities "^4.4.0"
|
||||
|
||||
htmlparser2@^9.1.0:
|
||||
version "9.1.0"
|
||||
resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-9.1.0.tgz#cdb498d8a75a51f739b61d3f718136c369bc8c23"
|
||||
integrity sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==
|
||||
dependencies:
|
||||
domelementtype "^2.3.0"
|
||||
domhandler "^5.0.3"
|
||||
domutils "^3.1.0"
|
||||
entities "^4.5.0"
|
||||
|
||||
http-errors@2.0.0, http-errors@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3"
|
||||
@@ -7574,6 +7711,11 @@ kuler@^2.0.0:
|
||||
resolved "https://registry.yarnpkg.com/kuler/-/kuler-2.0.0.tgz#e2c570a3800388fb44407e851531c1d670b061b3"
|
||||
integrity sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==
|
||||
|
||||
leac@^0.6.0:
|
||||
version "0.6.0"
|
||||
resolved "https://registry.yarnpkg.com/leac/-/leac-0.6.0.tgz#dcf136e382e666bd2475f44a1096061b70dc0912"
|
||||
integrity sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg==
|
||||
|
||||
leven@2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/leven/-/leven-2.1.0.tgz#c2e7a9f772094dee9d34202ae8acce4687875580"
|
||||
@@ -8068,6 +8210,13 @@ npm-which@^3.0.1:
|
||||
npm-path "^2.0.2"
|
||||
which "^1.2.10"
|
||||
|
||||
nth-check@^2.0.1:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.1.1.tgz#c9eab428effce36cd6b92c924bdb000ef1f1ed1d"
|
||||
integrity sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==
|
||||
dependencies:
|
||||
boolbase "^1.0.0"
|
||||
|
||||
nullthrows@^1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/nullthrows/-/nullthrows-1.1.1.tgz#7818258843856ae971eae4208ad7d7eb19a431b1"
|
||||
@@ -8262,6 +8411,21 @@ parse-passwd@^1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/parse-passwd/-/parse-passwd-1.0.0.tgz#6d5b934a456993b23d37f40a382d6f1666a8e5c6"
|
||||
integrity sha512-1Y1A//QUXEZK7YKz+rD9WydcE1+EuPr6ZBgKecAB8tmoW6UFv0NREVJe1p+jRxtThkcbbKkfwIbWJe/IeE6m2Q==
|
||||
|
||||
parse5-htmlparser2-tree-adapter@^7.0.0:
|
||||
version "7.1.0"
|
||||
resolved "https://registry.yarnpkg.com/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz#b5a806548ed893a43e24ccb42fbb78069311e81b"
|
||||
integrity sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==
|
||||
dependencies:
|
||||
domhandler "^5.0.3"
|
||||
parse5 "^7.0.0"
|
||||
|
||||
parse5-parser-stream@^7.1.2:
|
||||
version "7.1.2"
|
||||
resolved "https://registry.yarnpkg.com/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz#d7c20eadc37968d272e2c02660fff92dd27e60e1"
|
||||
integrity sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==
|
||||
dependencies:
|
||||
parse5 "^7.0.0"
|
||||
|
||||
parse5@^7.0.0:
|
||||
version "7.1.1"
|
||||
resolved "https://registry.yarnpkg.com/parse5/-/parse5-7.1.1.tgz#4649f940ccfb95d8754f37f73078ea20afe0c746"
|
||||
@@ -8276,6 +8440,14 @@ parse5@^7.1.2:
|
||||
dependencies:
|
||||
entities "^4.4.0"
|
||||
|
||||
parseley@^0.12.0:
|
||||
version "0.12.1"
|
||||
resolved "https://registry.yarnpkg.com/parseley/-/parseley-0.12.1.tgz#4afd561d50215ebe259e3e7a853e62f600683aef"
|
||||
integrity sha512-e6qHKe3a9HWr0oMRVDTRhKce+bRO8VGQR3NyVwcjwrbhMmFCX9KszEV35+rn4AdilFAq9VPxP/Fe1wC9Qjd2lw==
|
||||
dependencies:
|
||||
leac "^0.6.0"
|
||||
peberminta "^0.9.0"
|
||||
|
||||
parseurl@^1.3.3:
|
||||
version "1.3.3"
|
||||
resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4"
|
||||
@@ -8340,6 +8512,11 @@ path-type@^4.0.0:
|
||||
resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b"
|
||||
integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==
|
||||
|
||||
peberminta@^0.9.0:
|
||||
version "0.9.0"
|
||||
resolved "https://registry.yarnpkg.com/peberminta/-/peberminta-0.9.0.tgz#8ec9bc0eb84b7d368126e71ce9033501dca2a352"
|
||||
integrity sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ==
|
||||
|
||||
picocolors@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c"
|
||||
@@ -8778,12 +8955,20 @@ reusify@^1.0.4:
|
||||
resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76"
|
||||
integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==
|
||||
|
||||
rimraf@5.0.10, rimraf@6.0.1, rimraf@^3.0.0, rimraf@^3.0.2, rimraf@^6.0.1:
|
||||
version "5.0.10"
|
||||
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-5.0.10.tgz#23b9843d3dc92db71f96e1a2ce92e39fd2a8221c"
|
||||
integrity sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==
|
||||
rimraf@6.0.1, rimraf@^6.0.1:
|
||||
version "6.0.1"
|
||||
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-6.0.1.tgz#ffb8ad8844dd60332ab15f52bc104bc3ed71ea4e"
|
||||
integrity sha512-9dkvaxAsk/xNXSJzMgFqqMCuFgt2+KsOFek3TMLfo8NCPfWpBmqwyNn5Y+NX56QUYfCtsyhF3ayiboEoUmJk/A==
|
||||
dependencies:
|
||||
glob "^10.3.7"
|
||||
glob "^11.0.0"
|
||||
package-json-from-dist "^1.0.0"
|
||||
|
||||
rimraf@^3.0.0, rimraf@^3.0.2:
|
||||
version "3.0.2"
|
||||
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a"
|
||||
integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==
|
||||
dependencies:
|
||||
glob "^7.1.3"
|
||||
|
||||
rollup@^4.34.8:
|
||||
version "4.34.9"
|
||||
@@ -8875,6 +9060,13 @@ scheduler@0.0.0-experimental-4beb1fd8-20241118:
|
||||
resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.0.0-experimental-4beb1fd8-20241118.tgz#3143baa23dfb4daed6a9d0bfd44a8050a0cdab93"
|
||||
integrity sha512-b7GQktevD5BPcS+R5qY5se5oX4b8AHQyebWswGZBdLCmElIwR3Q+RO5EgsLOA4t5D3/TDjLm58CQG16uEB5rMA==
|
||||
|
||||
selderee@^0.11.0:
|
||||
version "0.11.0"
|
||||
resolved "https://registry.yarnpkg.com/selderee/-/selderee-0.11.0.tgz#6af0c7983e073ad3e35787ffe20cefd9daf0ec8a"
|
||||
integrity sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA==
|
||||
dependencies:
|
||||
parseley "^0.12.0"
|
||||
|
||||
semver@7.x, semver@^7.3.5:
|
||||
version "7.3.7"
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.7.tgz#12c5b649afdbf9049707796e22a4028814ce523f"
|
||||
@@ -9552,6 +9744,11 @@ undici-types@~6.19.2:
|
||||
resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.19.8.tgz#35111c9d1437ab83a7cdc0abae2f26d88eda0a02"
|
||||
integrity sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==
|
||||
|
||||
undici@^6.19.5:
|
||||
version "6.21.2"
|
||||
resolved "https://registry.yarnpkg.com/undici/-/undici-6.21.2.tgz#49c5884e8f9039c65a89ee9018ef3c8e2f1f4928"
|
||||
integrity sha512-uROZWze0R0itiAKVPsYhFov9LxrPMHLMEQFszeI2gCN6bnIIZ8twzBCJcN2LJrBBLfrP0t1FW0g+JmKVl8Vk1g==
|
||||
|
||||
unicode-canonical-property-names-ecmascript@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz#301acdc525631670d39f6146e0e77ff6bbdebddc"
|
||||
@@ -9717,11 +9914,23 @@ whatwg-encoding@^2.0.0:
|
||||
dependencies:
|
||||
iconv-lite "0.6.3"
|
||||
|
||||
whatwg-encoding@^3.1.1:
|
||||
version "3.1.1"
|
||||
resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz#d0f4ef769905d426e1688f3e34381a99b60b76e5"
|
||||
integrity sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==
|
||||
dependencies:
|
||||
iconv-lite "0.6.3"
|
||||
|
||||
whatwg-mimetype@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz#5fa1a7623867ff1af6ca3dc72ad6b8a4208beba7"
|
||||
integrity sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==
|
||||
|
||||
whatwg-mimetype@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz#bc1bf94a985dc50388d54a9258ac405c3ca2fc0a"
|
||||
integrity sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==
|
||||
|
||||
whatwg-url@^11.0.0:
|
||||
version "11.0.0"
|
||||
resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-11.0.0.tgz#0a849eebb5faf2119b901bb76fd795c2848d4018"
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -37,6 +37,7 @@ export default class Chrome extends Component {
|
||||
</div>
|
||||
</Theme.Provider>
|
||||
</Suspense>
|
||||
<p>This should appear in the first paint.</p>
|
||||
<script
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `assetManifest = ${JSON.stringify(assets)};`,
|
||||
|
||||
@@ -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,
|
||||
"preview_long": Error,
|
||||
},
|
||||
},
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
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;
|
||||
@@ -95,7 +95,7 @@ export default function HoveredFiberInfo({fiberData}: Props): React.Node {
|
||||
<div className={styles.Content}>
|
||||
{renderDurationInfo || <div>Did not client render.</div>}
|
||||
|
||||
<WhatChanged fiberID={id} />
|
||||
<WhatChanged fiberID={id} displayMode="compact" />
|
||||
</div>
|
||||
</div>
|
||||
</Fragment>
|
||||
|
||||
@@ -14,30 +14,17 @@ import {ProfilerContext} from './ProfilerContext';
|
||||
import {StoreContext} from '../context';
|
||||
|
||||
import styles from './WhatChanged.css';
|
||||
|
||||
function hookIndicesToString(indices: Array<number>): string {
|
||||
// This is debatable but I think 1-based might ake for a nicer UX.
|
||||
const numbers = indices.map(value => value + 1);
|
||||
|
||||
switch (numbers.length) {
|
||||
case 0:
|
||||
return 'No hooks changed';
|
||||
case 1:
|
||||
return `Hook ${numbers[0]} changed`;
|
||||
case 2:
|
||||
return `Hooks ${numbers[0]} and ${numbers[1]} changed`;
|
||||
default:
|
||||
return `Hooks ${numbers.slice(0, numbers.length - 1).join(', ')} and ${
|
||||
numbers[numbers.length - 1]
|
||||
} changed`;
|
||||
}
|
||||
}
|
||||
import HookChangeSummary from './HookChangeSummary';
|
||||
|
||||
type Props = {
|
||||
fiberID: number,
|
||||
displayMode?: 'detailed' | 'compact',
|
||||
};
|
||||
|
||||
export default function WhatChanged({fiberID}: Props): React.Node {
|
||||
export default function WhatChanged({
|
||||
fiberID,
|
||||
displayMode = 'detailed',
|
||||
}: Props): React.Node {
|
||||
const {profilerStore} = useContext(StoreContext);
|
||||
const {rootID, selectedCommitIndex} = useContext(ProfilerContext);
|
||||
|
||||
@@ -106,7 +93,12 @@ export default function WhatChanged({fiberID}: Props): React.Node {
|
||||
if (Array.isArray(hooks)) {
|
||||
changes.push(
|
||||
<div key="hooks" className={styles.Item}>
|
||||
• {hookIndicesToString(hooks)}
|
||||
<HookChangeSummary
|
||||
hooks={hooks}
|
||||
fiberID={fiberID}
|
||||
state={state}
|
||||
displayMode={displayMode}
|
||||
/>
|
||||
</div>,
|
||||
);
|
||||
} else {
|
||||
|
||||
@@ -20,10 +20,17 @@
|
||||
background: var(--color-button-background);
|
||||
color: var(--color-button);
|
||||
}
|
||||
|
||||
.ToggleOff:hover {
|
||||
color: var(--color-button-hover);
|
||||
}
|
||||
|
||||
.ToggleOn:hover,
|
||||
.ToggleOff:hover {
|
||||
background-color: var(--color-button-background-hover);
|
||||
}
|
||||
|
||||
|
||||
.ToggleOn,
|
||||
.ToggleOn:active {
|
||||
color: var(--color-button-active);
|
||||
|
||||
@@ -72,6 +72,14 @@ export function hasAlreadyLoadedHookNames(element: Element): boolean {
|
||||
return record != null && record.status === Resolved;
|
||||
}
|
||||
|
||||
export function getAlreadyLoadedHookNames(element: Element): HookNames | null {
|
||||
const record = map.get(element);
|
||||
if (record != null && record.status === Resolved) {
|
||||
return record.value;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function loadHookNames(
|
||||
element: Element,
|
||||
hooksTree: HooksTree,
|
||||
|
||||
72
packages/react-devtools-shared/src/hydration.js
vendored
72
packages/react-devtools-shared/src/hydration.js
vendored
@@ -43,7 +43,7 @@ export type Dehydrated = {
|
||||
type: string,
|
||||
};
|
||||
|
||||
// Typed arrays and other complex iteratable objects (e.g. Map, Set, ImmutableJS) need special handling.
|
||||
// Typed arrays, other complex iteratable objects (e.g. Map, Set, ImmutableJS) or Promises need special handling.
|
||||
// These objects can't be serialized without losing type information,
|
||||
// so a "Unserializable" type wrapper is used (with meta-data keys) to send nested values-
|
||||
// while preserving the original type and name.
|
||||
@@ -303,6 +303,76 @@ export function dehydrate(
|
||||
type,
|
||||
};
|
||||
|
||||
case 'thenable':
|
||||
isPathAllowedCheck = isPathAllowed(path);
|
||||
|
||||
if (level >= LEVEL_THRESHOLD && !isPathAllowedCheck) {
|
||||
return {
|
||||
inspectable:
|
||||
data.status === 'fulfilled' || data.status === 'rejected',
|
||||
preview_short: formatDataForPreview(data, false),
|
||||
preview_long: formatDataForPreview(data, true),
|
||||
name: data.toString(),
|
||||
type,
|
||||
};
|
||||
}
|
||||
|
||||
switch (data.status) {
|
||||
case 'fulfilled': {
|
||||
const unserializableValue: Unserializable = {
|
||||
unserializable: true,
|
||||
type: type,
|
||||
preview_short: formatDataForPreview(data, false),
|
||||
preview_long: formatDataForPreview(data, true),
|
||||
name: 'fulfilled Thenable',
|
||||
};
|
||||
|
||||
unserializableValue.value = dehydrate(
|
||||
data.value,
|
||||
cleaned,
|
||||
unserializable,
|
||||
path.concat(['value']),
|
||||
isPathAllowed,
|
||||
isPathAllowedCheck ? 1 : level + 1,
|
||||
);
|
||||
|
||||
unserializable.push(path);
|
||||
|
||||
return unserializableValue;
|
||||
}
|
||||
case 'rejected': {
|
||||
const unserializableValue: Unserializable = {
|
||||
unserializable: true,
|
||||
type: type,
|
||||
preview_short: formatDataForPreview(data, false),
|
||||
preview_long: formatDataForPreview(data, true),
|
||||
name: 'rejected Thenable',
|
||||
};
|
||||
|
||||
unserializableValue.reason = dehydrate(
|
||||
data.reason,
|
||||
cleaned,
|
||||
unserializable,
|
||||
path.concat(['reason']),
|
||||
isPathAllowed,
|
||||
isPathAllowedCheck ? 1 : level + 1,
|
||||
);
|
||||
|
||||
unserializable.push(path);
|
||||
|
||||
return unserializableValue;
|
||||
}
|
||||
default:
|
||||
cleaned.push(path);
|
||||
return {
|
||||
inspectable: false,
|
||||
preview_short: formatDataForPreview(data, false),
|
||||
preview_long: formatDataForPreview(data, true),
|
||||
name: data.toString(),
|
||||
type,
|
||||
};
|
||||
}
|
||||
|
||||
case 'object':
|
||||
isPathAllowedCheck = isPathAllowed(path);
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import {normalizeUrl} from 'react-devtools-shared/src/utils';
|
||||
import SourceMapConsumer from 'react-devtools-shared/src/hooks/SourceMapConsumer';
|
||||
|
||||
import type {Source} from 'react-devtools-shared/src/shared/types';
|
||||
@@ -91,9 +90,8 @@ export async function symbolicateSource(
|
||||
try {
|
||||
// sourceMapURL = https://react.dev/script.js.map
|
||||
void new URL(possiblyURL); // test if it is a valid URL
|
||||
const normalizedURL = normalizeUrl(possiblyURL);
|
||||
|
||||
return {sourceURL: normalizedURL, line, column};
|
||||
return {sourceURL: possiblyURL, line, column};
|
||||
} catch (e) {
|
||||
// This is not valid URL
|
||||
if (
|
||||
|
||||
55
packages/react-devtools-shared/src/utils.js
vendored
55
packages/react-devtools-shared/src/utils.js
vendored
@@ -563,6 +563,7 @@ export type DataType =
|
||||
| 'nan'
|
||||
| 'null'
|
||||
| 'number'
|
||||
| 'thenable'
|
||||
| 'object'
|
||||
| 'react_element'
|
||||
| 'regexp'
|
||||
@@ -631,6 +632,8 @@ export function getDataType(data: Object): DataType {
|
||||
}
|
||||
} else if (data.constructor && data.constructor.name === 'RegExp') {
|
||||
return 'regexp';
|
||||
} else if (typeof data.then === 'function') {
|
||||
return 'thenable';
|
||||
} else {
|
||||
// $FlowFixMe[method-unbinding]
|
||||
const toStringValue = Object.prototype.toString.call(data);
|
||||
@@ -934,6 +937,42 @@ export function formatDataForPreview(
|
||||
} catch (error) {
|
||||
return 'unserializable';
|
||||
}
|
||||
case 'thenable':
|
||||
let displayName: string;
|
||||
if (isPlainObject(data)) {
|
||||
displayName = 'Thenable';
|
||||
} else {
|
||||
let resolvedConstructorName = data.constructor.name;
|
||||
if (typeof resolvedConstructorName !== 'string') {
|
||||
resolvedConstructorName =
|
||||
Object.getPrototypeOf(data).constructor.name;
|
||||
}
|
||||
if (typeof resolvedConstructorName === 'string') {
|
||||
displayName = resolvedConstructorName;
|
||||
} else {
|
||||
displayName = 'Thenable';
|
||||
}
|
||||
}
|
||||
switch (data.status) {
|
||||
case 'pending':
|
||||
return `pending ${displayName}`;
|
||||
case 'fulfilled':
|
||||
if (showFormattedValue) {
|
||||
const formatted = formatDataForPreview(data.value, false);
|
||||
return `fulfilled ${displayName} {${truncateForDisplay(formatted)}}`;
|
||||
} else {
|
||||
return `fulfilled ${displayName} {…}`;
|
||||
}
|
||||
case 'rejected':
|
||||
if (showFormattedValue) {
|
||||
const formatted = formatDataForPreview(data.reason, false);
|
||||
return `rejected ${displayName} {${truncateForDisplay(formatted)}}`;
|
||||
} else {
|
||||
return `rejected ${displayName} {…}`;
|
||||
}
|
||||
default:
|
||||
return displayName;
|
||||
}
|
||||
case 'object':
|
||||
if (showFormattedValue) {
|
||||
const keys = Array.from(getAllEnumerableKeys(data)).sort(alphaSortKeys);
|
||||
@@ -963,7 +1002,7 @@ export function formatDataForPreview(
|
||||
case 'nan':
|
||||
case 'null':
|
||||
case 'undefined':
|
||||
return data;
|
||||
return String(data);
|
||||
default:
|
||||
try {
|
||||
return truncateForDisplay(String(data));
|
||||
@@ -996,9 +1035,17 @@ export function backendToFrontendSerializedElementMapper(
|
||||
};
|
||||
}
|
||||
|
||||
// Chrome normalizes urls like webpack-internals:// but new URL don't, so cannot use new URL here.
|
||||
export function normalizeUrl(url: string): string {
|
||||
return url.replace('/./', '/');
|
||||
/**
|
||||
* Should be used when treating url as a Chrome Resource URL.
|
||||
*/
|
||||
export function normalizeUrlIfValid(url: string): string {
|
||||
try {
|
||||
// TODO: Chrome will use the basepath to create a Resource URL.
|
||||
return new URL(url).toString();
|
||||
} catch {
|
||||
// Giving up if it's not a valid URL without basepath
|
||||
return url;
|
||||
}
|
||||
}
|
||||
|
||||
export function getIsReloadAndProfileSupported(): boolean {
|
||||
|
||||
@@ -2,7 +2,7 @@ Harness for testing local changes to the `react-devtools-inline` and `react-devt
|
||||
|
||||
## Development
|
||||
|
||||
This target should be run in parallel with the `react-devtools-inline` package. The first step then is to run that target following the instructions in the [`react-devtools-inline` README's local development section](https://github.com/facebook/react/tree/main/packages/react-devtools-inline#local-development).
|
||||
This target should be run in parallel with the `react-devtools-inline` package. The first step then is to run that target following the instructions in the [`react-devtools-inline` README's local development section](../react-devtools-inline/README.md#local-development).
|
||||
|
||||
The test harness can then be run as follows:
|
||||
```sh
|
||||
|
||||
@@ -49,6 +49,10 @@ const objectOfObjects = {
|
||||
j: 9,
|
||||
},
|
||||
qux: {},
|
||||
quux: {
|
||||
k: undefined,
|
||||
l: null,
|
||||
},
|
||||
};
|
||||
|
||||
function useOuterFoo() {
|
||||
@@ -106,6 +110,26 @@ function useInnerBaz() {
|
||||
return count;
|
||||
}
|
||||
|
||||
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(
|
||||
// eslint-disable-next-line react-internal/prod-error-codes
|
||||
new Error('test-error-do-not-surface'),
|
||||
);
|
||||
|
||||
export default function Hydration(): React.Node {
|
||||
return (
|
||||
<Fragment>
|
||||
@@ -120,17 +144,55 @@ export default function Hydration(): React.Node {
|
||||
date={new Date()}
|
||||
array={arrayOfArrays}
|
||||
object={objectOfObjects}
|
||||
unusedPromise={unusedPromise}
|
||||
usedFulfilledPromise={usedFulfilledPromise}
|
||||
usedFulfilledRichPromise={usedFulfilledRichPromise}
|
||||
usedPendingPromise={usedPendingPromise}
|
||||
usedRejectedPromise={usedRejectedPromise}
|
||||
/>
|
||||
<DeepHooks />
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
function Use({value}: {value: Promise<mixed>}): React.Node {
|
||||
React.use(value);
|
||||
return null;
|
||||
}
|
||||
|
||||
class IgnoreErrors extends React.Component {
|
||||
state: {hasError: boolean} = {hasError: false};
|
||||
static getDerivedStateFromError(): {hasError: boolean} {
|
||||
return {hasError: true};
|
||||
}
|
||||
|
||||
render(): React.Node {
|
||||
if (this.state.hasError) {
|
||||
return null;
|
||||
}
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
|
||||
function DehydratableProps({array, object}: any) {
|
||||
return (
|
||||
<ul>
|
||||
<li>array: {JSON.stringify(array, null, 2)}</li>
|
||||
<li>object: {JSON.stringify(object, null, 2)}</li>
|
||||
<React.Suspense>
|
||||
<Use value={usedPendingPromise} />
|
||||
</React.Suspense>
|
||||
<React.Suspense>
|
||||
<Use value={usedFulfilledPromise} />
|
||||
</React.Suspense>
|
||||
<React.Suspense>
|
||||
<Use value={usedFulfilledRichPromise} />
|
||||
</React.Suspense>
|
||||
<IgnoreErrors>
|
||||
<React.Suspense>
|
||||
<Use value={usedRejectedPromise} />
|
||||
</React.Suspense>
|
||||
</IgnoreErrors>
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -176,6 +176,14 @@ const appServer = new WebpackDevServer(
|
||||
logging: 'warn',
|
||||
overlay: {
|
||||
warnings: false,
|
||||
runtimeErrors: error => {
|
||||
const shouldIgnoreError =
|
||||
error !== null &&
|
||||
typeof error === 'object' &&
|
||||
error.message === 'test-error-do-not-surface';
|
||||
|
||||
return !shouldIgnoreError;
|
||||
},
|
||||
},
|
||||
},
|
||||
static: {
|
||||
|
||||
@@ -17,6 +17,7 @@ import type {
|
||||
Container,
|
||||
TextInstance,
|
||||
Instance,
|
||||
ActivityInstance,
|
||||
SuspenseInstance,
|
||||
Props,
|
||||
HoistableRoot,
|
||||
@@ -30,9 +31,10 @@ import {
|
||||
HostText,
|
||||
HostRoot,
|
||||
SuspenseComponent,
|
||||
ActivityComponent,
|
||||
} from 'react-reconciler/src/ReactWorkTags';
|
||||
|
||||
import {getParentSuspenseInstance} from './ReactFiberConfigDOM';
|
||||
import {getParentHydrationBoundary} from './ReactFiberConfigDOM';
|
||||
|
||||
import {enableScopeAPI} from 'shared/ReactFeatureFlags';
|
||||
|
||||
@@ -59,7 +61,12 @@ export function detachDeletedInstance(node: Instance): void {
|
||||
|
||||
export function precacheFiberNode(
|
||||
hostInst: Fiber,
|
||||
node: Instance | TextInstance | SuspenseInstance | ReactScopeInstance,
|
||||
node:
|
||||
| Instance
|
||||
| TextInstance
|
||||
| SuspenseInstance
|
||||
| ActivityInstance
|
||||
| ReactScopeInstance,
|
||||
): void {
|
||||
(node: any)[internalInstanceKey] = hostInst;
|
||||
}
|
||||
@@ -81,15 +88,16 @@ export function isContainerMarkedAsRoot(node: Container): boolean {
|
||||
|
||||
// Given a DOM node, return the closest HostComponent or HostText fiber ancestor.
|
||||
// If the target node is part of a hydrated or not yet rendered subtree, then
|
||||
// this may also return a SuspenseComponent or HostRoot to indicate that.
|
||||
// this may also return a SuspenseComponent, ActivityComponent or HostRoot to
|
||||
// indicate that.
|
||||
// Conceptually the HostRoot fiber is a child of the Container node. So if you
|
||||
// pass the Container node as the targetNode, you will not actually get the
|
||||
// HostRoot back. To get to the HostRoot, you need to pass a child of it.
|
||||
// The same thing applies to Suspense boundaries.
|
||||
// The same thing applies to Suspense and Activity boundaries.
|
||||
export function getClosestInstanceFromNode(targetNode: Node): null | Fiber {
|
||||
let targetInst = (targetNode: any)[internalInstanceKey];
|
||||
if (targetInst) {
|
||||
// Don't return HostRoot or SuspenseComponent here.
|
||||
// Don't return HostRoot, SuspenseComponent or ActivityComponent here.
|
||||
return targetInst;
|
||||
}
|
||||
// If the direct event target isn't a React owned DOM node, we need to look
|
||||
@@ -129,8 +137,8 @@ export function getClosestInstanceFromNode(targetNode: Node): null | Fiber {
|
||||
) {
|
||||
// Next we need to figure out if the node that skipped past is
|
||||
// nested within a dehydrated boundary and if so, which one.
|
||||
let suspenseInstance = getParentSuspenseInstance(targetNode);
|
||||
while (suspenseInstance !== null) {
|
||||
let hydrationInstance = getParentHydrationBoundary(targetNode);
|
||||
while (hydrationInstance !== null) {
|
||||
// We found a suspense instance. That means that we haven't
|
||||
// hydrated it yet. Even though we leave the comments in the
|
||||
// DOM after hydrating, and there are boundaries in the DOM
|
||||
@@ -140,15 +148,15 @@ export function getClosestInstanceFromNode(targetNode: Node): null | Fiber {
|
||||
// Let's get the fiber associated with the SuspenseComponent
|
||||
// as the deepest instance.
|
||||
// $FlowFixMe[prop-missing]
|
||||
const targetSuspenseInst = suspenseInstance[internalInstanceKey];
|
||||
if (targetSuspenseInst) {
|
||||
return targetSuspenseInst;
|
||||
const targetFiber = hydrationInstance[internalInstanceKey];
|
||||
if (targetFiber) {
|
||||
return targetFiber;
|
||||
}
|
||||
// If we don't find a Fiber on the comment, it might be because
|
||||
// we haven't gotten to hydrate it yet. There might still be a
|
||||
// parent boundary that hasn't above this one so we need to find
|
||||
// the outer most that is known.
|
||||
suspenseInstance = getParentSuspenseInstance(suspenseInstance);
|
||||
hydrationInstance = getParentHydrationBoundary(hydrationInstance);
|
||||
// If we don't find one, then that should mean that the parent
|
||||
// host component also hasn't hydrated yet. We can return it
|
||||
// below since it will bail out on the isMounted check later.
|
||||
@@ -176,6 +184,7 @@ export function getInstanceFromNode(node: Node): Fiber | null {
|
||||
tag === HostComponent ||
|
||||
tag === HostText ||
|
||||
tag === SuspenseComponent ||
|
||||
tag === ActivityComponent ||
|
||||
tag === HostHoistable ||
|
||||
tag === HostSingleton ||
|
||||
tag === HostRoot
|
||||
@@ -211,15 +220,17 @@ export function getNodeFromInstance(inst: Fiber): Instance | TextInstance {
|
||||
}
|
||||
|
||||
export function getFiberCurrentPropsFromNode(
|
||||
node: Container | Instance | TextInstance | SuspenseInstance,
|
||||
node:
|
||||
| Container
|
||||
| Instance
|
||||
| TextInstance
|
||||
| SuspenseInstance
|
||||
| ActivityInstance,
|
||||
): Props {
|
||||
return (node: any)[internalPropsKey] || null;
|
||||
}
|
||||
|
||||
export function updateFiberProps(
|
||||
node: Instance | TextInstance | SuspenseInstance,
|
||||
props: Props,
|
||||
): void {
|
||||
export function updateFiberProps(node: Instance, props: Props): void {
|
||||
(node: any)[internalPropsKey] = props;
|
||||
}
|
||||
|
||||
|
||||
@@ -187,13 +187,20 @@ export type Container =
|
||||
| interface extends DocumentFragment {_reactRootContainer?: FiberRoot};
|
||||
export type Instance = Element;
|
||||
export type TextInstance = Text;
|
||||
export interface SuspenseInstance extends Comment {
|
||||
_reactRetry?: () => void;
|
||||
|
||||
declare class ActivityInterface extends Comment {}
|
||||
declare class SuspenseInterface extends Comment {
|
||||
_reactRetry: void | (() => void);
|
||||
}
|
||||
|
||||
export type ActivityInstance = ActivityInterface;
|
||||
export type SuspenseInstance = SuspenseInterface;
|
||||
|
||||
type FormStateMarkerInstance = Comment;
|
||||
export type HydratableInstance =
|
||||
| Instance
|
||||
| TextInstance
|
||||
| ActivityInstance
|
||||
| SuspenseInstance
|
||||
| FormStateMarkerInstance;
|
||||
export type PublicInstance = Element | Text;
|
||||
@@ -226,6 +233,8 @@ type SelectionInformation = {
|
||||
|
||||
const SUPPRESS_HYDRATION_WARNING = 'suppressHydrationWarning';
|
||||
|
||||
const ACTIVITY_START_DATA = '&';
|
||||
const ACTIVITY_END_DATA = '/&';
|
||||
const SUSPENSE_START_DATA = '$';
|
||||
const SUSPENSE_END_DATA = '/$';
|
||||
const SUSPENSE_PENDING_START_DATA = '$?';
|
||||
@@ -947,7 +956,7 @@ export function appendChildToContainer(
|
||||
export function insertBefore(
|
||||
parentInstance: Instance,
|
||||
child: Instance | TextInstance,
|
||||
beforeChild: Instance | TextInstance | SuspenseInstance,
|
||||
beforeChild: Instance | TextInstance | SuspenseInstance | ActivityInstance,
|
||||
): void {
|
||||
if (supportsMoveBefore && child.parentNode !== null) {
|
||||
// $FlowFixMe[prop-missing]: We've checked this with supportsMoveBefore.
|
||||
@@ -960,7 +969,7 @@ export function insertBefore(
|
||||
export function insertInContainerBefore(
|
||||
container: Container,
|
||||
child: Instance | TextInstance,
|
||||
beforeChild: Instance | TextInstance | SuspenseInstance,
|
||||
beforeChild: Instance | TextInstance | SuspenseInstance | ActivityInstance,
|
||||
): void {
|
||||
if (__DEV__) {
|
||||
warnForReactChildrenConflict(container);
|
||||
@@ -1024,14 +1033,14 @@ function dispatchAfterDetachedBlur(target: HTMLElement): void {
|
||||
|
||||
export function removeChild(
|
||||
parentInstance: Instance,
|
||||
child: Instance | TextInstance | SuspenseInstance,
|
||||
child: Instance | TextInstance | SuspenseInstance | ActivityInstance,
|
||||
): void {
|
||||
parentInstance.removeChild(child);
|
||||
}
|
||||
|
||||
export function removeChildFromContainer(
|
||||
container: Container,
|
||||
child: Instance | TextInstance | SuspenseInstance,
|
||||
child: Instance | TextInstance | SuspenseInstance | ActivityInstance,
|
||||
): void {
|
||||
let parentNode: DocumentFragment | Element;
|
||||
if (container.nodeType === DOCUMENT_NODE) {
|
||||
@@ -1049,11 +1058,11 @@ export function removeChildFromContainer(
|
||||
parentNode.removeChild(child);
|
||||
}
|
||||
|
||||
export function clearSuspenseBoundary(
|
||||
function clearHydrationBoundary(
|
||||
parentInstance: Instance,
|
||||
suspenseInstance: SuspenseInstance,
|
||||
hydrationInstance: SuspenseInstance | ActivityInstance,
|
||||
): void {
|
||||
let node: Node = suspenseInstance;
|
||||
let node: Node = hydrationInstance;
|
||||
// Delete all nodes within this suspense boundary.
|
||||
// There might be nested nodes so we need to keep track of how
|
||||
// deep we are and only break out when we're back on top.
|
||||
@@ -1063,11 +1072,11 @@ export function clearSuspenseBoundary(
|
||||
parentInstance.removeChild(node);
|
||||
if (nextNode && nextNode.nodeType === COMMENT_NODE) {
|
||||
const data = ((nextNode: any).data: string);
|
||||
if (data === SUSPENSE_END_DATA) {
|
||||
if (data === SUSPENSE_END_DATA || data === ACTIVITY_END_DATA) {
|
||||
if (depth === 0) {
|
||||
parentInstance.removeChild(nextNode);
|
||||
// Retry if any event replaying was blocked on this.
|
||||
retryIfBlockedOn(suspenseInstance);
|
||||
retryIfBlockedOn(hydrationInstance);
|
||||
return;
|
||||
} else {
|
||||
depth--;
|
||||
@@ -1075,7 +1084,8 @@ export function clearSuspenseBoundary(
|
||||
} else if (
|
||||
data === SUSPENSE_START_DATA ||
|
||||
data === SUSPENSE_PENDING_START_DATA ||
|
||||
data === SUSPENSE_FALLBACK_START_DATA
|
||||
data === SUSPENSE_FALLBACK_START_DATA ||
|
||||
data === ACTIVITY_START_DATA
|
||||
) {
|
||||
depth++;
|
||||
} else if (data === PREAMBLE_CONTRIBUTION_HTML) {
|
||||
@@ -1102,12 +1112,26 @@ export function clearSuspenseBoundary(
|
||||
} while (node);
|
||||
// TODO: Warn, we didn't find the end comment boundary.
|
||||
// Retry if any event replaying was blocked on this.
|
||||
retryIfBlockedOn(suspenseInstance);
|
||||
retryIfBlockedOn(hydrationInstance);
|
||||
}
|
||||
|
||||
export function clearSuspenseBoundaryFromContainer(
|
||||
container: Container,
|
||||
export function clearActivityBoundary(
|
||||
parentInstance: Instance,
|
||||
activityInstance: ActivityInstance,
|
||||
): void {
|
||||
clearHydrationBoundary(parentInstance, activityInstance);
|
||||
}
|
||||
|
||||
export function clearSuspenseBoundary(
|
||||
parentInstance: Instance,
|
||||
suspenseInstance: SuspenseInstance,
|
||||
): void {
|
||||
clearHydrationBoundary(parentInstance, suspenseInstance);
|
||||
}
|
||||
|
||||
function clearHydrationBoundaryFromContainer(
|
||||
container: Container,
|
||||
hydrationInstance: SuspenseInstance | ActivityInstance,
|
||||
): void {
|
||||
let parentNode: DocumentFragment | Element;
|
||||
if (container.nodeType === DOCUMENT_NODE) {
|
||||
@@ -1122,11 +1146,82 @@ export function clearSuspenseBoundaryFromContainer(
|
||||
} else {
|
||||
parentNode = (container: any);
|
||||
}
|
||||
clearSuspenseBoundary(parentNode, suspenseInstance);
|
||||
clearHydrationBoundary(parentNode, hydrationInstance);
|
||||
// Retry if any event replaying was blocked on this.
|
||||
retryIfBlockedOn(container);
|
||||
}
|
||||
|
||||
export function clearActivityBoundaryFromContainer(
|
||||
container: Container,
|
||||
activityInstance: ActivityInstance,
|
||||
): void {
|
||||
clearHydrationBoundaryFromContainer(container, activityInstance);
|
||||
}
|
||||
|
||||
export function clearSuspenseBoundaryFromContainer(
|
||||
container: Container,
|
||||
suspenseInstance: SuspenseInstance,
|
||||
): void {
|
||||
clearHydrationBoundaryFromContainer(container, suspenseInstance);
|
||||
}
|
||||
|
||||
function hideOrUnhideDehydratedBoundary(
|
||||
suspenseInstance: SuspenseInstance | ActivityInstance,
|
||||
isHidden: boolean,
|
||||
) {
|
||||
let node: Node = suspenseInstance;
|
||||
// Unhide all nodes within this suspense boundary.
|
||||
let depth = 0;
|
||||
do {
|
||||
const nextNode = node.nextSibling;
|
||||
if (node.nodeType === ELEMENT_NODE) {
|
||||
const instance = ((node: any): HTMLElement & {_stashedDisplay?: string});
|
||||
if (isHidden) {
|
||||
instance._stashedDisplay = instance.style.display;
|
||||
instance.style.display = 'none';
|
||||
} else {
|
||||
instance.style.display = instance._stashedDisplay || '';
|
||||
if (instance.getAttribute('style') === '') {
|
||||
instance.removeAttribute('style');
|
||||
}
|
||||
}
|
||||
} else if (node.nodeType === TEXT_NODE) {
|
||||
const textNode = ((node: any): Text & {_stashedText?: string});
|
||||
if (isHidden) {
|
||||
textNode._stashedText = textNode.nodeValue;
|
||||
textNode.nodeValue = '';
|
||||
} else {
|
||||
textNode.nodeValue = textNode._stashedText || '';
|
||||
}
|
||||
}
|
||||
if (nextNode && nextNode.nodeType === COMMENT_NODE) {
|
||||
const data = ((nextNode: any).data: string);
|
||||
if (data === SUSPENSE_END_DATA) {
|
||||
if (depth === 0) {
|
||||
return;
|
||||
} else {
|
||||
depth--;
|
||||
}
|
||||
} else if (
|
||||
data === SUSPENSE_START_DATA ||
|
||||
data === SUSPENSE_PENDING_START_DATA ||
|
||||
data === SUSPENSE_FALLBACK_START_DATA
|
||||
) {
|
||||
depth++;
|
||||
}
|
||||
// TODO: Should we hide preamble contribution in this case?
|
||||
}
|
||||
// $FlowFixMe[incompatible-type] we bail out when we get a null
|
||||
node = nextNode;
|
||||
} while (node);
|
||||
}
|
||||
|
||||
export function hideDehydratedBoundary(
|
||||
suspenseInstance: SuspenseInstance,
|
||||
): void {
|
||||
hideOrUnhideDehydratedBoundary(suspenseInstance, true);
|
||||
}
|
||||
|
||||
export function hideInstance(instance: Instance): void {
|
||||
// TODO: Does this work for all element types? What about MathML? Should we
|
||||
// pass host context to this method?
|
||||
@@ -1144,6 +1239,12 @@ export function hideTextInstance(textInstance: TextInstance): void {
|
||||
textInstance.nodeValue = '';
|
||||
}
|
||||
|
||||
export function unhideDehydratedBoundary(
|
||||
dehydratedInstance: SuspenseInstance | ActivityInstance,
|
||||
): void {
|
||||
hideOrUnhideDehydratedBoundary(dehydratedInstance, false);
|
||||
}
|
||||
|
||||
export function unhideInstance(instance: Instance, props: Props): void {
|
||||
instance = ((instance: any): HTMLElement);
|
||||
const styleProp = props[STYLE];
|
||||
@@ -2986,10 +3087,10 @@ export function canHydrateTextInstance(
|
||||
return ((instance: any): TextInstance);
|
||||
}
|
||||
|
||||
export function canHydrateSuspenseInstance(
|
||||
function canHydrateHydrationBoundary(
|
||||
instance: HydratableInstance,
|
||||
inRootOrSingleton: boolean,
|
||||
): null | SuspenseInstance {
|
||||
): null | SuspenseInstance | ActivityInstance {
|
||||
while (instance.nodeType !== COMMENT_NODE) {
|
||||
if (!inRootOrSingleton) {
|
||||
return null;
|
||||
@@ -3000,8 +3101,42 @@ export function canHydrateSuspenseInstance(
|
||||
}
|
||||
instance = nextInstance;
|
||||
}
|
||||
// This has now been refined to a suspense node.
|
||||
return ((instance: any): SuspenseInstance);
|
||||
// This has now been refined to a hydration boundary node.
|
||||
return (instance: any);
|
||||
}
|
||||
|
||||
export function canHydrateActivityInstance(
|
||||
instance: HydratableInstance,
|
||||
inRootOrSingleton: boolean,
|
||||
): null | ActivityInstance {
|
||||
const hydratableInstance = canHydrateHydrationBoundary(
|
||||
instance,
|
||||
inRootOrSingleton,
|
||||
);
|
||||
if (
|
||||
hydratableInstance !== null &&
|
||||
hydratableInstance.data === ACTIVITY_START_DATA
|
||||
) {
|
||||
return (hydratableInstance: any);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function canHydrateSuspenseInstance(
|
||||
instance: HydratableInstance,
|
||||
inRootOrSingleton: boolean,
|
||||
): null | SuspenseInstance {
|
||||
const hydratableInstance = canHydrateHydrationBoundary(
|
||||
instance,
|
||||
inRootOrSingleton,
|
||||
);
|
||||
if (
|
||||
hydratableInstance !== null &&
|
||||
hydratableInstance.data !== ACTIVITY_START_DATA
|
||||
) {
|
||||
return (hydratableInstance: any);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function isSuspenseInstancePending(instance: SuspenseInstance): boolean {
|
||||
@@ -3125,12 +3260,13 @@ function getNextHydratable(node: ?Node) {
|
||||
nodeData === SUSPENSE_START_DATA ||
|
||||
nodeData === SUSPENSE_FALLBACK_START_DATA ||
|
||||
nodeData === SUSPENSE_PENDING_START_DATA ||
|
||||
nodeData === ACTIVITY_START_DATA ||
|
||||
nodeData === FORM_STATE_IS_MATCHING ||
|
||||
nodeData === FORM_STATE_IS_NOT_MATCHING
|
||||
) {
|
||||
break;
|
||||
}
|
||||
if (nodeData === SUSPENSE_END_DATA) {
|
||||
if (nodeData === SUSPENSE_END_DATA || nodeData === ACTIVITY_END_DATA) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -3169,6 +3305,12 @@ export function getFirstHydratableChildWithinContainer(
|
||||
return getNextHydratable(parentElement.firstChild);
|
||||
}
|
||||
|
||||
export function getFirstHydratableChildWithinActivityInstance(
|
||||
parentInstance: ActivityInstance,
|
||||
): null | HydratableInstance {
|
||||
return getNextHydratable(parentInstance.nextSibling);
|
||||
}
|
||||
|
||||
export function getFirstHydratableChildWithinSuspenseInstance(
|
||||
parentInstance: SuspenseInstance,
|
||||
): null | HydratableInstance {
|
||||
@@ -3220,6 +3362,12 @@ export function describeHydratableInstanceForDevWarnings(
|
||||
props: getPropsFromElement((instance: any)),
|
||||
};
|
||||
} else if (instance.nodeType === COMMENT_NODE) {
|
||||
if (instance.data === ACTIVITY_START_DATA) {
|
||||
return {
|
||||
type: 'Activity',
|
||||
props: {},
|
||||
};
|
||||
}
|
||||
return {
|
||||
type: 'Suspense',
|
||||
props: {},
|
||||
@@ -3311,6 +3459,13 @@ export function diffHydratedTextForDevWarnings(
|
||||
return null;
|
||||
}
|
||||
|
||||
export function hydrateActivityInstance(
|
||||
activityInstance: ActivityInstance,
|
||||
internalInstanceHandle: Object,
|
||||
) {
|
||||
precacheFiberNode(internalInstanceHandle, activityInstance);
|
||||
}
|
||||
|
||||
export function hydrateSuspenseInstance(
|
||||
suspenseInstance: SuspenseInstance,
|
||||
internalInstanceHandle: Object,
|
||||
@@ -3318,10 +3473,10 @@ export function hydrateSuspenseInstance(
|
||||
precacheFiberNode(internalInstanceHandle, suspenseInstance);
|
||||
}
|
||||
|
||||
export function getNextHydratableInstanceAfterSuspenseInstance(
|
||||
suspenseInstance: SuspenseInstance,
|
||||
function getNextHydratableInstanceAfterHydrationBoundary(
|
||||
hydrationInstance: SuspenseInstance | ActivityInstance,
|
||||
): null | HydratableInstance {
|
||||
let node = suspenseInstance.nextSibling;
|
||||
let node = hydrationInstance.nextSibling;
|
||||
// Skip past all nodes within this suspense boundary.
|
||||
// There might be nested nodes so we need to keep track of how
|
||||
// deep we are and only break out when we're back on top.
|
||||
@@ -3329,7 +3484,7 @@ export function getNextHydratableInstanceAfterSuspenseInstance(
|
||||
while (node) {
|
||||
if (node.nodeType === COMMENT_NODE) {
|
||||
const data = ((node: any).data: string);
|
||||
if (data === SUSPENSE_END_DATA) {
|
||||
if (data === SUSPENSE_END_DATA || data === ACTIVITY_END_DATA) {
|
||||
if (depth === 0) {
|
||||
return getNextHydratableSibling((node: any));
|
||||
} else {
|
||||
@@ -3338,7 +3493,8 @@ export function getNextHydratableInstanceAfterSuspenseInstance(
|
||||
} else if (
|
||||
data === SUSPENSE_START_DATA ||
|
||||
data === SUSPENSE_FALLBACK_START_DATA ||
|
||||
data === SUSPENSE_PENDING_START_DATA
|
||||
data === SUSPENSE_PENDING_START_DATA ||
|
||||
data === ACTIVITY_START_DATA
|
||||
) {
|
||||
depth++;
|
||||
}
|
||||
@@ -3349,12 +3505,24 @@ export function getNextHydratableInstanceAfterSuspenseInstance(
|
||||
return null;
|
||||
}
|
||||
|
||||
export function getNextHydratableInstanceAfterActivityInstance(
|
||||
activityInstance: ActivityInstance,
|
||||
): null | HydratableInstance {
|
||||
return getNextHydratableInstanceAfterHydrationBoundary(activityInstance);
|
||||
}
|
||||
|
||||
export function getNextHydratableInstanceAfterSuspenseInstance(
|
||||
suspenseInstance: SuspenseInstance,
|
||||
): null | HydratableInstance {
|
||||
return getNextHydratableInstanceAfterHydrationBoundary(suspenseInstance);
|
||||
}
|
||||
|
||||
// Returns the SuspenseInstance if this node is a direct child of a
|
||||
// SuspenseInstance. I.e. if its previous sibling is a Comment with
|
||||
// SUSPENSE_x_START_DATA. Otherwise, null.
|
||||
export function getParentSuspenseInstance(
|
||||
export function getParentHydrationBoundary(
|
||||
targetInstance: Node,
|
||||
): null | SuspenseInstance {
|
||||
): null | SuspenseInstance | ActivityInstance {
|
||||
let node = targetInstance.previousSibling;
|
||||
// Skip past all nodes within this suspense boundary.
|
||||
// There might be nested nodes so we need to keep track of how
|
||||
@@ -3366,14 +3534,15 @@ export function getParentSuspenseInstance(
|
||||
if (
|
||||
data === SUSPENSE_START_DATA ||
|
||||
data === SUSPENSE_FALLBACK_START_DATA ||
|
||||
data === SUSPENSE_PENDING_START_DATA
|
||||
data === SUSPENSE_PENDING_START_DATA ||
|
||||
data === ACTIVITY_START_DATA
|
||||
) {
|
||||
if (depth === 0) {
|
||||
return ((node: any): SuspenseInstance);
|
||||
return ((node: any): SuspenseInstance | ActivityInstance);
|
||||
} else {
|
||||
depth--;
|
||||
}
|
||||
} else if (data === SUSPENSE_END_DATA) {
|
||||
} else if (data === SUSPENSE_END_DATA || data === ACTIVITY_END_DATA) {
|
||||
depth++;
|
||||
}
|
||||
}
|
||||
@@ -3387,6 +3556,13 @@ export function commitHydratedContainer(container: Container): void {
|
||||
retryIfBlockedOn(container);
|
||||
}
|
||||
|
||||
export function commitHydratedActivityInstance(
|
||||
activityInstance: ActivityInstance,
|
||||
): void {
|
||||
// Retry if any event replaying was blocked on this.
|
||||
retryIfBlockedOn(activityInstance);
|
||||
}
|
||||
|
||||
export function commitHydratedSuspenseInstance(
|
||||
suspenseInstance: SuspenseInstance,
|
||||
): void {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user