Compare commits
157 Commits
plugin-use
...
main
| Author | SHA1 | Date |
|---|---|---|
|
|
4b877b6c66 | |
|
|
7bd330e0b0 | |
|
|
5309f10285 | |
|
|
d7a98a5e97 | |
|
|
2468a87358 | |
|
|
f8de255e94 | |
|
|
4bfcd02b2c | |
|
|
4cd7065665 | |
|
|
a389046a52 | |
|
|
67a05d03e3 | |
|
|
df12d7eac4 | |
|
|
7cd98ef2bc | |
|
|
b5810163e9 | |
|
|
fda1f0b902 | |
|
|
7ac5e9a602 | |
|
|
16d053d592 | |
|
|
efb381bbf9 | |
|
|
b00e27342d | |
|
|
783e7fcfa3 | |
|
|
377c5175f7 | |
|
|
aef7ce5547 | |
|
|
c10010a6a0 | |
|
|
f533cee8cb | |
|
|
2c1117a8d0 | |
|
|
fa7a447b9c | |
|
|
b7972822b5 | |
|
|
388686f291 | |
|
|
8a25302c66 | |
|
|
2c2476834a | |
|
|
fa4314841e | |
|
|
5dd90c5623 | |
|
|
559e83aebb | |
|
|
67f4fb0213 | |
|
|
8ea96ef84d | |
|
|
491aec5d61 | |
|
|
9545e4810c | |
|
|
86b0e91998 | |
|
|
b12bea62d9 | |
|
|
efbd68511d | |
|
|
18282f881d | |
|
|
540bab085d | |
|
|
6eadbe0c4a | |
|
|
ec5e9c2a75 | |
|
|
f87e97a0a6 | |
|
|
25b99efe0c | |
|
|
ed545ae3d3 | |
|
|
bf449ee74e | |
|
|
64d6be7122 | |
|
|
919620b293 | |
|
|
9ece58ebaa | |
|
|
5e5342b100 | |
|
|
9c58a0b647 | |
|
|
9ee7964302 | |
|
|
2fa6323818 | |
|
|
7ce765ec32 | |
|
|
a21d1475ff | |
|
|
36e4cbe2e9 | |
|
|
8f42196892 | |
|
|
967d46c76c | |
|
|
5d7ebb4b78 | |
|
|
fd3fb8e3c5 | |
|
|
c57a0f68a4 | |
|
|
c8369527ef | |
|
|
cc93a85332 | |
|
|
fdad813ac7 | |
|
|
e5708b3ea9 | |
|
|
d73d7d5908 | |
|
|
7f8c501f68 | |
|
|
22d5942675 | |
|
|
767f52237c | |
|
|
c826dc50de | |
|
|
cd2b79dedd | |
|
|
6d394e3d26 | |
|
|
1f248bdd71 | |
|
|
b90e8ebaa5 | |
|
|
a227bcd4f4 | |
|
|
c6db19f9cd | |
|
|
96fd2fb726 | |
|
|
d8089f2cf2 | |
|
|
1b4a0daba8 | |
|
|
b433c379d5 | |
|
|
d962f35cac | |
|
|
77d3b02e5c | |
|
|
b6006201b5 | |
|
|
da6c23a45c | |
|
|
2bfe4b246f | |
|
|
ab2385fa38 | |
|
|
39a3b72c6d | |
|
|
d121c67004 | |
|
|
7b0642bb98 | |
|
|
8256781fdf | |
|
|
72c890e312 | |
|
|
21021fb0f0 | |
|
|
5426af3d50 | |
|
|
f9de24a26a | |
|
|
6b90976bc1 | |
|
|
343a45ffa4 | |
|
|
58742c21b8 | |
|
|
0b931f90e8 | |
|
|
ac43bf6870 | |
|
|
ca41adb8c1 | |
|
|
dd0619b2ee | |
|
|
b55d319559 | |
|
|
ffb8eaca59 | |
|
|
f4f873f628 | |
|
|
44db16afc6 | |
|
|
451736b557 | |
|
|
fec97ecbc4 | |
|
|
9a9da7721e | |
|
|
e5146cb525 | |
|
|
657698e48d | |
|
|
dd5365878d | |
|
|
85bb7b685b | |
|
|
60cfeeebe3 | |
|
|
9cfba0f6ec | |
|
|
790ebc962d | |
|
|
c15579631f | |
|
|
d5fd60f7e6 | |
|
|
eeabb7312f | |
|
|
0ba4d7b0d8 | |
|
|
da94e8b24a | |
|
|
0700dd50bd | |
|
|
4a1cc2ddd0 | |
|
|
b14f8da155 | |
|
|
7329ea81c1 | |
|
|
0ae348018d | |
|
|
888874673f | |
|
|
09c8d25633 | |
|
|
8310854ceb | |
|
|
ca01f359b9 | |
|
|
43a70a610d | |
|
|
1308e49a69 | |
|
|
1a1d61fed9 | |
|
|
5b8cf20b38 | |
|
|
73deff0d51 | |
|
|
2d51251e60 | |
|
|
0ffc7f632b | |
|
|
29a3be78bd | |
|
|
4c2fc01900 | |
|
|
f0aafa1a7e | |
|
|
90995ef8b0 | |
|
|
f118b7cebf | |
|
|
fd0511c728 | |
|
|
85de6fde51 | |
|
|
5cbe6258bc | |
|
|
1f5cdf8c77 | |
|
|
f62cb39ee5 | |
|
|
f718199313 | |
|
|
41b4714f19 | |
|
|
fc90eb6368 | |
|
|
e0bbc26623 | |
|
|
8faf751937 | |
|
|
8342a09927 | |
|
|
afea1d0c53 | |
|
|
768f965de2 | |
|
|
d12bdcda69 | |
|
|
73b6435ca4 |
|
|
@ -426,7 +426,6 @@ jobs:
|
||||||
scripts/release/publish.js --ci --tags << parameters.dist_tag >>
|
scripts/release/publish.js --ci --tags << parameters.dist_tag >>
|
||||||
|
|
||||||
workflows:
|
workflows:
|
||||||
version: 2
|
|
||||||
|
|
||||||
build_and_test:
|
build_and_test:
|
||||||
unless: << pipeline.parameters.prerelease_commit_sha >>
|
unless: << pipeline.parameters.prerelease_commit_sha >>
|
||||||
|
|
@ -605,10 +604,20 @@ workflows:
|
||||||
when: << pipeline.parameters.prerelease_commit_sha >>
|
when: << pipeline.parameters.prerelease_commit_sha >>
|
||||||
jobs:
|
jobs:
|
||||||
- publish_prerelease:
|
- publish_prerelease:
|
||||||
name: Publish to Next channel
|
name: Publish to Canary channel
|
||||||
commit_sha: << pipeline.parameters.prerelease_commit_sha >>
|
commit_sha: << pipeline.parameters.prerelease_commit_sha >>
|
||||||
release_channel: stable
|
release_channel: stable
|
||||||
dist_tag: "next"
|
# The tags to use when publishing canaries. The main one we should
|
||||||
|
# always include is "canary" but we can use multiple (e.g. alpha,
|
||||||
|
# beta, rc). To declare multiple, use a comma-separated string, like
|
||||||
|
# this:
|
||||||
|
# dist_tag: "canary,alpha,beta,rc"
|
||||||
|
#
|
||||||
|
# TODO: We currently tag canaries with "next" in addition to "canary"
|
||||||
|
# because this used to be called the "next" channel and some
|
||||||
|
# downstream consumers might still expect that tag. We can remove this
|
||||||
|
# after some time has elapsed and the change has been communicated.
|
||||||
|
dist_tag: "canary,next"
|
||||||
- publish_prerelease:
|
- publish_prerelease:
|
||||||
name: Publish to Experimental channel
|
name: Publish to Experimental channel
|
||||||
requires:
|
requires:
|
||||||
|
|
@ -616,7 +625,7 @@ workflows:
|
||||||
# will sometimes fail if you try to concurrently publish two
|
# will sometimes fail if you try to concurrently publish two
|
||||||
# different versions of the same package, even if they use different
|
# different versions of the same package, even if they use different
|
||||||
# dist tags.
|
# dist tags.
|
||||||
- Publish to Next channel
|
- Publish to Canary channel
|
||||||
commit_sha: << pipeline.parameters.prerelease_commit_sha >>
|
commit_sha: << pipeline.parameters.prerelease_commit_sha >>
|
||||||
release_channel: experimental
|
release_channel: experimental
|
||||||
dist_tag: experimental
|
dist_tag: experimental
|
||||||
|
|
@ -634,10 +643,10 @@ workflows:
|
||||||
- main
|
- main
|
||||||
jobs:
|
jobs:
|
||||||
- publish_prerelease:
|
- publish_prerelease:
|
||||||
name: Publish to Next channel
|
name: Publish to Canary channel
|
||||||
commit_sha: << pipeline.git.revision >>
|
commit_sha: << pipeline.git.revision >>
|
||||||
release_channel: stable
|
release_channel: stable
|
||||||
dist_tag: "next"
|
dist_tag: "canary,next"
|
||||||
- publish_prerelease:
|
- publish_prerelease:
|
||||||
name: Publish to Experimental channel
|
name: Publish to Experimental channel
|
||||||
requires:
|
requires:
|
||||||
|
|
@ -645,7 +654,7 @@ workflows:
|
||||||
# will sometimes fail if you try to concurrently publish two
|
# will sometimes fail if you try to concurrently publish two
|
||||||
# different versions of the same package, even if they use different
|
# different versions of the same package, even if they use different
|
||||||
# dist tags.
|
# dist tags.
|
||||||
- Publish to Next channel
|
- Publish to Canary channel
|
||||||
commit_sha: << pipeline.git.revision >>
|
commit_sha: << pipeline.git.revision >>
|
||||||
release_channel: experimental
|
release_channel: experimental
|
||||||
dist_tag: experimental
|
dist_tag: experimental
|
||||||
|
|
|
||||||
13
.eslintrc.js
13
.eslintrc.js
|
|
@ -236,7 +236,14 @@ module.exports = {
|
||||||
'no-inner-declarations': [ERROR, 'functions'],
|
'no-inner-declarations': [ERROR, 'functions'],
|
||||||
'no-multi-spaces': ERROR,
|
'no-multi-spaces': ERROR,
|
||||||
'no-restricted-globals': [ERROR].concat(restrictedGlobals),
|
'no-restricted-globals': [ERROR].concat(restrictedGlobals),
|
||||||
'no-restricted-syntax': [ERROR, 'WithStatement'],
|
'no-restricted-syntax': [
|
||||||
|
ERROR,
|
||||||
|
'WithStatement',
|
||||||
|
{
|
||||||
|
selector: 'MemberExpression[property.name=/^(?:substring|substr)$/]',
|
||||||
|
message: 'Prefer string.slice() over .substring() and .substr().',
|
||||||
|
},
|
||||||
|
],
|
||||||
'no-shadow': ERROR,
|
'no-shadow': ERROR,
|
||||||
'no-unused-vars': [ERROR, {args: 'none'}],
|
'no-unused-vars': [ERROR, {args: 'none'}],
|
||||||
'no-use-before-define': OFF,
|
'no-use-before-define': OFF,
|
||||||
|
|
@ -409,7 +416,6 @@ module.exports = {
|
||||||
{
|
{
|
||||||
files: [
|
files: [
|
||||||
'packages/react-native-renderer/**/*.js',
|
'packages/react-native-renderer/**/*.js',
|
||||||
'packages/react-server-native-relay/**/*.js',
|
|
||||||
],
|
],
|
||||||
globals: {
|
globals: {
|
||||||
nativeFabricUIManager: 'readonly',
|
nativeFabricUIManager: 'readonly',
|
||||||
|
|
@ -449,6 +455,9 @@ module.exports = {
|
||||||
$ReadOnlyArray: 'readonly',
|
$ReadOnlyArray: 'readonly',
|
||||||
$Shape: 'readonly',
|
$Shape: 'readonly',
|
||||||
AnimationFrameID: 'readonly',
|
AnimationFrameID: 'readonly',
|
||||||
|
// For Flow type annotation. Only `BigInt` is valid at runtime.
|
||||||
|
bigint: 'readonly',
|
||||||
|
BigInt: 'readonly',
|
||||||
Class: 'readonly',
|
Class: 'readonly',
|
||||||
ClientRect: 'readonly',
|
ClientRect: 'readonly',
|
||||||
CopyInspectedElementPath: 'readonly',
|
CopyInspectedElementPath: 'readonly',
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,22 @@
|
||||||
name: Commit Artifacts for Facebook WWW and fbsource
|
name: Commit Artifacts for Meta WWW and fbsource
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [main]
|
branches: [main, meta-www, meta-fbsource]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
download_artifacts:
|
download_artifacts:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
www_branch_count: ${{ steps.check_branches.outputs.www_branch_count }}
|
||||||
|
fbsource_branch_count: ${{ steps.check_branches.outputs.fbsource_branch_count }}
|
||||||
steps:
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- name: "Check branches"
|
||||||
|
id: check_branches
|
||||||
|
run: |
|
||||||
|
echo "www_branch_count=$(git ls-remote --heads origin "refs/heads/meta-www" | wc -l)" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "fbsource_branch_count=$(git ls-remote --heads origin "refs/heads/meta-fbsource" | wc -l)" >> "$GITHUB_OUTPUT"
|
||||||
- name: Download and unzip artifacts
|
- name: Download and unzip artifacts
|
||||||
uses: actions/github-script@v6
|
uses: actions/github-script@v6
|
||||||
env:
|
env:
|
||||||
|
|
@ -168,6 +177,7 @@ jobs:
|
||||||
|
|
||||||
commit_www_artifacts:
|
commit_www_artifacts:
|
||||||
needs: download_artifacts
|
needs: download_artifacts
|
||||||
|
if: ${{ (github.ref == 'refs/heads/main' && needs.download_artifacts.outputs.www_branch_count == '0') || github.ref == 'refs/heads/meta-www' }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
|
|
@ -204,6 +214,7 @@ jobs:
|
||||||
commit_fbsource_artifacts:
|
commit_fbsource_artifacts:
|
||||||
needs: download_artifacts
|
needs: download_artifacts
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
if: ${{ (github.ref == 'refs/heads/main' && needs.download_artifacts.outputs.fbsource_branch_count == '0') || github.ref == 'refs/heads/meta-fbsource' }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
|
|
@ -217,7 +228,16 @@ jobs:
|
||||||
name: compiled-rn
|
name: compiled-rn
|
||||||
path: compiled-rn/
|
path: compiled-rn/
|
||||||
- run: git status -u
|
- run: git status -u
|
||||||
|
- name: Check if only the REVISION file has changed
|
||||||
|
id: check_should_commit
|
||||||
|
run: |
|
||||||
|
if git status --porcelain | grep -qv '/REVISION$'; then
|
||||||
|
echo "should_commit=true" >> "$GITHUB_OUTPUT"
|
||||||
|
else
|
||||||
|
echo "should_commit=false" >> "$GITHUB_OUTPUT"
|
||||||
|
fi
|
||||||
- name: Commit changes to branch
|
- name: Commit changes to branch
|
||||||
|
if: steps.check_should_commit.outputs.should_commit == 'true'
|
||||||
uses: stefanzweifel/git-auto-commit-action@v4
|
uses: stefanzweifel/git-auto-commit-action@v4
|
||||||
with:
|
with:
|
||||||
commit_message: |
|
commit_message: |
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
# [React](https://reactjs.org/) · [](https://github.com/facebook/react/blob/main/LICENSE) [](https://www.npmjs.com/package/react) [](https://circleci.com/gh/facebook/react) [](https://reactjs.org/docs/how-to-contribute.html#your-first-pull-request)
|
# [React](https://reactjs.org/) · [](https://github.com/facebook/react/blob/main/LICENSE) [](https://www.npmjs.com/package/react) [](https://circleci.com/gh/facebook/react) [](https://reactjs.org/docs/how-to-contribute.html#your-first-pull-request)
|
||||||
|
|
||||||
React is a JavaScript library for building user interfaces.
|
React is a JavaScript library for building user interfaces.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,12 +7,12 @@
|
||||||
//
|
//
|
||||||
// The @latest channel uses the version as-is, e.g.:
|
// The @latest channel uses the version as-is, e.g.:
|
||||||
//
|
//
|
||||||
// 18.0.0
|
// 18.3.0
|
||||||
//
|
//
|
||||||
// The @next channel appends additional information, with the scheme
|
// The @canary channel appends additional information, with the scheme
|
||||||
// <version>-<label>-<commit_sha>, e.g.:
|
// <version>-<label>-<commit_sha>, e.g.:
|
||||||
//
|
//
|
||||||
// 18.0.0-alpha-a1c2d3e4
|
// 18.3.0-canary-a1c2d3e4
|
||||||
//
|
//
|
||||||
// The @experimental channel doesn't include a version, only a date and a sha, e.g.:
|
// The @experimental channel doesn't include a version, only a date and a sha, e.g.:
|
||||||
//
|
//
|
||||||
|
|
@ -20,9 +20,13 @@
|
||||||
|
|
||||||
const ReactVersion = '18.3.0';
|
const ReactVersion = '18.3.0';
|
||||||
|
|
||||||
// The label used by the @next channel. Represents the upcoming release's
|
// The label used by the @canary channel. Represents the upcoming release's
|
||||||
// stability. Could be "alpha", "beta", "rc", etc.
|
// stability. Most of the time, this will be "canary", but we may temporarily
|
||||||
const nextChannelLabel = 'next';
|
// choose to change it to "alpha", "beta", "rc", etc.
|
||||||
|
//
|
||||||
|
// It only affects the label used in the version string. To customize the
|
||||||
|
// npm dist tags used during publish, refer to .circleci/config.yml.
|
||||||
|
const canaryChannelLabel = 'canary';
|
||||||
|
|
||||||
const stablePackages = {
|
const stablePackages = {
|
||||||
'eslint-plugin-react-hooks': '5.0.0',
|
'eslint-plugin-react-hooks': '5.0.0',
|
||||||
|
|
@ -40,14 +44,14 @@ const stablePackages = {
|
||||||
scheduler: '0.24.0',
|
scheduler: '0.24.0',
|
||||||
};
|
};
|
||||||
|
|
||||||
// These packages do not exist in the @next or @latest channel, only
|
// These packages do not exist in the @canary or @latest channel, only
|
||||||
// @experimental. We don't use semver, just the commit sha, so this is just a
|
// @experimental. We don't use semver, just the commit sha, so this is just a
|
||||||
// list of package names instead of a map.
|
// list of package names instead of a map.
|
||||||
const experimentalPackages = [];
|
const experimentalPackages = [];
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
ReactVersion,
|
ReactVersion,
|
||||||
nextChannelLabel,
|
canaryChannelLabel,
|
||||||
stablePackages,
|
stablePackages,
|
||||||
experimentalPackages,
|
experimentalPackages,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ class App extends PureComponent {
|
||||||
}
|
}
|
||||||
const multiplier = input.length !== 0 ? input.length : 1;
|
const multiplier = input.length !== 0 ? input.length : 1;
|
||||||
const complexity =
|
const complexity =
|
||||||
(parseInt(window.location.search.substring(1), 10) / 100) * 25 || 25;
|
(parseInt(window.location.search.slice(1), 10) / 100) * 25 || 25;
|
||||||
const data = _.range(5).map(t =>
|
const data = _.range(5).map(t =>
|
||||||
_.range(complexity * multiplier).map((j, i) => {
|
_.range(complexity * multiplier).map((j, i) => {
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@
|
||||||
<script src="https://unpkg.com/scheduler@canary/umd/scheduler-tracing.development.js"></script>
|
<script src="https://unpkg.com/scheduler@canary/umd/scheduler-tracing.development.js"></script>
|
||||||
<script src="https://unpkg.com/react@canary/umd/react.development.js"></script>
|
<script src="https://unpkg.com/react@canary/umd/react.development.js"></script>
|
||||||
<script src="https://unpkg.com/react-dom@canary/umd/react-dom.development.js"></script>
|
<script src="https://unpkg.com/react-dom@canary/umd/react-dom.development.js"></script>
|
||||||
<script src="https://unpkg.com/react-cache@next/umd/react-cache.development.js"></script>
|
<script src="https://unpkg.com/react-cache@canary/umd/react-cache.development.js"></script>
|
||||||
|
|
||||||
<!-- Don't use this in production: -->
|
<!-- Don't use this in production: -->
|
||||||
<script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>
|
<script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>
|
||||||
|
|
|
||||||
|
|
@ -11,11 +11,11 @@
|
||||||
__REACT_DEVTOOLS_GLOBAL_HOOK__ = parent.__REACT_DEVTOOLS_GLOBAL_HOOK__;
|
__REACT_DEVTOOLS_GLOBAL_HOOK__ = parent.__REACT_DEVTOOLS_GLOBAL_HOOK__;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script src="https://unpkg.com/scheduler@next/umd/scheduler.development.js"></script>
|
<script src="https://unpkg.com/scheduler@canary/umd/scheduler.development.js"></script>
|
||||||
<script src="https://unpkg.com/scheduler@next/umd/scheduler-tracing.development.js"></script>
|
<script src="https://unpkg.com/scheduler@canary/umd/scheduler-tracing.development.js"></script>
|
||||||
<script src="https://unpkg.com/react@next/umd/react.development.js"></script>
|
<script src="https://unpkg.com/react@canary/umd/react.development.js"></script>
|
||||||
<script src="https://unpkg.com/react-dom@next/umd/react-dom.development.js"></script>
|
<script src="https://unpkg.com/react-dom@canary/umd/react-dom.development.js"></script>
|
||||||
<script src="https://unpkg.com/react-cache@next/umd/react-cache.development.js"></script>
|
<script src="https://unpkg.com/react-cache@canary/umd/react-cache.development.js"></script>
|
||||||
|
|
||||||
<!-- Don't use this in production: -->
|
<!-- Don't use this in production: -->
|
||||||
<script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>
|
<script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
/* eslint-disable no-fallthrough, react/react-in-jsx-scope, react/jsx-no-undef */
|
/* eslint-disable react/react-in-jsx-scope, react/jsx-no-undef */
|
||||||
/* global React ReactCache ReactDOM SchedulerTracing ScheduleTracing */
|
/* global React ReactCache ReactDOM SchedulerTracing ScheduleTracing */
|
||||||
|
|
||||||
const apps = [];
|
const apps = [];
|
||||||
|
|
|
||||||
|
|
@ -334,6 +334,11 @@
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
class Foo {
|
||||||
|
flag = false;
|
||||||
|
object = {a: {b: {c: {d: 1}}}}
|
||||||
|
}
|
||||||
|
|
||||||
function UnserializableProps() {
|
function UnserializableProps() {
|
||||||
return (
|
return (
|
||||||
<ChildComponent
|
<ChildComponent
|
||||||
|
|
@ -343,6 +348,7 @@
|
||||||
setOfSets={setOfSets}
|
setOfSets={setOfSets}
|
||||||
typedArray={typedArray}
|
typedArray={typedArray}
|
||||||
immutable={immutable}
|
immutable={immutable}
|
||||||
|
classInstance={new Foo()}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ import semver from 'semver';
|
||||||
|
|
||||||
function parseQuery(qstr) {
|
function parseQuery(qstr) {
|
||||||
var query = {};
|
var query = {};
|
||||||
var a = qstr.substr(1).split('&');
|
var a = qstr.slice(1).split('&');
|
||||||
|
|
||||||
for (var i = 0; i < a.length; i++) {
|
for (var i = 0; i < a.length; i++) {
|
||||||
var b = a[i].split('=');
|
var b = a[i].split('=');
|
||||||
|
|
|
||||||
|
|
@ -95,6 +95,8 @@ app.all('/', async function (req, res, next) {
|
||||||
if (req.get('rsc-action')) {
|
if (req.get('rsc-action')) {
|
||||||
proxiedHeaders['Content-type'] = req.get('Content-type');
|
proxiedHeaders['Content-type'] = req.get('Content-type');
|
||||||
proxiedHeaders['rsc-action'] = req.get('rsc-action');
|
proxiedHeaders['rsc-action'] = req.get('rsc-action');
|
||||||
|
} else if (req.get('Content-type')) {
|
||||||
|
proxiedHeaders['Content-type'] = req.get('Content-type');
|
||||||
}
|
}
|
||||||
|
|
||||||
const promiseForData = request(
|
const promiseForData = request(
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,7 @@ const bodyParser = require('body-parser');
|
||||||
const busboy = require('busboy');
|
const busboy = require('busboy');
|
||||||
const app = express();
|
const app = express();
|
||||||
const compress = require('compression');
|
const compress = require('compression');
|
||||||
|
const {Readable} = require('node:stream');
|
||||||
|
|
||||||
app.use(compress());
|
app.use(compress());
|
||||||
|
|
||||||
|
|
@ -45,7 +46,7 @@ const {readFile} = require('fs').promises;
|
||||||
|
|
||||||
const React = require('react');
|
const React = require('react');
|
||||||
|
|
||||||
app.get('/', async function (req, res) {
|
async function renderApp(res, returnValue) {
|
||||||
const {renderToPipeableStream} = await import(
|
const {renderToPipeableStream} = await import(
|
||||||
'react-server-dom-webpack/server'
|
'react-server-dom-webpack/server'
|
||||||
);
|
);
|
||||||
|
|
@ -91,37 +92,74 @@ app.get('/', async function (req, res) {
|
||||||
),
|
),
|
||||||
React.createElement(App),
|
React.createElement(App),
|
||||||
];
|
];
|
||||||
const {pipe} = renderToPipeableStream(root, moduleMap);
|
// For client-invoked server actions we refresh the tree and return a return value.
|
||||||
|
const payload = returnValue ? {returnValue, root} : root;
|
||||||
|
const {pipe} = renderToPipeableStream(payload, moduleMap);
|
||||||
pipe(res);
|
pipe(res);
|
||||||
|
}
|
||||||
|
|
||||||
|
app.get('/', async function (req, res) {
|
||||||
|
await renderApp(res, null);
|
||||||
});
|
});
|
||||||
|
|
||||||
app.post('/', bodyParser.text(), async function (req, res) {
|
app.post('/', bodyParser.text(), async function (req, res) {
|
||||||
const {renderToPipeableStream, decodeReply, decodeReplyFromBusboy} =
|
const {
|
||||||
await import('react-server-dom-webpack/server');
|
renderToPipeableStream,
|
||||||
|
decodeReply,
|
||||||
|
decodeReplyFromBusboy,
|
||||||
|
decodeAction,
|
||||||
|
} = await import('react-server-dom-webpack/server');
|
||||||
const serverReference = req.get('rsc-action');
|
const serverReference = req.get('rsc-action');
|
||||||
const [filepath, name] = serverReference.split('#');
|
if (serverReference) {
|
||||||
const action = (await import(filepath))[name];
|
// This is the client-side case
|
||||||
// Validate that this is actually a function we intended to expose and
|
const [filepath, name] = serverReference.split('#');
|
||||||
// not the client trying to invoke arbitrary functions. In a real app,
|
const action = (await import(filepath))[name];
|
||||||
// you'd have a manifest verifying this before even importing it.
|
// Validate that this is actually a function we intended to expose and
|
||||||
if (action.$$typeof !== Symbol.for('react.server.reference')) {
|
// not the client trying to invoke arbitrary functions. In a real app,
|
||||||
throw new Error('Invalid action');
|
// you'd have a manifest verifying this before even importing it.
|
||||||
}
|
if (action.$$typeof !== Symbol.for('react.server.reference')) {
|
||||||
|
throw new Error('Invalid action');
|
||||||
|
}
|
||||||
|
|
||||||
let args;
|
let args;
|
||||||
if (req.is('multipart/form-data')) {
|
if (req.is('multipart/form-data')) {
|
||||||
// Use busboy to streamingly parse the reply from form-data.
|
// Use busboy to streamingly parse the reply from form-data.
|
||||||
const bb = busboy({headers: req.headers});
|
const bb = busboy({headers: req.headers});
|
||||||
const reply = decodeReplyFromBusboy(bb);
|
const reply = decodeReplyFromBusboy(bb);
|
||||||
req.pipe(bb);
|
req.pipe(bb);
|
||||||
args = await reply;
|
args = await reply;
|
||||||
|
} else {
|
||||||
|
args = await decodeReply(req.body);
|
||||||
|
}
|
||||||
|
const result = action.apply(null, args);
|
||||||
|
try {
|
||||||
|
// Wait for any mutations
|
||||||
|
await result;
|
||||||
|
} catch (x) {
|
||||||
|
// We handle the error on the client
|
||||||
|
}
|
||||||
|
// Refresh the client and return the value
|
||||||
|
renderApp(res, result);
|
||||||
} else {
|
} else {
|
||||||
args = await decodeReply(req.body);
|
// This is the progressive enhancement case
|
||||||
|
const UndiciRequest = require('undici').Request;
|
||||||
|
const fakeRequest = new UndiciRequest('http://localhost', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {'Content-Type': req.headers['content-type']},
|
||||||
|
body: Readable.toWeb(req),
|
||||||
|
duplex: 'half',
|
||||||
|
});
|
||||||
|
const formData = await fakeRequest.formData();
|
||||||
|
const action = await decodeAction(formData);
|
||||||
|
try {
|
||||||
|
// Wait for any mutations
|
||||||
|
await action();
|
||||||
|
} catch (x) {
|
||||||
|
const {setServerState} = await import('../src/ServerState.js');
|
||||||
|
setServerState('Error: ' + x.message);
|
||||||
|
}
|
||||||
|
renderApp(res, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = action.apply(null, args);
|
|
||||||
const {pipe} = renderToPipeableStream(result, {});
|
|
||||||
pipe(res);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
app.get('/todos', function (req, res) {
|
app.get('/todos', function (req, res) {
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,11 @@ import {Counter as Counter2} from './Counter2.js';
|
||||||
|
|
||||||
import ShowMore from './ShowMore.js';
|
import ShowMore from './ShowMore.js';
|
||||||
import Button from './Button.js';
|
import Button from './Button.js';
|
||||||
|
import Form from './Form.js';
|
||||||
|
|
||||||
import {like} from './actions.js';
|
import {like, greet} from './actions.js';
|
||||||
|
|
||||||
|
import {getServerState} from './ServerState.js';
|
||||||
|
|
||||||
export default async function App() {
|
export default async function App() {
|
||||||
const res = await fetch('http://localhost:3001/todos');
|
const res = await fetch('http://localhost:3001/todos');
|
||||||
|
|
@ -22,7 +25,7 @@ export default async function App() {
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<Container>
|
<Container>
|
||||||
<h1>Hello, world</h1>
|
<h1>{getServerState()}</h1>
|
||||||
<Counter />
|
<Counter />
|
||||||
<Counter2 />
|
<Counter2 />
|
||||||
<ul>
|
<ul>
|
||||||
|
|
@ -33,6 +36,7 @@ export default async function App() {
|
||||||
<ShowMore>
|
<ShowMore>
|
||||||
<p>Lorem ipsum</p>
|
<p>Lorem ipsum</p>
|
||||||
</ShowMore>
|
</ShowMore>
|
||||||
|
<Form action={greet} />
|
||||||
<div>
|
<div>
|
||||||
<Button action={like}>Like</Button>
|
<Button action={like}>Like</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,25 +1,26 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
|
import {experimental_useFormStatus as useFormStatus} from 'react-dom';
|
||||||
|
import ErrorBoundary from './ErrorBoundary.js';
|
||||||
|
|
||||||
export default function Button({action, children}) {
|
function ButtonDisabledWhilePending({action, children}) {
|
||||||
const [isPending, setIsPending] = React.useState(false);
|
const {pending} = useFormStatus();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button disabled={pending} formAction={action}>
|
||||||
disabled={isPending}
|
|
||||||
onClick={async () => {
|
|
||||||
setIsPending(true);
|
|
||||||
try {
|
|
||||||
const result = await action();
|
|
||||||
console.log(result);
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
} finally {
|
|
||||||
setIsPending(false);
|
|
||||||
}
|
|
||||||
}}>
|
|
||||||
{children}
|
{children}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default function Button({action, children}) {
|
||||||
|
return (
|
||||||
|
<ErrorBoundary>
|
||||||
|
<form>
|
||||||
|
<ButtonDisabledWhilePending action={action}>
|
||||||
|
{children}
|
||||||
|
</ButtonDisabledWhilePending>
|
||||||
|
</form>
|
||||||
|
</ErrorBoundary>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
|
export default class ErrorBoundary extends React.Component {
|
||||||
|
state = {error: null};
|
||||||
|
static getDerivedStateFromError(error) {
|
||||||
|
return {error};
|
||||||
|
}
|
||||||
|
render() {
|
||||||
|
if (this.state.error) {
|
||||||
|
return <div>Caught an error: {this.state.error.message}</div>;
|
||||||
|
}
|
||||||
|
return this.props.children;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import * as React from 'react';
|
||||||
|
import {experimental_useFormStatus as useFormStatus} from 'react-dom';
|
||||||
|
import ErrorBoundary from './ErrorBoundary.js';
|
||||||
|
|
||||||
|
function Status() {
|
||||||
|
const {pending} = useFormStatus();
|
||||||
|
return pending ? 'Saving...' : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Form({action, children}) {
|
||||||
|
const [isPending, setIsPending] = React.useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ErrorBoundary>
|
||||||
|
<form action={action}>
|
||||||
|
<label>
|
||||||
|
Name: <input name="name" />
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
File: <input type="file" name="file" />
|
||||||
|
</label>
|
||||||
|
<button>Say Hi</button>
|
||||||
|
<Status />
|
||||||
|
</form>
|
||||||
|
</ErrorBoundary>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
let serverState = 'Hello World';
|
||||||
|
|
||||||
|
export function setServerState(message) {
|
||||||
|
serverState = message;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getServerState() {
|
||||||
|
return serverState;
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,20 @@
|
||||||
'use server';
|
'use server';
|
||||||
|
|
||||||
|
import {setServerState} from './ServerState.js';
|
||||||
|
|
||||||
export async function like() {
|
export async function like() {
|
||||||
|
setServerState('Liked!');
|
||||||
return new Promise((resolve, reject) => resolve('Liked'));
|
return new Promise((resolve, reject) => resolve('Liked'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function greet(formData) {
|
||||||
|
const name = formData.get('name') || 'you';
|
||||||
|
setServerState('Hi ' + name);
|
||||||
|
const file = formData.get('file');
|
||||||
|
if (file) {
|
||||||
|
return `Ok, ${name}, here is ${file.name}:
|
||||||
|
${(await file.text()).toUpperCase()}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
return 'Hi ' + name + '!';
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,29 @@
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import {Suspense} from 'react';
|
import {use, Suspense, useState, startTransition} from 'react';
|
||||||
import ReactDOM from 'react-dom/client';
|
import ReactDOM from 'react-dom/client';
|
||||||
import {createFromFetch, encodeReply} from 'react-server-dom-webpack/client';
|
import {createFromFetch, encodeReply} from 'react-server-dom-webpack/client';
|
||||||
|
|
||||||
// TODO: This should be a dependency of the App but we haven't implemented CSS in Node yet.
|
// TODO: This should be a dependency of the App but we haven't implemented CSS in Node yet.
|
||||||
import './style.css';
|
import './style.css';
|
||||||
|
|
||||||
|
let updateRoot;
|
||||||
|
async function callServer(id, args) {
|
||||||
|
const response = fetch('/', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
Accept: 'text/x-component',
|
||||||
|
'rsc-action': id,
|
||||||
|
},
|
||||||
|
body: await encodeReply(args),
|
||||||
|
});
|
||||||
|
const {returnValue, root} = await createFromFetch(response, {callServer});
|
||||||
|
// Refresh the tree with the new RSC payload.
|
||||||
|
startTransition(() => {
|
||||||
|
updateRoot(root);
|
||||||
|
});
|
||||||
|
return returnValue;
|
||||||
|
}
|
||||||
|
|
||||||
let data = createFromFetch(
|
let data = createFromFetch(
|
||||||
fetch('/', {
|
fetch('/', {
|
||||||
headers: {
|
headers: {
|
||||||
|
|
@ -13,21 +31,14 @@ let data = createFromFetch(
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
async callServer(id, args) {
|
callServer,
|
||||||
const response = fetch('/', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
Accept: 'text/x-component',
|
|
||||||
'rsc-action': id,
|
|
||||||
},
|
|
||||||
body: await encodeReply(args),
|
|
||||||
});
|
|
||||||
return createFromFetch(response);
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
// TODO: This transition shouldn't really be necessary but it is for now.
|
function Shell({data}) {
|
||||||
React.startTransition(() => {
|
const [root, setRoot] = useState(use(data));
|
||||||
ReactDOM.hydrateRoot(document, data);
|
updateRoot = setRoot;
|
||||||
});
|
return root;
|
||||||
|
}
|
||||||
|
|
||||||
|
ReactDOM.hydrateRoot(document, <Shell data={data} />);
|
||||||
|
|
|
||||||
10
package.json
10
package.json
|
|
@ -65,8 +65,8 @@
|
||||||
"eslint-plugin-react-internal": "link:./scripts/eslint-rules",
|
"eslint-plugin-react-internal": "link:./scripts/eslint-rules",
|
||||||
"fbjs-scripts": "^3.0.1",
|
"fbjs-scripts": "^3.0.1",
|
||||||
"filesize": "^6.0.1",
|
"filesize": "^6.0.1",
|
||||||
"flow-bin": "^0.202.0",
|
"flow-bin": "^0.205.1",
|
||||||
"flow-remove-types": "^2.202.0",
|
"flow-remove-types": "^2.205.1",
|
||||||
"glob": "^7.1.6",
|
"glob": "^7.1.6",
|
||||||
"glob-stream": "^6.1.0",
|
"glob-stream": "^6.1.0",
|
||||||
"google-closure-compiler": "^20230206.0.0",
|
"google-closure-compiler": "^20230206.0.0",
|
||||||
|
|
@ -82,7 +82,6 @@
|
||||||
"minimist": "^1.2.3",
|
"minimist": "^1.2.3",
|
||||||
"mkdirp": "^0.5.1",
|
"mkdirp": "^0.5.1",
|
||||||
"ncp": "^2.0.0",
|
"ncp": "^2.0.0",
|
||||||
"pacote": "^10.3.0",
|
|
||||||
"prettier": "2.8.3",
|
"prettier": "2.8.3",
|
||||||
"pretty-format": "^29.4.1",
|
"pretty-format": "^29.4.1",
|
||||||
"prop-types": "^15.6.2",
|
"prop-types": "^15.6.2",
|
||||||
|
|
@ -93,6 +92,7 @@
|
||||||
"rollup-plugin-prettier": "^3.0.0",
|
"rollup-plugin-prettier": "^3.0.0",
|
||||||
"rollup-plugin-strip-banner": "^3.0.0",
|
"rollup-plugin-strip-banner": "^3.0.0",
|
||||||
"semver": "^7.1.1",
|
"semver": "^7.1.1",
|
||||||
|
"signedsource": "^2.0.0",
|
||||||
"targz": "^1.0.1",
|
"targz": "^1.0.1",
|
||||||
"through2": "^3.0.1",
|
"through2": "^3.0.1",
|
||||||
"tmp": "^0.1.0",
|
"tmp": "^0.1.0",
|
||||||
|
|
@ -102,7 +102,7 @@
|
||||||
"yargs": "^15.3.1"
|
"yargs": "^15.3.1"
|
||||||
},
|
},
|
||||||
"devEngines": {
|
"devEngines": {
|
||||||
"node": "16.x || 18.x || 19.x"
|
"node": "16.x || 18.x || 19.x || 20.x"
|
||||||
},
|
},
|
||||||
"jest": {
|
"jest": {
|
||||||
"testRegex": "/scripts/jest/dont-run-jest-directly\\.js$"
|
"testRegex": "/scripts/jest/dont-run-jest-directly\\.js$"
|
||||||
|
|
@ -117,7 +117,7 @@
|
||||||
"lint": "node ./scripts/tasks/eslint.js",
|
"lint": "node ./scripts/tasks/eslint.js",
|
||||||
"lint-build": "node ./scripts/rollup/validate/index.js",
|
"lint-build": "node ./scripts/rollup/validate/index.js",
|
||||||
"extract-errors": "node scripts/error-codes/extract-errors.js",
|
"extract-errors": "node scripts/error-codes/extract-errors.js",
|
||||||
"postinstall": "node node_modules/fbjs-scripts/node/check-dev-engines.js package.json && node ./scripts/flow/createFlowConfigs.js && node ./scripts/yarn/downloadReactIsForPrettyFormat.js",
|
"postinstall": "node node_modules/fbjs-scripts/node/check-dev-engines.js package.json && node ./scripts/flow/createFlowConfigs.js",
|
||||||
"debug-test": "yarn test --deprecated 'yarn test --debug'",
|
"debug-test": "yarn test --deprecated 'yarn test --debug'",
|
||||||
"test": "node ./scripts/jest/jest-cli.js",
|
"test": "node ./scripts/jest/jest-cli.js",
|
||||||
"test-stable": "node ./scripts/jest/jest-cli.js --release-channel=stable",
|
"test-stable": "node ./scripts/jest/jest-cli.js --release-channel=stable",
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ const ReactHooksESLintRule = ReactHooksESLintPlugin.rules['exhaustive-deps'];
|
||||||
function normalizeIndent(strings) {
|
function normalizeIndent(strings) {
|
||||||
const codeLines = strings[0].split('\n');
|
const codeLines = strings[0].split('\n');
|
||||||
const leftPadding = codeLines[1].match(/\s+/)[0];
|
const leftPadding = codeLines[1].match(/\s+/)[0];
|
||||||
return codeLines.map(line => line.substr(leftPadding.length)).join('\n');
|
return codeLines.map(line => line.slice(leftPadding.length)).join('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
// ***************************************************
|
// ***************************************************
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ ESLintTester.setDefaultConfig({
|
||||||
function normalizeIndent(strings) {
|
function normalizeIndent(strings) {
|
||||||
const codeLines = strings[0].split('\n');
|
const codeLines = strings[0].split('\n');
|
||||||
const leftPadding = codeLines[1].match(/\s+/)[0];
|
const leftPadding = codeLines[1].match(/\s+/)[0];
|
||||||
return codeLines.map(line => line.substr(leftPadding.length)).join('\n');
|
return codeLines.map(line => line.slice(leftPadding.length)).join('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
// ***************************************************
|
// ***************************************************
|
||||||
|
|
|
||||||
|
|
@ -1103,7 +1103,7 @@ export default {
|
||||||
extraWarning =
|
extraWarning =
|
||||||
` You can also do a functional update '${
|
` You can also do a functional update '${
|
||||||
setStateRecommendation.setter
|
setStateRecommendation.setter
|
||||||
}(${setStateRecommendation.missingDep.substring(
|
}(${setStateRecommendation.missingDep.slice(
|
||||||
0,
|
0,
|
||||||
1,
|
1,
|
||||||
)} => ...)' if you only need '${
|
)} => ...)' if you only need '${
|
||||||
|
|
|
||||||
|
|
@ -12,41 +12,40 @@ import enqueueTask from './enqueueTask';
|
||||||
|
|
||||||
export {act} from './internalAct';
|
export {act} from './internalAct';
|
||||||
|
|
||||||
function assertYieldsWereCleared(Scheduler) {
|
function assertYieldsWereCleared(caller) {
|
||||||
const actualYields = Scheduler.unstable_clearLog();
|
const actualYields = SchedulerMock.unstable_clearLog();
|
||||||
if (actualYields.length !== 0) {
|
if (actualYields.length !== 0) {
|
||||||
const error = Error(
|
const error = Error(
|
||||||
'The event log is not empty. Call assertLog(...) first.',
|
'The event log is not empty. Call assertLog(...) first.',
|
||||||
);
|
);
|
||||||
Error.captureStackTrace(error, assertYieldsWereCleared);
|
Error.captureStackTrace(error, caller);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function waitForMicrotasks() {
|
export async function waitForMicrotasks() {
|
||||||
return new Promise(resolve => {
|
return new Promise(resolve => {
|
||||||
enqueueTask(() => resolve());
|
enqueueTask(() => resolve());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function waitFor(expectedLog) {
|
export async function waitFor(expectedLog, options) {
|
||||||
assertYieldsWereCleared(SchedulerMock);
|
assertYieldsWereCleared(waitFor);
|
||||||
|
|
||||||
// Create the error object before doing any async work, to get a better
|
// Create the error object before doing any async work, to get a better
|
||||||
// stack trace.
|
// stack trace.
|
||||||
const error = new Error();
|
const error = new Error();
|
||||||
Error.captureStackTrace(error, waitFor);
|
Error.captureStackTrace(error, waitFor);
|
||||||
|
|
||||||
|
const stopAfter = expectedLog.length;
|
||||||
const actualLog = [];
|
const actualLog = [];
|
||||||
do {
|
do {
|
||||||
// Wait until end of current task/microtask.
|
// Wait until end of current task/microtask.
|
||||||
await waitForMicrotasks();
|
await waitForMicrotasks();
|
||||||
if (SchedulerMock.unstable_hasPendingWork()) {
|
if (SchedulerMock.unstable_hasPendingWork()) {
|
||||||
SchedulerMock.unstable_flushNumberOfYields(
|
SchedulerMock.unstable_flushNumberOfYields(stopAfter - actualLog.length);
|
||||||
expectedLog.length - actualLog.length,
|
|
||||||
);
|
|
||||||
actualLog.push(...SchedulerMock.unstable_clearLog());
|
actualLog.push(...SchedulerMock.unstable_clearLog());
|
||||||
if (expectedLog.length > actualLog.length) {
|
if (stopAfter > actualLog.length) {
|
||||||
// Continue flushing until we've logged the expected number of items.
|
// Continue flushing until we've logged the expected number of items.
|
||||||
} else {
|
} else {
|
||||||
// Once we've reached the expected sequence, wait one more microtask to
|
// Once we've reached the expected sequence, wait one more microtask to
|
||||||
|
|
@ -61,6 +60,12 @@ export async function waitFor(expectedLog) {
|
||||||
}
|
}
|
||||||
} while (true);
|
} while (true);
|
||||||
|
|
||||||
|
if (options && options.additionalLogsAfterAttemptingToYield) {
|
||||||
|
expectedLog = expectedLog.concat(
|
||||||
|
options.additionalLogsAfterAttemptingToYield,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (equals(actualLog, expectedLog)) {
|
if (equals(actualLog, expectedLog)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -74,7 +79,7 @@ ${diff(expectedLog, actualLog)}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function waitForAll(expectedLog) {
|
export async function waitForAll(expectedLog) {
|
||||||
assertYieldsWereCleared(SchedulerMock);
|
assertYieldsWereCleared(waitForAll);
|
||||||
|
|
||||||
// Create the error object before doing any async work, to get a better
|
// Create the error object before doing any async work, to get a better
|
||||||
// stack trace.
|
// stack trace.
|
||||||
|
|
@ -105,7 +110,7 @@ ${diff(expectedLog, actualLog)}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function waitForThrow(expectedError: mixed): mixed {
|
export async function waitForThrow(expectedError: mixed): mixed {
|
||||||
assertYieldsWereCleared(SchedulerMock);
|
assertYieldsWereCleared(waitForThrow);
|
||||||
|
|
||||||
// Create the error object before doing any async work, to get a better
|
// Create the error object before doing any async work, to get a better
|
||||||
// stack trace.
|
// stack trace.
|
||||||
|
|
@ -136,10 +141,18 @@ export async function waitForThrow(expectedError: mixed): mixed {
|
||||||
typeof expectedError === 'string' &&
|
typeof expectedError === 'string' &&
|
||||||
typeof x === 'object' &&
|
typeof x === 'object' &&
|
||||||
x !== null &&
|
x !== null &&
|
||||||
typeof x.message === 'string' &&
|
typeof x.message === 'string'
|
||||||
x.message.includes(expectedError)
|
|
||||||
) {
|
) {
|
||||||
return x;
|
if (x.message.includes(expectedError)) {
|
||||||
|
return x;
|
||||||
|
} else {
|
||||||
|
error.message = `
|
||||||
|
Expected error was not thrown.
|
||||||
|
|
||||||
|
${diff(expectedError, x.message)}
|
||||||
|
`;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
error.message = `
|
error.message = `
|
||||||
Expected error was not thrown.
|
Expected error was not thrown.
|
||||||
|
|
@ -151,12 +164,40 @@ ${diff(expectedError, x)}
|
||||||
} while (true);
|
} while (true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This is prefixed with `unstable_` because you should almost always try to
|
||||||
|
// avoid using it in tests. It's really only for testing a particular
|
||||||
|
// implementation detail (update starvation prevention).
|
||||||
|
export async function unstable_waitForExpired(expectedLog): mixed {
|
||||||
|
assertYieldsWereCleared(unstable_waitForExpired);
|
||||||
|
|
||||||
|
// Create the error object before doing any async work, to get a better
|
||||||
|
// stack trace.
|
||||||
|
const error = new Error();
|
||||||
|
Error.captureStackTrace(error, unstable_waitForExpired);
|
||||||
|
|
||||||
|
// Wait until end of current task/microtask.
|
||||||
|
await waitForMicrotasks();
|
||||||
|
SchedulerMock.unstable_flushExpired();
|
||||||
|
|
||||||
|
const actualLog = SchedulerMock.unstable_clearLog();
|
||||||
|
if (equals(actualLog, expectedLog)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
error.message = `
|
||||||
|
Expected sequence of events did not occur.
|
||||||
|
|
||||||
|
${diff(expectedLog, actualLog)}
|
||||||
|
`;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: This name is a bit misleading currently because it will stop as soon as
|
// TODO: This name is a bit misleading currently because it will stop as soon as
|
||||||
// React yields for any reason, not just for a paint. I've left it this way for
|
// React yields for any reason, not just for a paint. I've left it this way for
|
||||||
// now because that's how untable_flushUntilNextPaint already worked, but maybe
|
// now because that's how untable_flushUntilNextPaint already worked, but maybe
|
||||||
// we should split these use cases into separate APIs.
|
// we should split these use cases into separate APIs.
|
||||||
export async function waitForPaint(expectedLog) {
|
export async function waitForPaint(expectedLog) {
|
||||||
assertYieldsWereCleared(SchedulerMock);
|
assertYieldsWereCleared(waitForPaint);
|
||||||
|
|
||||||
// Create the error object before doing any async work, to get a better
|
// Create the error object before doing any async work, to get a better
|
||||||
// stack trace.
|
// stack trace.
|
||||||
|
|
@ -185,6 +226,30 @@ ${diff(expectedLog, actualLog)}
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function waitForDiscrete(expectedLog) {
|
||||||
|
assertYieldsWereCleared(waitForDiscrete);
|
||||||
|
|
||||||
|
// Create the error object before doing any async work, to get a better
|
||||||
|
// stack trace.
|
||||||
|
const error = new Error();
|
||||||
|
Error.captureStackTrace(error, waitForDiscrete);
|
||||||
|
|
||||||
|
// Wait until end of current task/microtask.
|
||||||
|
await waitForMicrotasks();
|
||||||
|
|
||||||
|
const actualLog = SchedulerMock.unstable_clearLog();
|
||||||
|
if (equals(actualLog, expectedLog)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
error.message = `
|
||||||
|
Expected sequence of events did not occur.
|
||||||
|
|
||||||
|
${diff(expectedLog, actualLog)}
|
||||||
|
`;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
export function assertLog(expectedLog) {
|
export function assertLog(expectedLog) {
|
||||||
const actualLog = SchedulerMock.unstable_clearLog();
|
const actualLog = SchedulerMock.unstable_clearLog();
|
||||||
if (equals(actualLog, expectedLog)) {
|
if (equals(actualLog, expectedLog)) {
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,7 @@ export async function act<T>(scope: () => Thenable<T>): Thenable<T> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// $FlowFixMe: Flow doesn't know about global Jest object
|
// $FlowFixMe[cannot-resolve-name]: Flow doesn't know about global Jest object
|
||||||
if (!jest.isMockFunction(setTimeout)) {
|
if (!jest.isMockFunction(setTimeout)) {
|
||||||
throw Error(
|
throw Error(
|
||||||
"This version of `act` requires Jest's timer mocks " +
|
"This version of `act` requires Jest's timer mocks " +
|
||||||
|
|
@ -70,7 +70,7 @@ export async function act<T>(scope: () => Thenable<T>): Thenable<T> {
|
||||||
// Wait until end of current task/microtask.
|
// Wait until end of current task/microtask.
|
||||||
await waitForMicrotasks();
|
await waitForMicrotasks();
|
||||||
|
|
||||||
// $FlowFixMe: Flow doesn't know about global Jest object
|
// $FlowFixMe[cannot-resolve-name]: Flow doesn't know about global Jest object
|
||||||
if (jest.isEnvironmentTornDown()) {
|
if (jest.isEnvironmentTornDown()) {
|
||||||
error.message =
|
error.message =
|
||||||
'The Jest environment was torn down before `act` completed. This ' +
|
'The Jest environment was torn down before `act` completed. This ' +
|
||||||
|
|
@ -79,8 +79,20 @@ export async function act<T>(scope: () => Thenable<T>): Thenable<T> {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!Scheduler.unstable_hasPendingWork()) {
|
if (!Scheduler.unstable_hasPendingWork()) {
|
||||||
// $FlowFixMe: Flow doesn't know about global Jest object
|
// $FlowFixMe[cannot-resolve-name]: Flow doesn't know about global Jest object
|
||||||
jest.runOnlyPendingTimers();
|
const j = jest;
|
||||||
|
if (j.getTimerCount() > 0) {
|
||||||
|
// There's a pending timer. Flush it now. We only do this in order to
|
||||||
|
// force Suspense fallbacks to display; the fact that it's a timer
|
||||||
|
// is an implementation detail. If there are other timers scheduled,
|
||||||
|
// those will also fire now, too, which is not ideal. (The public
|
||||||
|
// version of `act` doesn't do this.) For this reason, we should try
|
||||||
|
// to avoid using timers in our internal tests.
|
||||||
|
j.runOnlyPendingTimers();
|
||||||
|
// If a committing a fallback triggers another update, it might not
|
||||||
|
// get scheduled until a microtask. So wait one more time.
|
||||||
|
await waitForMicrotasks();
|
||||||
|
}
|
||||||
if (Scheduler.unstable_hasPendingWork()) {
|
if (Scheduler.unstable_hasPendingWork()) {
|
||||||
// Committing a fallback scheduled additional work. Continue flushing.
|
// Committing a fallback scheduled additional work. Continue flushing.
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -238,13 +238,13 @@ function applyTextProps(instance, props, prevProps = {}) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export * from 'react-reconciler/src/ReactFiberHostConfigWithNoPersistence';
|
export * from 'react-reconciler/src/ReactFiberConfigWithNoPersistence';
|
||||||
export * from 'react-reconciler/src/ReactFiberHostConfigWithNoHydration';
|
export * from 'react-reconciler/src/ReactFiberConfigWithNoHydration';
|
||||||
export * from 'react-reconciler/src/ReactFiberHostConfigWithNoScopes';
|
export * from 'react-reconciler/src/ReactFiberConfigWithNoScopes';
|
||||||
export * from 'react-reconciler/src/ReactFiberHostConfigWithNoTestSelectors';
|
export * from 'react-reconciler/src/ReactFiberConfigWithNoTestSelectors';
|
||||||
export * from 'react-reconciler/src/ReactFiberHostConfigWithNoMicrotasks';
|
export * from 'react-reconciler/src/ReactFiberConfigWithNoMicrotasks';
|
||||||
export * from 'react-reconciler/src/ReactFiberHostConfigWithNoResources';
|
export * from 'react-reconciler/src/ReactFiberConfigWithNoResources';
|
||||||
export * from 'react-reconciler/src/ReactFiberHostConfigWithNoSingletons';
|
export * from 'react-reconciler/src/ReactFiberConfigWithNoSingletons';
|
||||||
|
|
||||||
export function appendInitialChild(parentInstance, child) {
|
export function appendInitialChild(parentInstance, child) {
|
||||||
if (typeof child === 'string') {
|
if (typeof child === 'string') {
|
||||||
|
|
@ -346,6 +346,10 @@ export function getCurrentEventPriority() {
|
||||||
return DefaultEventPriority;
|
return DefaultEventPriority;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function shouldAttemptEagerTransition() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// The ART renderer is secondary to the React DOM renderer.
|
// The ART renderer is secondary to the React DOM renderer.
|
||||||
export const isPrimaryRenderer = false;
|
export const isPrimaryRenderer = false;
|
||||||
|
|
||||||
|
|
@ -475,11 +479,5 @@ export function suspendInstance(type, props) {}
|
||||||
export function waitForCommitToBeReady() {
|
export function waitForCommitToBeReady() {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line no-undef
|
|
||||||
export function prepareRendererToRender(container: Container): void {
|
|
||||||
// noop
|
|
||||||
}
|
|
||||||
|
|
||||||
export function resetRendererAfterRender(): void {
|
export const NotPendingTransition = null;
|
||||||
// noop
|
|
||||||
}
|
|
||||||
|
|
@ -33,12 +33,16 @@ const ReactTestRenderer = require('react-test-renderer');
|
||||||
|
|
||||||
// Isolate the noop renderer
|
// Isolate the noop renderer
|
||||||
jest.resetModules();
|
jest.resetModules();
|
||||||
|
const ReactNoop = require('react-noop-renderer');
|
||||||
|
const Scheduler = require('scheduler');
|
||||||
|
|
||||||
let Group;
|
let Group;
|
||||||
let Shape;
|
let Shape;
|
||||||
let Surface;
|
let Surface;
|
||||||
let TestComponent;
|
let TestComponent;
|
||||||
|
|
||||||
|
let waitFor;
|
||||||
|
|
||||||
const Missing = {};
|
const Missing = {};
|
||||||
|
|
||||||
function testDOMNodeStructure(domNode, expectedStructure) {
|
function testDOMNodeStructure(domNode, expectedStructure) {
|
||||||
|
|
@ -76,6 +80,8 @@ describe('ReactART', () => {
|
||||||
Shape = ReactART.Shape;
|
Shape = ReactART.Shape;
|
||||||
Surface = ReactART.Surface;
|
Surface = ReactART.Surface;
|
||||||
|
|
||||||
|
({waitFor} = require('internal-test-utils'));
|
||||||
|
|
||||||
TestComponent = class extends React.Component {
|
TestComponent = class extends React.Component {
|
||||||
group = React.createRef();
|
group = React.createRef();
|
||||||
|
|
||||||
|
|
@ -357,6 +363,58 @@ describe('ReactART', () => {
|
||||||
doClick(instance);
|
doClick(instance);
|
||||||
expect(onClick2).toBeCalled();
|
expect(onClick2).toBeCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// @gate !enableSyncDefaultUpdates
|
||||||
|
it('can concurrently render with a "primary" renderer while sharing context', async () => {
|
||||||
|
const CurrentRendererContext = React.createContext(null);
|
||||||
|
|
||||||
|
function Yield(props) {
|
||||||
|
Scheduler.log(props.value);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let ops = [];
|
||||||
|
function LogCurrentRenderer() {
|
||||||
|
return (
|
||||||
|
<CurrentRendererContext.Consumer>
|
||||||
|
{currentRenderer => {
|
||||||
|
ops.push(currentRenderer);
|
||||||
|
return null;
|
||||||
|
}}
|
||||||
|
</CurrentRendererContext.Consumer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Using test renderer instead of the DOM renderer here because async
|
||||||
|
// testing APIs for the DOM renderer don't exist.
|
||||||
|
ReactNoop.render(
|
||||||
|
<CurrentRendererContext.Provider value="Test">
|
||||||
|
<Yield value="A" />
|
||||||
|
<Yield value="B" />
|
||||||
|
<LogCurrentRenderer />
|
||||||
|
<Yield value="C" />
|
||||||
|
</CurrentRendererContext.Provider>,
|
||||||
|
);
|
||||||
|
|
||||||
|
await waitFor(['A']);
|
||||||
|
|
||||||
|
ReactDOM.render(
|
||||||
|
<Surface>
|
||||||
|
<LogCurrentRenderer />
|
||||||
|
<CurrentRendererContext.Provider value="ART">
|
||||||
|
<LogCurrentRenderer />
|
||||||
|
</CurrentRendererContext.Provider>
|
||||||
|
</Surface>,
|
||||||
|
container,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(ops).toEqual([null, 'ART']);
|
||||||
|
|
||||||
|
ops = [];
|
||||||
|
await waitFor(['B', 'C']);
|
||||||
|
|
||||||
|
expect(ops).toEqual(['Test']);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('ReactARTComponents', () => {
|
describe('ReactARTComponents', () => {
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ let textResourceShouldFail;
|
||||||
let waitForAll;
|
let waitForAll;
|
||||||
let assertLog;
|
let assertLog;
|
||||||
let waitForThrow;
|
let waitForThrow;
|
||||||
|
let act;
|
||||||
|
|
||||||
describe('ReactCache', () => {
|
describe('ReactCache', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
|
@ -40,6 +41,7 @@ describe('ReactCache', () => {
|
||||||
waitForAll = InternalTestUtils.waitForAll;
|
waitForAll = InternalTestUtils.waitForAll;
|
||||||
assertLog = InternalTestUtils.assertLog;
|
assertLog = InternalTestUtils.assertLog;
|
||||||
waitForThrow = InternalTestUtils.waitForThrow;
|
waitForThrow = InternalTestUtils.waitForThrow;
|
||||||
|
act = InternalTestUtils.act;
|
||||||
|
|
||||||
TextResource = createResource(
|
TextResource = createResource(
|
||||||
([text, ms = 0]) => {
|
([text, ms = 0]) => {
|
||||||
|
|
@ -145,11 +147,14 @@ describe('ReactCache', () => {
|
||||||
await waitForAll(['Suspend! [Hi]', 'Loading...']);
|
await waitForAll(['Suspend! [Hi]', 'Loading...']);
|
||||||
|
|
||||||
textResourceShouldFail = true;
|
textResourceShouldFail = true;
|
||||||
jest.advanceTimersByTime(100);
|
let error;
|
||||||
assertLog(['Promise rejected [Hi]']);
|
try {
|
||||||
|
await act(() => jest.advanceTimersByTime(100));
|
||||||
await waitForThrow('Failed to load: Hi');
|
} catch (e) {
|
||||||
assertLog(['Error! [Hi]', 'Error! [Hi]']);
|
error = e;
|
||||||
|
}
|
||||||
|
expect(error.message).toMatch('Failed to load: Hi');
|
||||||
|
assertLog(['Promise rejected [Hi]', 'Error! [Hi]', 'Error! [Hi]']);
|
||||||
|
|
||||||
// Should throw again on a subsequent read
|
// Should throw again on a subsequent read
|
||||||
root.update(<App />);
|
root.update(<App />);
|
||||||
|
|
@ -217,9 +222,8 @@ describe('ReactCache', () => {
|
||||||
assertLog(['Promise resolved [2]']);
|
assertLog(['Promise resolved [2]']);
|
||||||
await waitForAll([1, 2, 'Suspend! [3]']);
|
await waitForAll([1, 2, 'Suspend! [3]']);
|
||||||
|
|
||||||
jest.advanceTimersByTime(100);
|
await act(() => jest.advanceTimersByTime(100));
|
||||||
assertLog(['Promise resolved [3]']);
|
assertLog(['Promise resolved [3]', 1, 2, 3]);
|
||||||
await waitForAll([1, 2, 3]);
|
|
||||||
|
|
||||||
expect(root).toMatchRenderedOutput('123');
|
expect(root).toMatchRenderedOutput('123');
|
||||||
|
|
||||||
|
|
@ -234,13 +238,17 @@ describe('ReactCache', () => {
|
||||||
|
|
||||||
await waitForAll([1, 'Suspend! [4]', 'Loading...']);
|
await waitForAll([1, 'Suspend! [4]', 'Loading...']);
|
||||||
|
|
||||||
jest.advanceTimersByTime(100);
|
await act(() => jest.advanceTimersByTime(100));
|
||||||
assertLog(['Promise resolved [4]']);
|
assertLog([
|
||||||
await waitForAll([1, 4, 'Suspend! [5]', 'Loading...']);
|
'Promise resolved [4]',
|
||||||
|
1,
|
||||||
jest.advanceTimersByTime(100);
|
4,
|
||||||
assertLog(['Promise resolved [5]']);
|
'Suspend! [5]',
|
||||||
await waitForAll([1, 4, 5]);
|
'Promise resolved [5]',
|
||||||
|
1,
|
||||||
|
4,
|
||||||
|
5,
|
||||||
|
]);
|
||||||
|
|
||||||
expect(root).toMatchRenderedOutput('145');
|
expect(root).toMatchRenderedOutput('145');
|
||||||
|
|
||||||
|
|
@ -262,13 +270,18 @@ describe('ReactCache', () => {
|
||||||
'Suspend! [2]',
|
'Suspend! [2]',
|
||||||
'Loading...',
|
'Loading...',
|
||||||
]);
|
]);
|
||||||
jest.advanceTimersByTime(100);
|
|
||||||
assertLog(['Promise resolved [2]']);
|
|
||||||
await waitForAll([1, 2, 'Suspend! [3]', 'Loading...']);
|
|
||||||
|
|
||||||
jest.advanceTimersByTime(100);
|
await act(() => jest.advanceTimersByTime(100));
|
||||||
assertLog(['Promise resolved [3]']);
|
assertLog([
|
||||||
await waitForAll([1, 2, 3]);
|
'Promise resolved [2]',
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
'Suspend! [3]',
|
||||||
|
'Promise resolved [3]',
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
3,
|
||||||
|
]);
|
||||||
expect(root).toMatchRenderedOutput('123');
|
expect(root).toMatchRenderedOutput('123');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -291,9 +304,8 @@ describe('ReactCache', () => {
|
||||||
|
|
||||||
await waitForAll(['Loading...']);
|
await waitForAll(['Loading...']);
|
||||||
|
|
||||||
jest.advanceTimersByTime(1000);
|
await act(() => jest.advanceTimersByTime(1000));
|
||||||
assertLog(['Promise resolved [B]', 'Promise resolved [A]']);
|
assertLog(['Promise resolved [B]', 'Promise resolved [A]', 'Result']);
|
||||||
await waitForAll(['Result']);
|
|
||||||
expect(root).toMatchRenderedOutput('Result');
|
expect(root).toMatchRenderedOutput('Result');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,4 +7,4 @@
|
||||||
* @flow
|
* @flow
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export * from './src/ReactFlightClientStream';
|
export * from './src/ReactFlightClient';
|
||||||
|
|
|
||||||
|
|
@ -13,25 +13,37 @@ import type {LazyComponent} from 'react/src/ReactLazy';
|
||||||
import type {
|
import type {
|
||||||
ClientReference,
|
ClientReference,
|
||||||
ClientReferenceMetadata,
|
ClientReferenceMetadata,
|
||||||
UninitializedModel,
|
|
||||||
Response,
|
|
||||||
SSRManifest,
|
SSRManifest,
|
||||||
} from './ReactFlightClientHostConfig';
|
StringDecoder,
|
||||||
|
} from './ReactFlightClientConfig';
|
||||||
|
|
||||||
|
import type {HintModel} from 'react-server/src/ReactFlightServerConfig';
|
||||||
|
|
||||||
|
import type {CallServerCallback} from './ReactFlightReplyClient';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
resolveClientReference,
|
resolveClientReference,
|
||||||
preloadModule,
|
preloadModule,
|
||||||
requireModule,
|
requireModule,
|
||||||
parseModel,
|
dispatchHint,
|
||||||
} from './ReactFlightClientHostConfig';
|
readPartialStringChunk,
|
||||||
|
readFinalStringChunk,
|
||||||
|
supportsBinaryStreams,
|
||||||
|
createStringDecoder,
|
||||||
|
} from './ReactFlightClientConfig';
|
||||||
|
|
||||||
import {knownServerReferences} from './ReactFlightServerReferenceRegistry';
|
import {
|
||||||
|
encodeFormAction,
|
||||||
|
knownServerReferences,
|
||||||
|
} from './ReactFlightReplyClient';
|
||||||
|
|
||||||
import {REACT_LAZY_TYPE, REACT_ELEMENT_TYPE} from 'shared/ReactSymbols';
|
import {REACT_LAZY_TYPE, REACT_ELEMENT_TYPE} from 'shared/ReactSymbols';
|
||||||
|
|
||||||
import {getOrCreateServerContext} from 'shared/ReactServerContextRegistry';
|
import {getOrCreateServerContext} from 'shared/ReactServerContextRegistry';
|
||||||
|
|
||||||
export type CallServerCallback = <A, T>(id: any, args: A) => Promise<T>;
|
export type {CallServerCallback};
|
||||||
|
|
||||||
|
type UninitializedModel = string;
|
||||||
|
|
||||||
export type JSONValue =
|
export type JSONValue =
|
||||||
| number
|
| number
|
||||||
|
|
@ -150,15 +162,15 @@ Chunk.prototype.then = function <T>(
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ResponseBase = {
|
export type Response = {
|
||||||
_bundlerConfig: SSRManifest,
|
_bundlerConfig: SSRManifest,
|
||||||
_callServer: CallServerCallback,
|
_callServer: CallServerCallback,
|
||||||
_chunks: Map<number, SomeChunk<any>>,
|
_chunks: Map<number, SomeChunk<any>>,
|
||||||
...
|
_partialRow: string,
|
||||||
|
_fromJSON: (key: string, value: JSONValue) => any,
|
||||||
|
_stringDecoder: StringDecoder,
|
||||||
};
|
};
|
||||||
|
|
||||||
export type {Response};
|
|
||||||
|
|
||||||
function readChunk<T>(chunk: SomeChunk<T>): T {
|
function readChunk<T>(chunk: SomeChunk<T>): T {
|
||||||
// If we have resolved content, we try to initialize it first which
|
// If we have resolved content, we try to initialize it first which
|
||||||
// might put us back into one of the other states.
|
// might put us back into one of the other states.
|
||||||
|
|
@ -189,12 +201,12 @@ export function getRoot<T>(response: Response): Thenable<T> {
|
||||||
}
|
}
|
||||||
|
|
||||||
function createPendingChunk<T>(response: Response): PendingChunk<T> {
|
function createPendingChunk<T>(response: Response): PendingChunk<T> {
|
||||||
// $FlowFixMe Flow doesn't support functions as constructors
|
// $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors
|
||||||
return new Chunk(PENDING, null, null, response);
|
return new Chunk(PENDING, null, null, response);
|
||||||
}
|
}
|
||||||
|
|
||||||
function createBlockedChunk<T>(response: Response): BlockedChunk<T> {
|
function createBlockedChunk<T>(response: Response): BlockedChunk<T> {
|
||||||
// $FlowFixMe Flow doesn't support functions as constructors
|
// $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors
|
||||||
return new Chunk(BLOCKED, null, null, response);
|
return new Chunk(BLOCKED, null, null, response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -202,7 +214,7 @@ function createErrorChunk<T>(
|
||||||
response: Response,
|
response: Response,
|
||||||
error: ErrorWithDigest,
|
error: ErrorWithDigest,
|
||||||
): ErroredChunk<T> {
|
): ErroredChunk<T> {
|
||||||
// $FlowFixMe Flow doesn't support functions as constructors
|
// $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors
|
||||||
return new Chunk(ERRORED, null, error, response);
|
return new Chunk(ERRORED, null, error, response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -253,7 +265,7 @@ function createResolvedModelChunk<T>(
|
||||||
response: Response,
|
response: Response,
|
||||||
value: UninitializedModel,
|
value: UninitializedModel,
|
||||||
): ResolvedModelChunk<T> {
|
): ResolvedModelChunk<T> {
|
||||||
// $FlowFixMe Flow doesn't support functions as constructors
|
// $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors
|
||||||
return new Chunk(RESOLVED_MODEL, value, null, response);
|
return new Chunk(RESOLVED_MODEL, value, null, response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -261,7 +273,7 @@ function createResolvedModuleChunk<T>(
|
||||||
response: Response,
|
response: Response,
|
||||||
value: ClientReference<T>,
|
value: ClientReference<T>,
|
||||||
): ResolvedModuleChunk<T> {
|
): ResolvedModuleChunk<T> {
|
||||||
// $FlowFixMe Flow doesn't support functions as constructors
|
// $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors
|
||||||
return new Chunk(RESOLVED_MODULE, value, null, response);
|
return new Chunk(RESOLVED_MODULE, value, null, response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -497,11 +509,14 @@ function createServerReferenceProxy<A: Iterable<any>, T>(
|
||||||
return callServer(metaData.id, bound.concat(args));
|
return callServer(metaData.id, bound.concat(args));
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
// Expose encoder for use by SSR.
|
||||||
|
// TODO: Only expose this in SSR builds and not the browser client.
|
||||||
|
proxy.$$FORM_ACTION = encodeFormAction;
|
||||||
knownServerReferences.set(proxy, metaData);
|
knownServerReferences.set(proxy, metaData);
|
||||||
return proxy;
|
return proxy;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parseModelString(
|
function parseModelString(
|
||||||
response: Response,
|
response: Response,
|
||||||
parentObject: Object,
|
parentObject: Object,
|
||||||
key: string,
|
key: string,
|
||||||
|
|
@ -515,11 +530,11 @@ export function parseModelString(
|
||||||
switch (value[1]) {
|
switch (value[1]) {
|
||||||
case '$': {
|
case '$': {
|
||||||
// This was an escaped string value.
|
// This was an escaped string value.
|
||||||
return value.substring(1);
|
return value.slice(1);
|
||||||
}
|
}
|
||||||
case 'L': {
|
case 'L': {
|
||||||
// Lazy node
|
// Lazy node
|
||||||
const id = parseInt(value.substring(2), 16);
|
const id = parseInt(value.slice(2), 16);
|
||||||
const chunk = getChunk(response, id);
|
const chunk = getChunk(response, id);
|
||||||
// We create a React.lazy wrapper around any lazy values.
|
// We create a React.lazy wrapper around any lazy values.
|
||||||
// When passed into React, we'll know how to suspend on this.
|
// When passed into React, we'll know how to suspend on this.
|
||||||
|
|
@ -527,21 +542,21 @@ export function parseModelString(
|
||||||
}
|
}
|
||||||
case '@': {
|
case '@': {
|
||||||
// Promise
|
// Promise
|
||||||
const id = parseInt(value.substring(2), 16);
|
const id = parseInt(value.slice(2), 16);
|
||||||
const chunk = getChunk(response, id);
|
const chunk = getChunk(response, id);
|
||||||
return chunk;
|
return chunk;
|
||||||
}
|
}
|
||||||
case 'S': {
|
case 'S': {
|
||||||
// Symbol
|
// Symbol
|
||||||
return Symbol.for(value.substring(2));
|
return Symbol.for(value.slice(2));
|
||||||
}
|
}
|
||||||
case 'P': {
|
case 'P': {
|
||||||
// Server Context Provider
|
// Server Context Provider
|
||||||
return getOrCreateServerContext(value.substring(2)).Provider;
|
return getOrCreateServerContext(value.slice(2)).Provider;
|
||||||
}
|
}
|
||||||
case 'F': {
|
case 'F': {
|
||||||
// Server Reference
|
// Server Reference
|
||||||
const id = parseInt(value.substring(2), 16);
|
const id = parseInt(value.slice(2), 16);
|
||||||
const chunk = getChunk(response, id);
|
const chunk = getChunk(response, id);
|
||||||
switch (chunk.status) {
|
switch (chunk.status) {
|
||||||
case RESOLVED_MODEL:
|
case RESOLVED_MODEL:
|
||||||
|
|
@ -559,14 +574,38 @@ export function parseModelString(
|
||||||
throw chunk.reason;
|
throw chunk.reason;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
case 'I': {
|
||||||
|
// $Infinity
|
||||||
|
return Infinity;
|
||||||
|
}
|
||||||
|
case '-': {
|
||||||
|
// $-0 or $-Infinity
|
||||||
|
if (value === '$-0') {
|
||||||
|
return -0;
|
||||||
|
} else {
|
||||||
|
return -Infinity;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case 'N': {
|
||||||
|
// $NaN
|
||||||
|
return NaN;
|
||||||
|
}
|
||||||
case 'u': {
|
case 'u': {
|
||||||
// matches "$undefined"
|
// matches "$undefined"
|
||||||
// Special encoding for `undefined` which can't be serialized as JSON otherwise.
|
// Special encoding for `undefined` which can't be serialized as JSON otherwise.
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
case 'D': {
|
||||||
|
// Date
|
||||||
|
return new Date(Date.parse(value.slice(2)));
|
||||||
|
}
|
||||||
|
case 'n': {
|
||||||
|
// BigInt
|
||||||
|
return BigInt(value.slice(2));
|
||||||
|
}
|
||||||
default: {
|
default: {
|
||||||
// We assume that anything else is a reference ID.
|
// We assume that anything else is a reference ID.
|
||||||
const id = parseInt(value.substring(1), 16);
|
const id = parseInt(value.slice(1), 16);
|
||||||
const chunk = getChunk(response, id);
|
const chunk = getChunk(response, id);
|
||||||
switch (chunk.status) {
|
switch (chunk.status) {
|
||||||
case RESOLVED_MODEL:
|
case RESOLVED_MODEL:
|
||||||
|
|
@ -597,7 +636,7 @@ export function parseModelString(
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parseModelTuple(
|
function parseModelTuple(
|
||||||
response: Response,
|
response: Response,
|
||||||
value: {+[key: string]: JSONValue} | $ReadOnlyArray<JSONValue>,
|
value: {+[key: string]: JSONValue} | $ReadOnlyArray<JSONValue>,
|
||||||
): any {
|
): any {
|
||||||
|
|
@ -621,17 +660,25 @@ function missingCall() {
|
||||||
export function createResponse(
|
export function createResponse(
|
||||||
bundlerConfig: SSRManifest,
|
bundlerConfig: SSRManifest,
|
||||||
callServer: void | CallServerCallback,
|
callServer: void | CallServerCallback,
|
||||||
): ResponseBase {
|
): Response {
|
||||||
const chunks: Map<number, SomeChunk<any>> = new Map();
|
const chunks: Map<number, SomeChunk<any>> = new Map();
|
||||||
const response = {
|
const response: Response = {
|
||||||
_bundlerConfig: bundlerConfig,
|
_bundlerConfig: bundlerConfig,
|
||||||
_callServer: callServer !== undefined ? callServer : missingCall,
|
_callServer: callServer !== undefined ? callServer : missingCall,
|
||||||
_chunks: chunks,
|
_chunks: chunks,
|
||||||
|
_partialRow: '',
|
||||||
|
_stringDecoder: (null: any),
|
||||||
|
_fromJSON: (null: any),
|
||||||
};
|
};
|
||||||
|
if (supportsBinaryStreams) {
|
||||||
|
response._stringDecoder = createStringDecoder();
|
||||||
|
}
|
||||||
|
// Don't inline this call because it causes closure to outline the call above.
|
||||||
|
response._fromJSON = createFromJSONCallback(response);
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resolveModel(
|
function resolveModel(
|
||||||
response: Response,
|
response: Response,
|
||||||
id: number,
|
id: number,
|
||||||
model: UninitializedModel,
|
model: UninitializedModel,
|
||||||
|
|
@ -645,7 +692,7 @@ export function resolveModel(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resolveModule(
|
function resolveModule(
|
||||||
response: Response,
|
response: Response,
|
||||||
id: number,
|
id: number,
|
||||||
model: UninitializedModel,
|
model: UninitializedModel,
|
||||||
|
|
@ -694,7 +741,7 @@ export function resolveModule(
|
||||||
}
|
}
|
||||||
|
|
||||||
type ErrorWithDigest = Error & {digest?: string};
|
type ErrorWithDigest = Error & {digest?: string};
|
||||||
export function resolveErrorProd(
|
function resolveErrorProd(
|
||||||
response: Response,
|
response: Response,
|
||||||
id: number,
|
id: number,
|
||||||
digest: string,
|
digest: string,
|
||||||
|
|
@ -723,7 +770,7 @@ export function resolveErrorProd(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resolveErrorDev(
|
function resolveErrorDev(
|
||||||
response: Response,
|
response: Response,
|
||||||
id: number,
|
id: number,
|
||||||
digest: string,
|
digest: string,
|
||||||
|
|
@ -754,6 +801,114 @@ export function resolveErrorDev(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolveHint(
|
||||||
|
response: Response,
|
||||||
|
code: string,
|
||||||
|
model: UninitializedModel,
|
||||||
|
): void {
|
||||||
|
const hintModel = parseModel<HintModel>(response, model);
|
||||||
|
dispatchHint(code, hintModel);
|
||||||
|
}
|
||||||
|
|
||||||
|
function processFullRow(response: Response, row: string): void {
|
||||||
|
if (row === '') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const colon = row.indexOf(':', 0);
|
||||||
|
const id = parseInt(row.slice(0, colon), 16);
|
||||||
|
const tag = row[colon + 1];
|
||||||
|
// When tags that are not text are added, check them here before
|
||||||
|
// parsing the row as text.
|
||||||
|
// switch (tag) {
|
||||||
|
// }
|
||||||
|
switch (tag) {
|
||||||
|
case 'I': {
|
||||||
|
resolveModule(response, id, row.slice(colon + 2));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
case 'H': {
|
||||||
|
const code = row[colon + 2];
|
||||||
|
resolveHint(response, code, row.slice(colon + 3));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
case 'E': {
|
||||||
|
const errorInfo = JSON.parse(row.slice(colon + 2));
|
||||||
|
if (__DEV__) {
|
||||||
|
resolveErrorDev(
|
||||||
|
response,
|
||||||
|
id,
|
||||||
|
errorInfo.digest,
|
||||||
|
errorInfo.message,
|
||||||
|
errorInfo.stack,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
resolveErrorProd(response, id, errorInfo.digest);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
// We assume anything else is JSON.
|
||||||
|
resolveModel(response, id, row.slice(colon + 1));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function processStringChunk(
|
||||||
|
response: Response,
|
||||||
|
chunk: string,
|
||||||
|
offset: number,
|
||||||
|
): void {
|
||||||
|
let linebreak = chunk.indexOf('\n', offset);
|
||||||
|
while (linebreak > -1) {
|
||||||
|
const fullrow = response._partialRow + chunk.slice(offset, linebreak);
|
||||||
|
processFullRow(response, fullrow);
|
||||||
|
response._partialRow = '';
|
||||||
|
offset = linebreak + 1;
|
||||||
|
linebreak = chunk.indexOf('\n', offset);
|
||||||
|
}
|
||||||
|
response._partialRow += chunk.slice(offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function processBinaryChunk(
|
||||||
|
response: Response,
|
||||||
|
chunk: Uint8Array,
|
||||||
|
): void {
|
||||||
|
if (!supportsBinaryStreams) {
|
||||||
|
throw new Error("This environment don't support binary chunks.");
|
||||||
|
}
|
||||||
|
const stringDecoder = response._stringDecoder;
|
||||||
|
let linebreak = chunk.indexOf(10); // newline
|
||||||
|
while (linebreak > -1) {
|
||||||
|
const fullrow =
|
||||||
|
response._partialRow +
|
||||||
|
readFinalStringChunk(stringDecoder, chunk.subarray(0, linebreak));
|
||||||
|
processFullRow(response, fullrow);
|
||||||
|
response._partialRow = '';
|
||||||
|
chunk = chunk.subarray(linebreak + 1);
|
||||||
|
linebreak = chunk.indexOf(10); // newline
|
||||||
|
}
|
||||||
|
response._partialRow += readPartialStringChunk(stringDecoder, chunk);
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseModel<T>(response: Response, json: UninitializedModel): T {
|
||||||
|
return JSON.parse(json, response._fromJSON);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createFromJSONCallback(response: Response) {
|
||||||
|
// $FlowFixMe[missing-this-annot]
|
||||||
|
return function (key: string, value: JSONValue) {
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
// We can't use .bind here because we need the "this" value.
|
||||||
|
return parseModelString(response, this, key, value);
|
||||||
|
}
|
||||||
|
if (typeof value === 'object' && value !== null) {
|
||||||
|
return parseModelTuple(response, value);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function close(response: Response): void {
|
export function close(response: Response): void {
|
||||||
// In case there are any remaining unresolved chunks, they won't
|
// In case there are any remaining unresolved chunks, they won't
|
||||||
// be resolved now. So we need to issue an error to those.
|
// be resolved now. So we need to issue an error to those.
|
||||||
|
|
|
||||||
|
|
@ -1,33 +0,0 @@
|
||||||
/**
|
|
||||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
||||||
*
|
|
||||||
* This source code is licensed under the MIT license found in the
|
|
||||||
* LICENSE file in the root directory of this source tree.
|
|
||||||
*
|
|
||||||
* @flow
|
|
||||||
*/
|
|
||||||
|
|
||||||
export type StringDecoder = void;
|
|
||||||
|
|
||||||
export const supportsBinaryStreams = false;
|
|
||||||
|
|
||||||
export function createStringDecoder(): void {
|
|
||||||
// eslint-disable-next-line react-internal/prod-error-codes
|
|
||||||
throw new Error('Should never be called');
|
|
||||||
}
|
|
||||||
|
|
||||||
export function readPartialStringChunk(
|
|
||||||
decoder: StringDecoder,
|
|
||||||
buffer: Uint8Array,
|
|
||||||
): string {
|
|
||||||
// eslint-disable-next-line react-internal/prod-error-codes
|
|
||||||
throw new Error('Should never be called');
|
|
||||||
}
|
|
||||||
|
|
||||||
export function readFinalStringChunk(
|
|
||||||
decoder: StringDecoder,
|
|
||||||
buffer: Uint8Array,
|
|
||||||
): string {
|
|
||||||
// eslint-disable-next-line react-internal/prod-error-codes
|
|
||||||
throw new Error('Should never be called');
|
|
||||||
}
|
|
||||||
|
|
@ -1,23 +0,0 @@
|
||||||
/**
|
|
||||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
||||||
*
|
|
||||||
* This source code is licensed under the MIT license found in the
|
|
||||||
* LICENSE file in the root directory of this source tree.
|
|
||||||
*
|
|
||||||
* @flow
|
|
||||||
*/
|
|
||||||
|
|
||||||
import type {ResponseBase} from './ReactFlightClient';
|
|
||||||
import type {StringDecoder} from './ReactFlightClientHostConfig';
|
|
||||||
|
|
||||||
export type Response = ResponseBase & {
|
|
||||||
_partialRow: string,
|
|
||||||
_fromJSON: (key: string, value: JSONValue) => any,
|
|
||||||
_stringDecoder: StringDecoder,
|
|
||||||
};
|
|
||||||
|
|
||||||
export type UninitializedModel = string;
|
|
||||||
|
|
||||||
export function parseModel<T>(response: Response, json: UninitializedModel): T {
|
|
||||||
return JSON.parse(json, response._fromJSON);
|
|
||||||
}
|
|
||||||
|
|
@ -1,140 +0,0 @@
|
||||||
/**
|
|
||||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
||||||
*
|
|
||||||
* This source code is licensed under the MIT license found in the
|
|
||||||
* LICENSE file in the root directory of this source tree.
|
|
||||||
*
|
|
||||||
* @flow
|
|
||||||
*/
|
|
||||||
|
|
||||||
import type {CallServerCallback} from './ReactFlightClient';
|
|
||||||
import type {Response} from './ReactFlightClientHostConfigStream';
|
|
||||||
import type {SSRManifest} from './ReactFlightClientHostConfig';
|
|
||||||
|
|
||||||
import {
|
|
||||||
resolveModule,
|
|
||||||
resolveModel,
|
|
||||||
resolveErrorProd,
|
|
||||||
resolveErrorDev,
|
|
||||||
createResponse as createResponseBase,
|
|
||||||
parseModelString,
|
|
||||||
parseModelTuple,
|
|
||||||
} from './ReactFlightClient';
|
|
||||||
|
|
||||||
import {
|
|
||||||
readPartialStringChunk,
|
|
||||||
readFinalStringChunk,
|
|
||||||
supportsBinaryStreams,
|
|
||||||
createStringDecoder,
|
|
||||||
} from './ReactFlightClientHostConfig';
|
|
||||||
|
|
||||||
export type {Response};
|
|
||||||
|
|
||||||
function processFullRow(response: Response, row: string): void {
|
|
||||||
if (row === '') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const colon = row.indexOf(':', 0);
|
|
||||||
const id = parseInt(row.substring(0, colon), 16);
|
|
||||||
const tag = row[colon + 1];
|
|
||||||
// When tags that are not text are added, check them here before
|
|
||||||
// parsing the row as text.
|
|
||||||
// switch (tag) {
|
|
||||||
// }
|
|
||||||
switch (tag) {
|
|
||||||
case 'I': {
|
|
||||||
resolveModule(response, id, row.substring(colon + 2));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
case 'E': {
|
|
||||||
const errorInfo = JSON.parse(row.substring(colon + 2));
|
|
||||||
if (__DEV__) {
|
|
||||||
resolveErrorDev(
|
|
||||||
response,
|
|
||||||
id,
|
|
||||||
errorInfo.digest,
|
|
||||||
errorInfo.message,
|
|
||||||
errorInfo.stack,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
resolveErrorProd(response, id, errorInfo.digest);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
default: {
|
|
||||||
// We assume anything else is JSON.
|
|
||||||
resolveModel(response, id, row.substring(colon + 1));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function processStringChunk(
|
|
||||||
response: Response,
|
|
||||||
chunk: string,
|
|
||||||
offset: number,
|
|
||||||
): void {
|
|
||||||
let linebreak = chunk.indexOf('\n', offset);
|
|
||||||
while (linebreak > -1) {
|
|
||||||
const fullrow = response._partialRow + chunk.substring(offset, linebreak);
|
|
||||||
processFullRow(response, fullrow);
|
|
||||||
response._partialRow = '';
|
|
||||||
offset = linebreak + 1;
|
|
||||||
linebreak = chunk.indexOf('\n', offset);
|
|
||||||
}
|
|
||||||
response._partialRow += chunk.substring(offset);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function processBinaryChunk(
|
|
||||||
response: Response,
|
|
||||||
chunk: Uint8Array,
|
|
||||||
): void {
|
|
||||||
if (!supportsBinaryStreams) {
|
|
||||||
throw new Error("This environment don't support binary chunks.");
|
|
||||||
}
|
|
||||||
const stringDecoder = response._stringDecoder;
|
|
||||||
let linebreak = chunk.indexOf(10); // newline
|
|
||||||
while (linebreak > -1) {
|
|
||||||
const fullrow =
|
|
||||||
response._partialRow +
|
|
||||||
readFinalStringChunk(stringDecoder, chunk.subarray(0, linebreak));
|
|
||||||
processFullRow(response, fullrow);
|
|
||||||
response._partialRow = '';
|
|
||||||
chunk = chunk.subarray(linebreak + 1);
|
|
||||||
linebreak = chunk.indexOf(10); // newline
|
|
||||||
}
|
|
||||||
response._partialRow += readPartialStringChunk(stringDecoder, chunk);
|
|
||||||
}
|
|
||||||
|
|
||||||
function createFromJSONCallback(response: Response) {
|
|
||||||
// $FlowFixMe[missing-this-annot]
|
|
||||||
return function (key: string, value: JSONValue) {
|
|
||||||
if (typeof value === 'string') {
|
|
||||||
// We can't use .bind here because we need the "this" value.
|
|
||||||
return parseModelString(response, this, key, value);
|
|
||||||
}
|
|
||||||
if (typeof value === 'object' && value !== null) {
|
|
||||||
return parseModelTuple(response, value);
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createResponse(
|
|
||||||
bundlerConfig: SSRManifest,
|
|
||||||
callServer: void | CallServerCallback,
|
|
||||||
): Response {
|
|
||||||
// NOTE: CHECK THE COMPILER OUTPUT EACH TIME YOU CHANGE THIS.
|
|
||||||
// It should be inlined to one object literal but minor changes can break it.
|
|
||||||
const stringDecoder = supportsBinaryStreams ? createStringDecoder() : null;
|
|
||||||
const response: any = createResponseBase(bundlerConfig, callServer);
|
|
||||||
response._partialRow = '';
|
|
||||||
if (supportsBinaryStreams) {
|
|
||||||
response._stringDecoder = stringDecoder;
|
|
||||||
}
|
|
||||||
// Don't inline this call because it causes closure to outline the call above.
|
|
||||||
response._fromJSON = createFromJSONCallback(response);
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
|
|
||||||
export {reportGlobalError, getRoot, close} from './ReactFlightClient';
|
|
||||||
|
|
@ -7,9 +7,7 @@
|
||||||
* @flow
|
* @flow
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type {Thenable} from 'shared/ReactTypes';
|
import type {Thenable, ReactCustomFormAction} from 'shared/ReactTypes';
|
||||||
|
|
||||||
import {knownServerReferences} from './ReactFlightServerReferenceRegistry';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
REACT_ELEMENT_TYPE,
|
REACT_ELEMENT_TYPE,
|
||||||
|
|
@ -25,6 +23,10 @@ import {
|
||||||
} from 'shared/ReactSerializationErrors';
|
} from 'shared/ReactSerializationErrors';
|
||||||
|
|
||||||
import isArray from 'shared/isArray';
|
import isArray from 'shared/isArray';
|
||||||
|
import type {
|
||||||
|
FulfilledThenable,
|
||||||
|
RejectedThenable,
|
||||||
|
} from '../../shared/ReactTypes';
|
||||||
|
|
||||||
type ReactJSONValue =
|
type ReactJSONValue =
|
||||||
| string
|
| string
|
||||||
|
|
@ -36,6 +38,15 @@ type ReactJSONValue =
|
||||||
|
|
||||||
export opaque type ServerReference<T> = T;
|
export opaque type ServerReference<T> = T;
|
||||||
|
|
||||||
|
export type CallServerCallback = <A, T>(id: any, args: A) => Promise<T>;
|
||||||
|
|
||||||
|
export type ServerReferenceId = any;
|
||||||
|
|
||||||
|
export const knownServerReferences: WeakMap<
|
||||||
|
Function,
|
||||||
|
{id: ServerReferenceId, bound: null | Thenable<Array<any>>},
|
||||||
|
> = new WeakMap();
|
||||||
|
|
||||||
// Serializable values
|
// Serializable values
|
||||||
export type ReactServerValue =
|
export type ReactServerValue =
|
||||||
// References are passed by their value
|
// References are passed by their value
|
||||||
|
|
@ -71,10 +82,43 @@ function serializeSymbolReference(name: string): string {
|
||||||
return '$S' + name;
|
return '$S' + name;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function serializeFormDataReference(id: number): string {
|
||||||
|
// Why K? F is "Function". D is "Date". What else?
|
||||||
|
return '$K' + id.toString(16);
|
||||||
|
}
|
||||||
|
|
||||||
|
function serializeNumber(number: number): string | number {
|
||||||
|
if (Number.isFinite(number)) {
|
||||||
|
if (number === 0 && 1 / number === -Infinity) {
|
||||||
|
return '$-0';
|
||||||
|
} else {
|
||||||
|
return number;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (number === Infinity) {
|
||||||
|
return '$Infinity';
|
||||||
|
} else if (number === -Infinity) {
|
||||||
|
return '$-Infinity';
|
||||||
|
} else {
|
||||||
|
return '$NaN';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function serializeUndefined(): string {
|
function serializeUndefined(): string {
|
||||||
return '$undefined';
|
return '$undefined';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function serializeDateFromDateJSON(dateJSON: string): string {
|
||||||
|
// JSON.stringify automatically calls Date.prototype.toJSON which calls toISOString.
|
||||||
|
// We need only tack on a $D prefix.
|
||||||
|
return '$D' + dateJSON;
|
||||||
|
}
|
||||||
|
|
||||||
|
function serializeBigInt(n: bigint): string {
|
||||||
|
return '$n' + n.toString(10);
|
||||||
|
}
|
||||||
|
|
||||||
function escapeStringValue(value: string): string {
|
function escapeStringValue(value: string): string {
|
||||||
if (value[0] === '$') {
|
if (value[0] === '$') {
|
||||||
// We need to escape $ prefixed strings since we use those to encode
|
// We need to escape $ prefixed strings since we use those to encode
|
||||||
|
|
@ -87,6 +131,7 @@ function escapeStringValue(value: string): string {
|
||||||
|
|
||||||
export function processReply(
|
export function processReply(
|
||||||
root: ReactServerValue,
|
root: ReactServerValue,
|
||||||
|
formFieldPrefix: string,
|
||||||
resolve: (string | FormData) => void,
|
resolve: (string | FormData) => void,
|
||||||
reject: (error: mixed) => void,
|
reject: (error: mixed) => void,
|
||||||
): void {
|
): void {
|
||||||
|
|
@ -102,10 +147,16 @@ export function processReply(
|
||||||
value: ReactServerValue,
|
value: ReactServerValue,
|
||||||
): ReactJSONValue {
|
): ReactJSONValue {
|
||||||
const parent = this;
|
const parent = this;
|
||||||
|
|
||||||
|
// Make sure that `parent[key]` wasn't JSONified before `value` was passed to us
|
||||||
if (__DEV__) {
|
if (__DEV__) {
|
||||||
// $FlowFixMe
|
// $FlowFixMe[incompatible-use]
|
||||||
const originalValue = this[key];
|
const originalValue = parent[key];
|
||||||
if (typeof originalValue === 'object' && originalValue !== value) {
|
if (
|
||||||
|
typeof originalValue === 'object' &&
|
||||||
|
originalValue !== value &&
|
||||||
|
!(originalValue instanceof Date)
|
||||||
|
) {
|
||||||
if (objectName(originalValue) !== 'Object') {
|
if (objectName(originalValue) !== 'Object') {
|
||||||
console.error(
|
console.error(
|
||||||
'Only plain objects can be passed to Server Functions from the Client. ' +
|
'Only plain objects can be passed to Server Functions from the Client. ' +
|
||||||
|
|
@ -146,7 +197,7 @@ export function processReply(
|
||||||
// $FlowFixMe[incompatible-type] We know it's not null because we assigned it above.
|
// $FlowFixMe[incompatible-type] We know it's not null because we assigned it above.
|
||||||
const data: FormData = formData;
|
const data: FormData = formData;
|
||||||
// eslint-disable-next-line react-internal/safe-string-coercion
|
// eslint-disable-next-line react-internal/safe-string-coercion
|
||||||
data.append('' + promiseId, partJSON);
|
data.append(formFieldPrefix + promiseId, partJSON);
|
||||||
pendingParts--;
|
pendingParts--;
|
||||||
if (pendingParts === 0) {
|
if (pendingParts === 0) {
|
||||||
resolve(data);
|
resolve(data);
|
||||||
|
|
@ -160,6 +211,24 @@ export function processReply(
|
||||||
);
|
);
|
||||||
return serializePromiseID(promiseId);
|
return serializePromiseID(promiseId);
|
||||||
}
|
}
|
||||||
|
// TODO: Should we the Object.prototype.toString.call() to test for cross-realm objects?
|
||||||
|
if (value instanceof FormData) {
|
||||||
|
if (formData === null) {
|
||||||
|
// Upgrade to use FormData to allow us to use rich objects as its values.
|
||||||
|
formData = new FormData();
|
||||||
|
}
|
||||||
|
const data: FormData = formData;
|
||||||
|
const refId = nextPartId++;
|
||||||
|
// Copy all the form fields with a prefix for this reference.
|
||||||
|
// These must come first in the form order because we assume that all the
|
||||||
|
// fields are available before this is referenced.
|
||||||
|
const prefix = formFieldPrefix + refId + '_';
|
||||||
|
// $FlowFixMe[prop-missing]: FormData has forEach.
|
||||||
|
value.forEach((originalValue: string | File, originalKey: string) => {
|
||||||
|
data.append(prefix + originalKey, originalValue);
|
||||||
|
});
|
||||||
|
return serializeFormDataReference(refId);
|
||||||
|
}
|
||||||
if (!isArray(value)) {
|
if (!isArray(value)) {
|
||||||
const iteratorFn = getIteratorFn(value);
|
const iteratorFn = getIteratorFn(value);
|
||||||
if (iteratorFn) {
|
if (iteratorFn) {
|
||||||
|
|
@ -212,18 +281,32 @@ export function processReply(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// $FlowFixMe
|
// $FlowFixMe[incompatible-return]
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof value === 'string') {
|
if (typeof value === 'string') {
|
||||||
|
// TODO: Maybe too clever. If we support URL there's no similar trick.
|
||||||
|
if (value[value.length - 1] === 'Z') {
|
||||||
|
// Possibly a Date, whose toJSON automatically calls toISOString
|
||||||
|
// $FlowFixMe[incompatible-use]
|
||||||
|
const originalValue = parent[key];
|
||||||
|
if (originalValue instanceof Date) {
|
||||||
|
return serializeDateFromDateJSON(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return escapeStringValue(value);
|
return escapeStringValue(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof value === 'boolean' || typeof value === 'number') {
|
if (typeof value === 'boolean') {
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (typeof value === 'number') {
|
||||||
|
return serializeNumber(value);
|
||||||
|
}
|
||||||
|
|
||||||
if (typeof value === 'undefined') {
|
if (typeof value === 'undefined') {
|
||||||
return serializeUndefined();
|
return serializeUndefined();
|
||||||
}
|
}
|
||||||
|
|
@ -239,7 +322,7 @@ export function processReply(
|
||||||
// The reference to this function came from the same client so we can pass it back.
|
// The reference to this function came from the same client so we can pass it back.
|
||||||
const refId = nextPartId++;
|
const refId = nextPartId++;
|
||||||
// eslint-disable-next-line react-internal/safe-string-coercion
|
// eslint-disable-next-line react-internal/safe-string-coercion
|
||||||
formData.set('' + refId, metaDataJSON);
|
formData.set(formFieldPrefix + refId, metaDataJSON);
|
||||||
return serializeServerReferenceID(refId);
|
return serializeServerReferenceID(refId);
|
||||||
}
|
}
|
||||||
throw new Error(
|
throw new Error(
|
||||||
|
|
@ -249,13 +332,13 @@ export function processReply(
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof value === 'symbol') {
|
if (typeof value === 'symbol') {
|
||||||
// $FlowFixMe `description` might be undefined
|
// $FlowFixMe[incompatible-type] `description` might be undefined
|
||||||
const name: string = value.description;
|
const name: string = value.description;
|
||||||
if (Symbol.for(name) !== value) {
|
if (Symbol.for(name) !== value) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'Only global symbols received from Symbol.for(...) can be passed to Server Functions. ' +
|
'Only global symbols received from Symbol.for(...) can be passed to Server Functions. ' +
|
||||||
`The symbol Symbol.for(${
|
`The symbol Symbol.for(${
|
||||||
// $FlowFixMe `description` might be undefined
|
// $FlowFixMe[incompatible-type] `description` might be undefined
|
||||||
value.description
|
value.description
|
||||||
}) cannot be found among global symbols.`,
|
}) cannot be found among global symbols.`,
|
||||||
);
|
);
|
||||||
|
|
@ -264,9 +347,7 @@ export function processReply(
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof value === 'bigint') {
|
if (typeof value === 'bigint') {
|
||||||
throw new Error(
|
return serializeBigInt(value);
|
||||||
`BigInt (${value}) is not yet supported as an argument to a Server Function.`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error(
|
throw new Error(
|
||||||
|
|
@ -281,10 +362,112 @@ export function processReply(
|
||||||
resolve(json);
|
resolve(json);
|
||||||
} else {
|
} else {
|
||||||
// Otherwise, we use FormData to let us stream in the result.
|
// Otherwise, we use FormData to let us stream in the result.
|
||||||
formData.set('0', json);
|
formData.set(formFieldPrefix + '0', json);
|
||||||
if (pendingParts === 0) {
|
if (pendingParts === 0) {
|
||||||
// $FlowFixMe[incompatible-call] this has already been refined.
|
// $FlowFixMe[incompatible-call] this has already been refined.
|
||||||
resolve(formData);
|
resolve(formData);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const boundCache: WeakMap<
|
||||||
|
{id: ServerReferenceId, bound: null | Thenable<Array<any>>},
|
||||||
|
Thenable<FormData>,
|
||||||
|
> = new WeakMap();
|
||||||
|
|
||||||
|
function encodeFormData(reference: any): Thenable<FormData> {
|
||||||
|
let resolve, reject;
|
||||||
|
// We need to have a handle on the thenable so that we can synchronously set
|
||||||
|
// its status from processReply, when it can complete synchronously.
|
||||||
|
const thenable: Thenable<FormData> = new Promise((res, rej) => {
|
||||||
|
resolve = res;
|
||||||
|
reject = rej;
|
||||||
|
});
|
||||||
|
processReply(
|
||||||
|
reference,
|
||||||
|
'',
|
||||||
|
(body: string | FormData) => {
|
||||||
|
if (typeof body === 'string') {
|
||||||
|
const data = new FormData();
|
||||||
|
data.append('0', body);
|
||||||
|
body = data;
|
||||||
|
}
|
||||||
|
const fulfilled: FulfilledThenable<FormData> = (thenable: any);
|
||||||
|
fulfilled.status = 'fulfilled';
|
||||||
|
fulfilled.value = body;
|
||||||
|
resolve(body);
|
||||||
|
},
|
||||||
|
e => {
|
||||||
|
const rejected: RejectedThenable<FormData> = (thenable: any);
|
||||||
|
rejected.status = 'rejected';
|
||||||
|
rejected.reason = e;
|
||||||
|
reject(e);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return thenable;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function encodeFormAction(
|
||||||
|
this: any => Promise<any>,
|
||||||
|
identifierPrefix: string,
|
||||||
|
): ReactCustomFormAction {
|
||||||
|
const reference = knownServerReferences.get(this);
|
||||||
|
if (!reference) {
|
||||||
|
throw new Error(
|
||||||
|
'Tried to encode a Server Action from a different instance than the encoder is from. ' +
|
||||||
|
'This is a bug in React.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let data: null | FormData = null;
|
||||||
|
let name;
|
||||||
|
const boundPromise = reference.bound;
|
||||||
|
if (boundPromise !== null) {
|
||||||
|
let thenable = boundCache.get(reference);
|
||||||
|
if (!thenable) {
|
||||||
|
thenable = encodeFormData(reference);
|
||||||
|
boundCache.set(reference, thenable);
|
||||||
|
}
|
||||||
|
if (thenable.status === 'rejected') {
|
||||||
|
throw thenable.reason;
|
||||||
|
} else if (thenable.status !== 'fulfilled') {
|
||||||
|
throw thenable;
|
||||||
|
}
|
||||||
|
const encodedFormData = thenable.value;
|
||||||
|
// This is hacky but we need the identifier prefix to be added to
|
||||||
|
// all fields but the suspense cache would break since we might get
|
||||||
|
// a new identifier each time. So we just append it at the end instead.
|
||||||
|
const prefixedData = new FormData();
|
||||||
|
// $FlowFixMe[prop-missing]
|
||||||
|
encodedFormData.forEach((value: string | File, key: string) => {
|
||||||
|
prefixedData.append('$ACTION_' + identifierPrefix + ':' + key, value);
|
||||||
|
});
|
||||||
|
data = prefixedData;
|
||||||
|
// We encode the name of the prefix containing the data.
|
||||||
|
name = '$ACTION_REF_' + identifierPrefix;
|
||||||
|
} else {
|
||||||
|
// This is the simple case so we can just encode the ID.
|
||||||
|
name = '$ACTION_ID_' + reference.id;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
name: name,
|
||||||
|
method: 'POST',
|
||||||
|
encType: 'multipart/form-data',
|
||||||
|
data: data,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createServerReference<A: Iterable<any>, T>(
|
||||||
|
id: ServerReferenceId,
|
||||||
|
callServer: CallServerCallback,
|
||||||
|
): (...A) => Promise<T> {
|
||||||
|
const proxy = function (): Promise<T> {
|
||||||
|
// $FlowFixMe[method-unbinding]
|
||||||
|
const args = Array.prototype.slice.call(arguments);
|
||||||
|
return callServer(id, args);
|
||||||
|
};
|
||||||
|
// Expose encoder for use by SSR.
|
||||||
|
// TODO: Only expose this in SSR builds and not the browser client.
|
||||||
|
proxy.$$FORM_ACTION = encodeFormAction;
|
||||||
|
knownServerReferences.set(proxy, {id: id, bound: null});
|
||||||
|
return proxy;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
/**
|
|
||||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
||||||
*
|
|
||||||
* This source code is licensed under the MIT license found in the
|
|
||||||
* LICENSE file in the root directory of this source tree.
|
|
||||||
*
|
|
||||||
* @flow
|
|
||||||
*/
|
|
||||||
|
|
||||||
import type {Thenable} from 'shared/ReactTypes';
|
|
||||||
|
|
||||||
type ServerReferenceId = any;
|
|
||||||
|
|
||||||
export const knownServerReferences: WeakMap<
|
|
||||||
Function,
|
|
||||||
{id: ServerReferenceId, bound: null | Thenable<Array<any>>},
|
|
||||||
> = new WeakMap();
|
|
||||||
|
|
@ -213,6 +213,40 @@ describe('ReactFlight', () => {
|
||||||
expect(ReactNoop).toMatchRenderedOutput(null);
|
expect(ReactNoop).toMatchRenderedOutput(null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// @gate FIXME
|
||||||
|
it('should transport undefined object values', async () => {
|
||||||
|
function ServerComponent(props) {
|
||||||
|
return 'prop' in props
|
||||||
|
? `\`prop\` in props as '${props.prop}'`
|
||||||
|
: '`prop` not in props';
|
||||||
|
}
|
||||||
|
const ClientComponent = clientReference(ServerComponent);
|
||||||
|
|
||||||
|
const model = (
|
||||||
|
<>
|
||||||
|
<div>
|
||||||
|
Server: <ServerComponent prop={undefined} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
Client: <ClientComponent prop={undefined} />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
const transport = ReactNoopFlightServer.render(model);
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
ReactNoop.render(await ReactNoopFlightClient.read(transport));
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(ReactNoop).toMatchRenderedOutput(
|
||||||
|
<>
|
||||||
|
<div>Server: `prop` in props as 'undefined'</div>
|
||||||
|
<div>Client: `prop` in props as 'undefined'</div>
|
||||||
|
</>,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it('can render an empty fragment', async () => {
|
it('can render an empty fragment', async () => {
|
||||||
function Empty() {
|
function Empty() {
|
||||||
return <React.Fragment />;
|
return <React.Fragment />;
|
||||||
|
|
@ -229,6 +263,66 @@ describe('ReactFlight', () => {
|
||||||
expect(ReactNoop).toMatchRenderedOutput(null);
|
expect(ReactNoop).toMatchRenderedOutput(null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('can transport weird numbers', async () => {
|
||||||
|
const nums = [0, -0, Infinity, -Infinity, NaN];
|
||||||
|
function ComponentClient({prop}) {
|
||||||
|
expect(prop).not.toBe(nums);
|
||||||
|
expect(prop).toEqual(nums);
|
||||||
|
expect(prop.every((p, i) => Object.is(p, nums[i]))).toBe(true);
|
||||||
|
return `prop: ${prop}`;
|
||||||
|
}
|
||||||
|
const Component = clientReference(ComponentClient);
|
||||||
|
|
||||||
|
const model = <Component prop={nums} />;
|
||||||
|
|
||||||
|
const transport = ReactNoopFlightServer.render(model);
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
ReactNoop.render(await ReactNoopFlightClient.read(transport));
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(ReactNoop).toMatchRenderedOutput(
|
||||||
|
// already checked -0 with expects above
|
||||||
|
'prop: 0,0,Infinity,-Infinity,NaN',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can transport BigInt', async () => {
|
||||||
|
function ComponentClient({prop}) {
|
||||||
|
return `prop: ${prop} (${typeof prop})`;
|
||||||
|
}
|
||||||
|
const Component = clientReference(ComponentClient);
|
||||||
|
|
||||||
|
const model = <Component prop={90071992547409910000n} />;
|
||||||
|
|
||||||
|
const transport = ReactNoopFlightServer.render(model);
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
ReactNoop.render(await ReactNoopFlightClient.read(transport));
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(ReactNoop).toMatchRenderedOutput(
|
||||||
|
'prop: 90071992547409910000 (bigint)',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can transport Date', async () => {
|
||||||
|
function ComponentClient({prop}) {
|
||||||
|
return `prop: ${prop.toISOString()}`;
|
||||||
|
}
|
||||||
|
const Component = clientReference(ComponentClient);
|
||||||
|
|
||||||
|
const model = <Component prop={new Date(1234567890123)} />;
|
||||||
|
|
||||||
|
const transport = ReactNoopFlightServer.render(model);
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
ReactNoop.render(await ReactNoopFlightClient.read(transport));
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(ReactNoop).toMatchRenderedOutput('prop: 2009-02-13T23:31:30.123Z');
|
||||||
|
});
|
||||||
|
|
||||||
it('can render a lazy component as a shared component on the server', async () => {
|
it('can render a lazy component as a shared component on the server', async () => {
|
||||||
function SharedComponent({text}) {
|
function SharedComponent({text}) {
|
||||||
return (
|
return (
|
||||||
|
|
@ -436,7 +530,6 @@ describe('ReactFlight', () => {
|
||||||
expect(ReactNoop).toMatchRenderedOutput(<div>I am client</div>);
|
expect(ReactNoop).toMatchRenderedOutput(<div>I am client</div>);
|
||||||
});
|
});
|
||||||
|
|
||||||
// @gate enableUseHook
|
|
||||||
it('should error if a non-serializable value is passed to a host component', async () => {
|
it('should error if a non-serializable value is passed to a host component', async () => {
|
||||||
function ClientImpl({children}) {
|
function ClientImpl({children}) {
|
||||||
return children;
|
return children;
|
||||||
|
|
@ -547,7 +640,6 @@ describe('ReactFlight', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// @gate enableUseHook
|
|
||||||
it('should trigger the inner most error boundary inside a Client Component', async () => {
|
it('should trigger the inner most error boundary inside a Client Component', async () => {
|
||||||
function ServerComponent() {
|
function ServerComponent() {
|
||||||
throw new Error('This was thrown in the Server Component.');
|
throw new Error('This was thrown in the Server Component.');
|
||||||
|
|
@ -598,28 +690,39 @@ describe('ReactFlight', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should warn in DEV if a toJSON instance is passed to a host component', () => {
|
it('should warn in DEV if a toJSON instance is passed to a host component', () => {
|
||||||
|
const obj = {
|
||||||
|
toJSON() {
|
||||||
|
return 123;
|
||||||
|
},
|
||||||
|
};
|
||||||
expect(() => {
|
expect(() => {
|
||||||
const transport = ReactNoopFlightServer.render(
|
const transport = ReactNoopFlightServer.render(<input value={obj} />);
|
||||||
<input value={new Date()} />,
|
|
||||||
);
|
|
||||||
ReactNoopFlightClient.read(transport);
|
ReactNoopFlightClient.read(transport);
|
||||||
}).toErrorDev(
|
}).toErrorDev(
|
||||||
'Only plain objects can be passed to Client Components from Server Components. ' +
|
'Only plain objects can be passed to Client Components from Server Components. ' +
|
||||||
'Date objects are not supported.',
|
'Objects with toJSON methods are not supported. ' +
|
||||||
|
'Convert it manually to a simple value before passing it to props.\n' +
|
||||||
|
' <input value={{toJSON: function}}>\n' +
|
||||||
|
' ^^^^^^^^^^^^^^^^^^^^',
|
||||||
{withoutStack: true},
|
{withoutStack: true},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should warn in DEV if a toJSON instance is passed to a host component child', () => {
|
it('should warn in DEV if a toJSON instance is passed to a host component child', () => {
|
||||||
|
class MyError extends Error {
|
||||||
|
toJSON() {
|
||||||
|
return 123;
|
||||||
|
}
|
||||||
|
}
|
||||||
expect(() => {
|
expect(() => {
|
||||||
const transport = ReactNoopFlightServer.render(
|
const transport = ReactNoopFlightServer.render(
|
||||||
<div>Current date: {new Date()}</div>,
|
<div>Womp womp: {new MyError('spaghetti')}</div>,
|
||||||
);
|
);
|
||||||
ReactNoopFlightClient.read(transport);
|
ReactNoopFlightClient.read(transport);
|
||||||
}).toErrorDev(
|
}).toErrorDev(
|
||||||
'Date objects cannot be rendered as text children. Try formatting it using toString().\n' +
|
'Error objects cannot be rendered as text children. Try formatting it using toString().\n' +
|
||||||
' <div>Current date: {Date}</div>\n' +
|
' <div>Womp womp: {Error}</div>\n' +
|
||||||
' ^^^^^^',
|
' ^^^^^^^',
|
||||||
{withoutStack: true},
|
{withoutStack: true},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
@ -651,37 +754,46 @@ describe('ReactFlight', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should warn in DEV if a toJSON instance is passed to a Client Component', () => {
|
it('should warn in DEV if a toJSON instance is passed to a Client Component', () => {
|
||||||
|
const obj = {
|
||||||
|
toJSON() {
|
||||||
|
return 123;
|
||||||
|
},
|
||||||
|
};
|
||||||
function ClientImpl({value}) {
|
function ClientImpl({value}) {
|
||||||
return <div>{value}</div>;
|
return <div>{value}</div>;
|
||||||
}
|
}
|
||||||
const Client = clientReference(ClientImpl);
|
const Client = clientReference(ClientImpl);
|
||||||
expect(() => {
|
expect(() => {
|
||||||
const transport = ReactNoopFlightServer.render(
|
const transport = ReactNoopFlightServer.render(<Client value={obj} />);
|
||||||
<Client value={new Date()} />,
|
|
||||||
);
|
|
||||||
ReactNoopFlightClient.read(transport);
|
ReactNoopFlightClient.read(transport);
|
||||||
}).toErrorDev(
|
}).toErrorDev(
|
||||||
'Only plain objects can be passed to Client Components from Server Components. ' +
|
'Only plain objects can be passed to Client Components from Server Components. ' +
|
||||||
'Date objects are not supported.',
|
'Objects with toJSON methods are not supported.',
|
||||||
{withoutStack: true},
|
{withoutStack: true},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should warn in DEV if a toJSON instance is passed to a Client Component child', () => {
|
it('should warn in DEV if a toJSON instance is passed to a Client Component child', () => {
|
||||||
|
const obj = {
|
||||||
|
toJSON() {
|
||||||
|
return 123;
|
||||||
|
},
|
||||||
|
};
|
||||||
function ClientImpl({children}) {
|
function ClientImpl({children}) {
|
||||||
return <div>{children}</div>;
|
return <div>{children}</div>;
|
||||||
}
|
}
|
||||||
const Client = clientReference(ClientImpl);
|
const Client = clientReference(ClientImpl);
|
||||||
expect(() => {
|
expect(() => {
|
||||||
const transport = ReactNoopFlightServer.render(
|
const transport = ReactNoopFlightServer.render(
|
||||||
<Client>Current date: {new Date()}</Client>,
|
<Client>Current date: {obj}</Client>,
|
||||||
);
|
);
|
||||||
ReactNoopFlightClient.read(transport);
|
ReactNoopFlightClient.read(transport);
|
||||||
}).toErrorDev(
|
}).toErrorDev(
|
||||||
'Only plain objects can be passed to Client Components from Server Components. ' +
|
'Only plain objects can be passed to Client Components from Server Components. ' +
|
||||||
'Date objects are not supported.\n' +
|
'Objects with toJSON methods are not supported. ' +
|
||||||
' <>Current date: {Date}</>\n' +
|
'Convert it manually to a simple value before passing it to props.\n' +
|
||||||
' ^^^^^^',
|
' <>Current date: {{toJSON: function}}</>\n' +
|
||||||
|
' ^^^^^^^^^^^^^^^^^^^^',
|
||||||
{withoutStack: true},
|
{withoutStack: true},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -23,27 +23,24 @@
|
||||||
// So `$$$config` looks like a global variable, but it's
|
// So `$$$config` looks like a global variable, but it's
|
||||||
// really an argument to a top-level wrapping function.
|
// really an argument to a top-level wrapping function.
|
||||||
|
|
||||||
declare var $$$hostConfig: any;
|
declare var $$$config: any;
|
||||||
|
|
||||||
export type Response = any;
|
|
||||||
export opaque type SSRManifest = mixed;
|
export opaque type SSRManifest = mixed;
|
||||||
export opaque type ServerManifest = mixed;
|
export opaque type ServerManifest = mixed;
|
||||||
export opaque type ServerReferenceId = string;
|
export opaque type ServerReferenceId = string;
|
||||||
export opaque type ClientReferenceMetadata = mixed;
|
export opaque type ClientReferenceMetadata = mixed;
|
||||||
export opaque type ClientReference<T> = mixed; // eslint-disable-line no-unused-vars
|
export opaque type ClientReference<T> = mixed; // eslint-disable-line no-unused-vars
|
||||||
export const resolveClientReference = $$$hostConfig.resolveClientReference;
|
export const resolveClientReference = $$$config.resolveClientReference;
|
||||||
export const resolveServerReference = $$$hostConfig.resolveServerReference;
|
export const resolveServerReference = $$$config.resolveServerReference;
|
||||||
export const preloadModule = $$$hostConfig.preloadModule;
|
export const preloadModule = $$$config.preloadModule;
|
||||||
export const requireModule = $$$hostConfig.requireModule;
|
export const requireModule = $$$config.requireModule;
|
||||||
|
export const dispatchHint = $$$config.dispatchHint;
|
||||||
|
|
||||||
export opaque type Source = mixed;
|
export opaque type Source = mixed;
|
||||||
|
|
||||||
export type UninitializedModel = string;
|
|
||||||
export const parseModel = $$$hostConfig.parseModel;
|
|
||||||
|
|
||||||
export opaque type StringDecoder = mixed; // eslint-disable-line no-undef
|
export opaque type StringDecoder = mixed; // eslint-disable-line no-undef
|
||||||
|
|
||||||
export const supportsBinaryStreams = $$$hostConfig.supportsBinaryStreams;
|
export const supportsBinaryStreams = $$$config.supportsBinaryStreams;
|
||||||
export const createStringDecoder = $$$hostConfig.createStringDecoder;
|
export const createStringDecoder = $$$config.createStringDecoder;
|
||||||
export const readPartialStringChunk = $$$hostConfig.readPartialStringChunk;
|
export const readPartialStringChunk = $$$config.readPartialStringChunk;
|
||||||
export const readFinalStringChunk = $$$hostConfig.readFinalStringChunk;
|
export const readFinalStringChunk = $$$config.readFinalStringChunk;
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
*/
|
||||||
|
|
||||||
|
export * from 'react-client/src/ReactFlightClientConfigBrowser';
|
||||||
|
export * from 'react-server-dom-webpack/src/ReactFlightClientConfigWebpackBundler';
|
||||||
|
export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM';
|
||||||
|
|
@ -7,8 +7,8 @@
|
||||||
* @flow
|
* @flow
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export * from 'react-client/src/ReactFlightClientHostConfigBrowser';
|
export * from 'react-client/src/ReactFlightClientConfigBrowser';
|
||||||
export * from 'react-client/src/ReactFlightClientHostConfigStream';
|
export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM';
|
||||||
|
|
||||||
export type Response = any;
|
export type Response = any;
|
||||||
export opaque type SSRManifest = mixed;
|
export opaque type SSRManifest = mixed;
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
*/
|
||||||
|
|
||||||
|
export * from 'react-client/src/ReactFlightClientConfigBrowser';
|
||||||
|
export * from 'react-server-dom-webpack/src/ReactFlightClientConfigWebpackBundler';
|
||||||
|
export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM';
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
*/
|
||||||
|
|
||||||
|
export * from 'react-client/src/ReactFlightClientConfigBrowser';
|
||||||
|
export * from 'react-server-dom-webpack/src/ReactFlightClientConfigWebpackBundler';
|
||||||
|
export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM';
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
*/
|
||||||
|
|
||||||
|
export * from 'react-client/src/ReactFlightClientConfigNode';
|
||||||
|
export * from 'react-server-dom-webpack/src/ReactFlightClientConfigWebpackBundler';
|
||||||
|
export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM';
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
*/
|
||||||
|
|
||||||
|
export * from 'react-client/src/ReactFlightClientConfigNode';
|
||||||
|
export * from 'react-server-dom-webpack/src/ReactFlightClientConfigNodeBundler';
|
||||||
|
export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM';
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
/**
|
|
||||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
||||||
*
|
|
||||||
* This source code is licensed under the MIT license found in the
|
|
||||||
* LICENSE file in the root directory of this source tree.
|
|
||||||
*
|
|
||||||
* @flow
|
|
||||||
*/
|
|
||||||
|
|
||||||
export * from 'react-client/src/ReactFlightClientHostConfigBrowser';
|
|
||||||
export * from 'react-client/src/ReactFlightClientHostConfigStream';
|
|
||||||
export * from 'react-server-dom-webpack/src/ReactFlightClientWebpackBundlerConfig';
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
/**
|
|
||||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
||||||
*
|
|
||||||
* This source code is licensed under the MIT license found in the
|
|
||||||
* LICENSE file in the root directory of this source tree.
|
|
||||||
*
|
|
||||||
* @flow
|
|
||||||
*/
|
|
||||||
|
|
||||||
export * from 'react-client/src/ReactFlightClientHostConfigBrowser';
|
|
||||||
export * from 'react-client/src/ReactFlightClientHostConfigStream';
|
|
||||||
export * from 'react-server-dom-webpack/src/ReactFlightClientWebpackBundlerConfig';
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
/**
|
|
||||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
||||||
*
|
|
||||||
* This source code is licensed under the MIT license found in the
|
|
||||||
* LICENSE file in the root directory of this source tree.
|
|
||||||
*
|
|
||||||
* @flow
|
|
||||||
*/
|
|
||||||
|
|
||||||
export * from 'react-client/src/ReactFlightClientHostConfigBrowser';
|
|
||||||
export * from 'react-client/src/ReactFlightClientHostConfigStream';
|
|
||||||
export * from 'react-server-dom-webpack/src/ReactFlightClientWebpackBundlerConfig';
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
/**
|
|
||||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
||||||
*
|
|
||||||
* This source code is licensed under the MIT license found in the
|
|
||||||
* LICENSE file in the root directory of this source tree.
|
|
||||||
*
|
|
||||||
* @flow
|
|
||||||
*/
|
|
||||||
|
|
||||||
export * from 'react-client/src/ReactFlightClientHostConfigNode';
|
|
||||||
export * from 'react-client/src/ReactFlightClientHostConfigStream';
|
|
||||||
export * from 'react-server-dom-webpack/src/ReactFlightClientWebpackBundlerConfig';
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
/**
|
|
||||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
||||||
*
|
|
||||||
* This source code is licensed under the MIT license found in the
|
|
||||||
* LICENSE file in the root directory of this source tree.
|
|
||||||
*
|
|
||||||
* @flow
|
|
||||||
*/
|
|
||||||
|
|
||||||
export * from 'react-client/src/ReactFlightClientHostConfigNode';
|
|
||||||
export * from 'react-client/src/ReactFlightClientHostConfigStream';
|
|
||||||
export * from 'react-server-dom-webpack/src/ReactFlightClientNodeBundlerConfig';
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
/**
|
|
||||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
||||||
*
|
|
||||||
* This source code is licensed under the MIT license found in the
|
|
||||||
* LICENSE file in the root directory of this source tree.
|
|
||||||
*
|
|
||||||
* @flow
|
|
||||||
*/
|
|
||||||
|
|
||||||
export * from 'react-server-dom-relay/src/ReactFlightDOMRelayClientHostConfig';
|
|
||||||
export * from '../ReactFlightClientHostConfigNoStream';
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
/**
|
|
||||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
||||||
*
|
|
||||||
* This source code is licensed under the MIT license found in the
|
|
||||||
* LICENSE file in the root directory of this source tree.
|
|
||||||
*
|
|
||||||
* @flow
|
|
||||||
*/
|
|
||||||
|
|
||||||
export * from 'react-server-native-relay/src/ReactFlightNativeRelayClientHostConfig';
|
|
||||||
export * from '../ReactFlightClientHostConfigNoStream';
|
|
||||||
|
|
@ -51,9 +51,19 @@ type Dispatch<A> = A => void;
|
||||||
|
|
||||||
let primitiveStackCache: null | Map<string, Array<any>> = null;
|
let primitiveStackCache: null | Map<string, Array<any>> = null;
|
||||||
|
|
||||||
|
type MemoCache = {
|
||||||
|
data: Array<Array<any>>,
|
||||||
|
index: number,
|
||||||
|
};
|
||||||
|
|
||||||
|
type FunctionComponentUpdateQueue = {
|
||||||
|
memoCache?: MemoCache | null,
|
||||||
|
};
|
||||||
|
|
||||||
type Hook = {
|
type Hook = {
|
||||||
memoizedState: any,
|
memoizedState: any,
|
||||||
next: Hook | null,
|
next: Hook | null,
|
||||||
|
updateQueue: FunctionComponentUpdateQueue | null,
|
||||||
};
|
};
|
||||||
|
|
||||||
function getPrimitiveStackCache(): Map<string, Array<any>> {
|
function getPrimitiveStackCache(): Map<string, Array<any>> {
|
||||||
|
|
@ -79,6 +89,10 @@ function getPrimitiveStackCache(): Map<string, Array<any>> {
|
||||||
Dispatcher.useDebugValue(null);
|
Dispatcher.useDebugValue(null);
|
||||||
Dispatcher.useCallback(() => {});
|
Dispatcher.useCallback(() => {});
|
||||||
Dispatcher.useMemo(() => null);
|
Dispatcher.useMemo(() => null);
|
||||||
|
if (typeof Dispatcher.useMemoCache === 'function') {
|
||||||
|
// This type check is for Flow only.
|
||||||
|
Dispatcher.useMemoCache(0);
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
readHookLog = hookLog;
|
readHookLog = hookLog;
|
||||||
hookLog = [];
|
hookLog = [];
|
||||||
|
|
@ -106,6 +120,13 @@ function readContext<T>(context: ReactContext<T>): T {
|
||||||
return context._currentValue;
|
return context._currentValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function use<T>(): T {
|
||||||
|
// TODO: What should this do if it receives an unresolved promise?
|
||||||
|
throw new Error(
|
||||||
|
'Support for `use` not yet implemented in react-debug-tools.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function useContext<T>(context: ReactContext<T>): T {
|
function useContext<T>(context: ReactContext<T>): T {
|
||||||
hookLog.push({
|
hookLog.push({
|
||||||
primitive: 'Context',
|
primitive: 'Context',
|
||||||
|
|
@ -123,7 +144,7 @@ function useState<S>(
|
||||||
hook !== null
|
hook !== null
|
||||||
? hook.memoizedState
|
? hook.memoizedState
|
||||||
: typeof initialState === 'function'
|
: typeof initialState === 'function'
|
||||||
? // $FlowFixMe: Flow doesn't like mixed types
|
? // $FlowFixMe[incompatible-use]: Flow doesn't like mixed types
|
||||||
initialState()
|
initialState()
|
||||||
: initialState;
|
: initialState;
|
||||||
hookLog.push({primitive: 'State', stackError: new Error(), value: state});
|
hookLog.push({primitive: 'State', stackError: new Error(), value: state});
|
||||||
|
|
@ -326,7 +347,40 @@ function useId(): string {
|
||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function useMemoCache(size: number): Array<any> {
|
||||||
|
const hook = nextHook();
|
||||||
|
let memoCache: MemoCache;
|
||||||
|
if (
|
||||||
|
hook !== null &&
|
||||||
|
hook.updateQueue !== null &&
|
||||||
|
hook.updateQueue.memoCache != null
|
||||||
|
) {
|
||||||
|
memoCache = hook.updateQueue.memoCache;
|
||||||
|
} else {
|
||||||
|
memoCache = {
|
||||||
|
data: [],
|
||||||
|
index: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let data = memoCache.data[memoCache.index];
|
||||||
|
if (data === undefined) {
|
||||||
|
const MEMO_CACHE_SENTINEL = Symbol.for('react.memo_cache_sentinel');
|
||||||
|
data = new Array(size);
|
||||||
|
for (let i = 0; i < size; i++) {
|
||||||
|
data[i] = MEMO_CACHE_SENTINEL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
hookLog.push({
|
||||||
|
primitive: 'MemoCache',
|
||||||
|
stackError: new Error(),
|
||||||
|
value: data,
|
||||||
|
});
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
const Dispatcher: DispatcherType = {
|
const Dispatcher: DispatcherType = {
|
||||||
|
use,
|
||||||
readContext,
|
readContext,
|
||||||
useCacheRefresh,
|
useCacheRefresh,
|
||||||
useCallback,
|
useCallback,
|
||||||
|
|
@ -337,6 +391,7 @@ const Dispatcher: DispatcherType = {
|
||||||
useLayoutEffect,
|
useLayoutEffect,
|
||||||
useInsertionEffect,
|
useInsertionEffect,
|
||||||
useMemo,
|
useMemo,
|
||||||
|
useMemoCache,
|
||||||
useReducer,
|
useReducer,
|
||||||
useRef,
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
|
|
@ -513,10 +568,10 @@ function parseCustomHookName(functionName: void | string): string {
|
||||||
if (startIndex === -1) {
|
if (startIndex === -1) {
|
||||||
startIndex = 0;
|
startIndex = 0;
|
||||||
}
|
}
|
||||||
if (functionName.substr(startIndex, 3) === 'use') {
|
if (functionName.slice(startIndex, startIndex + 3) === 'use') {
|
||||||
startIndex += 3;
|
startIndex += 3;
|
||||||
}
|
}
|
||||||
return functionName.substr(startIndex);
|
return functionName.slice(startIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildTree(
|
function buildTree(
|
||||||
|
|
@ -674,7 +729,7 @@ function handleRenderFunctionError(error: any): void {
|
||||||
// that the error is caused by user's code in renderFunction.
|
// that the error is caused by user's code in renderFunction.
|
||||||
// In this case, we should wrap the original error inside a custom error
|
// In this case, we should wrap the original error inside a custom error
|
||||||
// so that devtools can give a clear message about it.
|
// so that devtools can give a clear message about it.
|
||||||
// $FlowFixMe: Flow doesn't know about 2nd argument of Error constructor
|
// $FlowFixMe[extra-arg]: Flow doesn't know about 2nd argument of Error constructor
|
||||||
const wrapperError = new Error('Error rendering inspected component', {
|
const wrapperError = new Error('Error rendering inspected component', {
|
||||||
cause: error,
|
cause: error,
|
||||||
});
|
});
|
||||||
|
|
@ -682,7 +737,7 @@ function handleRenderFunctionError(error: any): void {
|
||||||
// TODO: refactor this if we ever combine the devtools and debug tools packages
|
// TODO: refactor this if we ever combine the devtools and debug tools packages
|
||||||
wrapperError.name = 'ReactDebugToolsRenderError';
|
wrapperError.name = 'ReactDebugToolsRenderError';
|
||||||
// this stage-4 proposal is not supported by all environments yet.
|
// this stage-4 proposal is not supported by all environments yet.
|
||||||
// $FlowFixMe Flow doesn't have this type yet.
|
// $FlowFixMe[prop-missing] Flow doesn't have this type yet.
|
||||||
wrapperError.cause = error;
|
wrapperError.cause = error;
|
||||||
throw wrapperError;
|
throw wrapperError;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ let React;
|
||||||
let ReactTestRenderer;
|
let ReactTestRenderer;
|
||||||
let ReactDebugTools;
|
let ReactDebugTools;
|
||||||
let act;
|
let act;
|
||||||
|
let useMemoCache;
|
||||||
|
|
||||||
describe('ReactHooksInspectionIntegration', () => {
|
describe('ReactHooksInspectionIntegration', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
|
@ -22,6 +23,7 @@ describe('ReactHooksInspectionIntegration', () => {
|
||||||
ReactTestRenderer = require('react-test-renderer');
|
ReactTestRenderer = require('react-test-renderer');
|
||||||
act = require('internal-test-utils').act;
|
act = require('internal-test-utils').act;
|
||||||
ReactDebugTools = require('react-debug-tools');
|
ReactDebugTools = require('react-debug-tools');
|
||||||
|
useMemoCache = React.unstable_useMemoCache;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should inspect the current state of useState hooks', async () => {
|
it('should inspect the current state of useState hooks', async () => {
|
||||||
|
|
@ -633,6 +635,33 @@ describe('ReactHooksInspectionIntegration', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// @gate enableUseMemoCacheHook
|
||||||
|
it('should support useMemoCache hook', () => {
|
||||||
|
function Foo() {
|
||||||
|
const $ = useMemoCache(1);
|
||||||
|
let t0;
|
||||||
|
|
||||||
|
if ($[0] === Symbol.for('react.memo_cache_sentinel')) {
|
||||||
|
t0 = <div>{1}</div>;
|
||||||
|
$[0] = t0;
|
||||||
|
} else {
|
||||||
|
t0 = $[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
return t0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderer = ReactTestRenderer.create(<Foo />);
|
||||||
|
const childFiber = renderer.root.findByType(Foo)._currentFiber();
|
||||||
|
const tree = ReactDebugTools.inspectHooksOfFiber(childFiber);
|
||||||
|
|
||||||
|
expect(tree.length).toEqual(1);
|
||||||
|
expect(tree[0].isStateEditable).toBe(false);
|
||||||
|
expect(tree[0].name).toBe('MemoCache');
|
||||||
|
expect(tree[0].value).toHaveLength(1);
|
||||||
|
expect(tree[0].value[0]).toEqual(<div>{1}</div>);
|
||||||
|
});
|
||||||
|
|
||||||
describe('useDebugValue', () => {
|
describe('useDebugValue', () => {
|
||||||
it('should support inspectable values for multiple custom hooks', () => {
|
it('should support inspectable values for multiple custom hooks', () => {
|
||||||
function useLabeledValue(label) {
|
function useLabeledValue(label) {
|
||||||
|
|
@ -869,7 +898,7 @@ describe('ReactHooksInspectionIntegration', () => {
|
||||||
const Suspense = React.Suspense;
|
const Suspense = React.Suspense;
|
||||||
|
|
||||||
function Foo(props) {
|
function Foo(props) {
|
||||||
const [value] = React.useState(props.defaultValue.substr(0, 3));
|
const [value] = React.useState(props.defaultValue.slice(0, 3));
|
||||||
return <div>{value}</div>;
|
return <div>{value}</div>;
|
||||||
}
|
}
|
||||||
Foo.defaultProps = {
|
Foo.defaultProps = {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "react-devtools-core",
|
"name": "react-devtools-core",
|
||||||
"version": "4.27.4",
|
"version": "4.27.8",
|
||||||
"description": "Use react-devtools outside of the browser",
|
"description": "Use react-devtools outside of the browser",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"main": "./dist/backend.js",
|
"main": "./dist/backend.js",
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import Agent from 'react-devtools-shared/src/backend/agent';
|
||||||
import Bridge from 'react-devtools-shared/src/bridge';
|
import Bridge from 'react-devtools-shared/src/bridge';
|
||||||
import {installHook} from 'react-devtools-shared/src/hook';
|
import {installHook} from 'react-devtools-shared/src/hook';
|
||||||
import {initBackend} from 'react-devtools-shared/src/backend';
|
import {initBackend} from 'react-devtools-shared/src/backend';
|
||||||
|
import {installConsoleFunctionsToWindow} from 'react-devtools-shared/src/backend/console';
|
||||||
import {__DEBUG__} from 'react-devtools-shared/src/constants';
|
import {__DEBUG__} from 'react-devtools-shared/src/constants';
|
||||||
import setupNativeStyleEditor from 'react-devtools-shared/src/backend/NativeStyleEditor/setupNativeStyleEditor';
|
import setupNativeStyleEditor from 'react-devtools-shared/src/backend/NativeStyleEditor/setupNativeStyleEditor';
|
||||||
import {getDefaultComponentFilters} from 'react-devtools-shared/src/utils';
|
import {getDefaultComponentFilters} from 'react-devtools-shared/src/utils';
|
||||||
|
|
@ -38,6 +39,9 @@ type ConnectOptions = {
|
||||||
...
|
...
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Install a global variable to allow patching console early (during injection).
|
||||||
|
// This provides React Native developers with components stacks even if they don't run DevTools.
|
||||||
|
installConsoleFunctionsToWindow();
|
||||||
installHook(window);
|
installHook(window);
|
||||||
|
|
||||||
const hook: ?DevToolsHook = window.__REACT_DEVTOOLS_GLOBAL_HOOK__;
|
const hook: ?DevToolsHook = window.__REACT_DEVTOOLS_GLOBAL_HOOK__;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
/**
|
/**
|
||||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
* Copyright (c) Meta Platforms, Inc. and its affiliates.
|
||||||
*
|
*
|
||||||
* This source code is licensed under the MIT license found in the
|
* This source code is licensed under the MIT license found in the
|
||||||
* LICENSE file in the root directory of this source tree.
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
|
@ -7,10 +7,8 @@
|
||||||
* @flow
|
* @flow
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {
|
import type {ConsolePatchSettings} from 'react-devtools-shared/src/backend/types';
|
||||||
type ConsolePatchSettings,
|
import {writeConsolePatchSettingsToWindow} from 'react-devtools-shared/src/backend/console';
|
||||||
writeConsolePatchSettingsToWindow,
|
|
||||||
} from 'react-devtools-shared/src/backend/console';
|
|
||||||
import {castBool, castBrowserTheme} from 'react-devtools-shared/src/utils';
|
import {castBool, castBrowserTheme} from 'react-devtools-shared/src/utils';
|
||||||
|
|
||||||
// Note: all keys should be optional in this type, because users can use newer
|
// Note: all keys should be optional in this type, because users can use newer
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ import {
|
||||||
__DEBUG__,
|
__DEBUG__,
|
||||||
LOCAL_STORAGE_DEFAULT_TAB_KEY,
|
LOCAL_STORAGE_DEFAULT_TAB_KEY,
|
||||||
} from 'react-devtools-shared/src/constants';
|
} from 'react-devtools-shared/src/constants';
|
||||||
import {localStorageSetItem} from '../../react-devtools-shared/src/storage';
|
import {localStorageSetItem} from 'react-devtools-shared/src/storage';
|
||||||
|
|
||||||
import type {FrontendBridge} from 'react-devtools-shared/src/bridge';
|
import type {FrontendBridge} from 'react-devtools-shared/src/bridge';
|
||||||
import type {InspectedElement} from 'react-devtools-shared/src/devtools/views/Components/types';
|
import type {InspectedElement} from 'react-devtools-shared/src/devtools/views/Components/types';
|
||||||
|
|
@ -167,7 +167,6 @@ function onDisconnected() {
|
||||||
disconnectedCallback();
|
disconnectedCallback();
|
||||||
}
|
}
|
||||||
|
|
||||||
// $FlowFixMe[missing-local-annot]
|
|
||||||
function onError({code, message}: $FlowFixMe) {
|
function onError({code, message}: $FlowFixMe) {
|
||||||
safeUnmount();
|
safeUnmount();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,9 @@
|
||||||
"manifest_version": 3,
|
"manifest_version": 3,
|
||||||
"name": "React Developer Tools",
|
"name": "React Developer Tools",
|
||||||
"description": "Adds React debugging tools to the Chrome Developer Tools.",
|
"description": "Adds React debugging tools to the Chrome Developer Tools.",
|
||||||
"version": "4.27.4",
|
"version": "4.27.8",
|
||||||
"version_name": "4.27.4",
|
"version_name": "4.27.8",
|
||||||
"minimum_chrome_version": "88",
|
"minimum_chrome_version": "102",
|
||||||
"icons": {
|
"icons": {
|
||||||
"16": "icons/16-production.png",
|
"16": "icons/16-production.png",
|
||||||
"32": "icons/32-production.png",
|
"32": "icons/32-production.png",
|
||||||
|
|
@ -29,9 +29,7 @@
|
||||||
"resources": [
|
"resources": [
|
||||||
"main.html",
|
"main.html",
|
||||||
"panel.html",
|
"panel.html",
|
||||||
"build/react_devtools_backend.js",
|
"build/*.js"
|
||||||
"build/proxy.js",
|
|
||||||
"build/installHook.js"
|
|
||||||
],
|
],
|
||||||
"matches": [
|
"matches": [
|
||||||
"<all_urls>"
|
"<all_urls>"
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ const {resolve} = require('path');
|
||||||
const {argv} = require('yargs');
|
const {argv} = require('yargs');
|
||||||
|
|
||||||
const EXTENSION_PATH = resolve('./chrome/build/unpacked');
|
const EXTENSION_PATH = resolve('./chrome/build/unpacked');
|
||||||
const START_URL = argv.url || 'https://reactjs.org/';
|
const START_URL = argv.url || 'https://react.dev/';
|
||||||
|
|
||||||
chromeLaunch(START_URL, {
|
chromeLaunch(START_URL, {
|
||||||
args: [
|
args: [
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ const main = async buildId => {
|
||||||
const json = JSON.parse(file);
|
const json = JSON.parse(file);
|
||||||
const alias = json.alias[0];
|
const alias = json.alias[0];
|
||||||
|
|
||||||
const commit = execSync('git rev-parse HEAD').toString().trim().substr(0, 7);
|
const commit = execSync('git rev-parse HEAD').toString().trim().slice(0, 7);
|
||||||
|
|
||||||
let date = new Date();
|
let date = new Date();
|
||||||
date = `${date.toLocaleDateString()} – ${date.toLocaleTimeString()}`;
|
date = `${date.toLocaleDateString()} – ${date.toLocaleTimeString()}`;
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,9 @@
|
||||||
"manifest_version": 3,
|
"manifest_version": 3,
|
||||||
"name": "React Developer Tools",
|
"name": "React Developer Tools",
|
||||||
"description": "Adds React debugging tools to the Microsoft Edge Developer Tools.",
|
"description": "Adds React debugging tools to the Microsoft Edge Developer Tools.",
|
||||||
"version": "4.27.4",
|
"version": "4.27.8",
|
||||||
"version_name": "4.27.4",
|
"version_name": "4.27.8",
|
||||||
"minimum_chrome_version": "88",
|
"minimum_chrome_version": "102",
|
||||||
"icons": {
|
"icons": {
|
||||||
"16": "icons/16-production.png",
|
"16": "icons/16-production.png",
|
||||||
"32": "icons/32-production.png",
|
"32": "icons/32-production.png",
|
||||||
|
|
@ -29,9 +29,7 @@
|
||||||
"resources": [
|
"resources": [
|
||||||
"main.html",
|
"main.html",
|
||||||
"panel.html",
|
"panel.html",
|
||||||
"build/react_devtools_backend.js",
|
"build/*.js"
|
||||||
"build/proxy.js",
|
|
||||||
"build/installHook.js"
|
|
||||||
],
|
],
|
||||||
"matches": [
|
"matches": [
|
||||||
"<all_urls>"
|
"<all_urls>"
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ const {resolve} = require('path');
|
||||||
const {argv} = require('yargs');
|
const {argv} = require('yargs');
|
||||||
|
|
||||||
const EXTENSION_PATH = resolve('./edge/build/unpacked');
|
const EXTENSION_PATH = resolve('./edge/build/unpacked');
|
||||||
const START_URL = argv.url || 'https://reactjs.org/';
|
const START_URL = argv.url || 'https://react.dev/';
|
||||||
|
|
||||||
const extargs = `--load-extension=${EXTENSION_PATH}`;
|
const extargs = `--load-extension=${EXTENSION_PATH}`;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
"manifest_version": 2,
|
"manifest_version": 2,
|
||||||
"name": "React Developer Tools",
|
"name": "React Developer Tools",
|
||||||
"description": "Adds React debugging tools to the Firefox Developer Tools.",
|
"description": "Adds React debugging tools to the Firefox Developer Tools.",
|
||||||
"version": "4.27.4",
|
"version": "4.27.8",
|
||||||
"applications": {
|
"applications": {
|
||||||
"gecko": {
|
"gecko": {
|
||||||
"id": "@react-devtools",
|
"id": "@react-devtools",
|
||||||
|
|
@ -30,9 +30,7 @@
|
||||||
"web_accessible_resources": [
|
"web_accessible_resources": [
|
||||||
"main.html",
|
"main.html",
|
||||||
"panel.html",
|
"panel.html",
|
||||||
"build/react_devtools_backend.js",
|
"build/*.js"
|
||||||
"build/proxy.js",
|
|
||||||
"build/installHook.js"
|
|
||||||
],
|
],
|
||||||
"background": {
|
"background": {
|
||||||
"scripts": [
|
"scripts": [
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ const {resolve} = require('path');
|
||||||
const {argv} = require('yargs');
|
const {argv} = require('yargs');
|
||||||
|
|
||||||
const EXTENSION_PATH = resolve('./firefox/build/unpacked');
|
const EXTENSION_PATH = resolve('./firefox/build/unpacked');
|
||||||
const START_URL = argv.url || 'https://reactjs.org/';
|
const START_URL = argv.url || 'https://react.dev/';
|
||||||
|
|
||||||
const firefoxVersion = process.env.WEB_EXT_FIREFOX;
|
const firefoxVersion = process.env.WEB_EXT_FIREFOX;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@
|
||||||
|
|
||||||
/* eslint-disable no-unused-vars */
|
/* eslint-disable no-unused-vars */
|
||||||
|
|
||||||
type JestMockFn<TArguments: $ReadOnlyArray<*>, TReturn> = {
|
type JestMockFn<TArguments: $ReadOnlyArray<any>, TReturn> = {
|
||||||
(...args: TArguments): TReturn,
|
(...args: TArguments): TReturn,
|
||||||
/**
|
/**
|
||||||
* An object for introspecting mock calls
|
* An object for introspecting mock calls
|
||||||
|
|
@ -626,7 +626,7 @@ interface JestExpectType {
|
||||||
* Use .toBeInstanceOf(Class) to check that an object is an instance of a
|
* Use .toBeInstanceOf(Class) to check that an object is an instance of a
|
||||||
* class.
|
* class.
|
||||||
*/
|
*/
|
||||||
toBeInstanceOf(cls: Class<*>): void;
|
toBeInstanceOf(cls: Class<any>): void;
|
||||||
/**
|
/**
|
||||||
* .toBeNull() is the same as .toBe(null) but the error messages are a bit
|
* .toBeNull() is the same as .toBe(null) but the error messages are a bit
|
||||||
* nicer.
|
* nicer.
|
||||||
|
|
@ -815,7 +815,7 @@ type JestObjectType = {
|
||||||
* Returns a new, unused mock function. Optionally takes a mock
|
* Returns a new, unused mock function. Optionally takes a mock
|
||||||
* implementation.
|
* implementation.
|
||||||
*/
|
*/
|
||||||
fn<TArguments: $ReadOnlyArray<*>, TReturn>(
|
fn<TArguments: $ReadOnlyArray<any>, TReturn>(
|
||||||
implementation?: (...args: TArguments) => TReturn
|
implementation?: (...args: TArguments) => TReturn
|
||||||
): JestMockFn<TArguments, TReturn>,
|
): JestMockFn<TArguments, TReturn>,
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -1,109 +1,34 @@
|
||||||
// Do not use imports or top-level requires here!
|
/**
|
||||||
// Running module factories is intentionally delayed until we know the hook exists.
|
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||||
// This is to avoid issues like: https://github.com/facebook/react-devtools/issues/1039
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*
|
||||||
|
* @flow
|
||||||
|
*/
|
||||||
|
|
||||||
// @flow strict-local
|
import type {DevToolsHook} from 'react-devtools-shared/src/backend/types';
|
||||||
|
|
||||||
'use strict';
|
import Agent from 'react-devtools-shared/src/backend/agent';
|
||||||
|
import Bridge from 'react-devtools-shared/src/bridge';
|
||||||
|
import {initBackend} from 'react-devtools-shared/src/backend';
|
||||||
|
import setupNativeStyleEditor from 'react-devtools-shared/src/backend/NativeStyleEditor/setupNativeStyleEditor';
|
||||||
|
|
||||||
let welcomeHasInitialized = false;
|
import {COMPACT_VERSION_NAME} from './utils';
|
||||||
|
|
||||||
// $FlowFixMe[missing-local-annot]
|
setup(window.__REACT_DEVTOOLS_GLOBAL_HOOK__);
|
||||||
function welcome(event: $FlowFixMe) {
|
|
||||||
if (
|
|
||||||
event.source !== window ||
|
|
||||||
event.data.source !== 'react-devtools-content-script'
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// In some circumstances, this method is called more than once for a single welcome message.
|
function setup(hook: ?DevToolsHook) {
|
||||||
// The exact circumstances of this are unclear, though it seems related to 3rd party event batching code.
|
|
||||||
//
|
|
||||||
// Regardless, call this method multiple times can cause DevTools to add duplicate elements to the Store
|
|
||||||
// (and throw an error) or worse yet, choke up entirely and freeze the browser.
|
|
||||||
//
|
|
||||||
// The simplest solution is to ignore the duplicate events.
|
|
||||||
// To be clear, this SHOULD NOT BE NECESSARY, since we remove the event handler below.
|
|
||||||
//
|
|
||||||
// See https://github.com/facebook/react/issues/24162
|
|
||||||
if (welcomeHasInitialized) {
|
|
||||||
console.warn(
|
|
||||||
'React DevTools detected duplicate welcome "message" events from the content script.',
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
welcomeHasInitialized = true;
|
|
||||||
|
|
||||||
window.removeEventListener('message', welcome);
|
|
||||||
|
|
||||||
setup(window.__REACT_DEVTOOLS_GLOBAL_HOOK__);
|
|
||||||
}
|
|
||||||
|
|
||||||
window.addEventListener('message', welcome);
|
|
||||||
|
|
||||||
function setup(hook: any) {
|
|
||||||
if (hook == null) {
|
if (hook == null) {
|
||||||
// DevTools didn't get injected into this page (maybe b'c of the contentType).
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const Agent = require('react-devtools-shared/src/backend/agent').default;
|
|
||||||
const Bridge = require('react-devtools-shared/src/bridge').default;
|
|
||||||
const {initBackend} = require('react-devtools-shared/src/backend');
|
|
||||||
const setupNativeStyleEditor =
|
|
||||||
require('react-devtools-shared/src/backend/NativeStyleEditor/setupNativeStyleEditor').default;
|
|
||||||
|
|
||||||
const bridge = new Bridge<$FlowFixMe, $FlowFixMe>({
|
hook.backends.set(COMPACT_VERSION_NAME, {
|
||||||
listen(fn) {
|
Agent,
|
||||||
const listener = (event: $FlowFixMe) => {
|
Bridge,
|
||||||
if (
|
initBackend,
|
||||||
event.source !== window ||
|
setupNativeStyleEditor,
|
||||||
!event.data ||
|
|
||||||
event.data.source !== 'react-devtools-content-script' ||
|
|
||||||
!event.data.payload
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
fn(event.data.payload);
|
|
||||||
};
|
|
||||||
window.addEventListener('message', listener);
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener('message', listener);
|
|
||||||
};
|
|
||||||
},
|
|
||||||
send(event: string, payload: any, transferable?: Array<any>) {
|
|
||||||
window.postMessage(
|
|
||||||
{
|
|
||||||
source: 'react-devtools-bridge',
|
|
||||||
payload: {event, payload},
|
|
||||||
},
|
|
||||||
'*',
|
|
||||||
transferable,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const agent = new Agent(bridge);
|
hook.emit('devtools-backend-installed', COMPACT_VERSION_NAME);
|
||||||
agent.addListener('shutdown', () => {
|
|
||||||
// If we received 'shutdown' from `agent`, we assume the `bridge` is already shutting down,
|
|
||||||
// and that caused the 'shutdown' event on the `agent`, so we don't need to call `bridge.shutdown()` here.
|
|
||||||
hook.emit('shutdown');
|
|
||||||
});
|
|
||||||
|
|
||||||
initBackend(hook, agent, window);
|
|
||||||
|
|
||||||
// Let the frontend know that the backend has attached listeners and is ready for messages.
|
|
||||||
// This covers the case of syncing saved values after reloading/navigating while DevTools remain open.
|
|
||||||
bridge.send('extensionBackendInitialized');
|
|
||||||
|
|
||||||
// Setup React Native style editor if a renderer like react-native-web has injected it.
|
|
||||||
if (hook.resolveRNStyle) {
|
|
||||||
setupNativeStyleEditor(
|
|
||||||
bridge,
|
|
||||||
agent,
|
|
||||||
hook.resolveRNStyle,
|
|
||||||
hook.nativeStyleEditorValidAttributes,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,179 @@
|
||||||
|
/**
|
||||||
|
* 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 type {
|
||||||
|
DevToolsHook,
|
||||||
|
ReactRenderer,
|
||||||
|
} from 'react-devtools-shared/src/backend/types';
|
||||||
|
import {hasAssignedBackend} from 'react-devtools-shared/src/backend/utils';
|
||||||
|
import {COMPACT_VERSION_NAME} from './utils';
|
||||||
|
|
||||||
|
let welcomeHasInitialized = false;
|
||||||
|
|
||||||
|
function welcome(event: $FlowFixMe) {
|
||||||
|
if (
|
||||||
|
event.source !== window ||
|
||||||
|
event.data.source !== 'react-devtools-content-script'
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// In some circumstances, this method is called more than once for a single welcome message.
|
||||||
|
// The exact circumstances of this are unclear, though it seems related to 3rd party event batching code.
|
||||||
|
//
|
||||||
|
// Regardless, call this method multiple times can cause DevTools to add duplicate elements to the Store
|
||||||
|
// (and throw an error) or worse yet, choke up entirely and freeze the browser.
|
||||||
|
//
|
||||||
|
// The simplest solution is to ignore the duplicate events.
|
||||||
|
// To be clear, this SHOULD NOT BE NECESSARY, since we remove the event handler below.
|
||||||
|
//
|
||||||
|
// See https://github.com/facebook/react/issues/24162
|
||||||
|
if (welcomeHasInitialized) {
|
||||||
|
console.warn(
|
||||||
|
'React DevTools detected duplicate welcome "message" events from the content script.',
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
welcomeHasInitialized = true;
|
||||||
|
|
||||||
|
window.removeEventListener('message', welcome);
|
||||||
|
|
||||||
|
setup(window.__REACT_DEVTOOLS_GLOBAL_HOOK__);
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('message', welcome);
|
||||||
|
|
||||||
|
function setup(hook: ?DevToolsHook) {
|
||||||
|
// this should not happen, but Chrome can be weird sometimes
|
||||||
|
if (hook == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// register renderers that have already injected themselves.
|
||||||
|
hook.renderers.forEach(renderer => {
|
||||||
|
registerRenderer(renderer, hook);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Activate and remove from required all present backends, registered within the hook
|
||||||
|
hook.backends.forEach((_, backendVersion) => {
|
||||||
|
requiredBackends.delete(backendVersion);
|
||||||
|
activateBackend(backendVersion, hook);
|
||||||
|
});
|
||||||
|
|
||||||
|
updateRequiredBackends();
|
||||||
|
|
||||||
|
// register renderers that inject themselves later.
|
||||||
|
hook.sub('renderer', ({renderer}) => {
|
||||||
|
registerRenderer(renderer, hook);
|
||||||
|
updateRequiredBackends();
|
||||||
|
});
|
||||||
|
|
||||||
|
// listen for backend installations.
|
||||||
|
hook.sub('devtools-backend-installed', version => {
|
||||||
|
activateBackend(version, hook);
|
||||||
|
updateRequiredBackends();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const requiredBackends = new Set<string>();
|
||||||
|
|
||||||
|
function registerRenderer(renderer: ReactRenderer, hook: DevToolsHook) {
|
||||||
|
let version = renderer.reconcilerVersion || renderer.version;
|
||||||
|
if (!hasAssignedBackend(version)) {
|
||||||
|
version = COMPACT_VERSION_NAME;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if required backend is already activated, no need to require again
|
||||||
|
if (!hook.backends.has(version)) {
|
||||||
|
requiredBackends.add(version);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function activateBackend(version: string, hook: DevToolsHook) {
|
||||||
|
const backend = hook.backends.get(version);
|
||||||
|
if (!backend) {
|
||||||
|
throw new Error(`Could not find backend for version "${version}"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const {Agent, Bridge, initBackend, setupNativeStyleEditor} = backend;
|
||||||
|
const bridge = new Bridge({
|
||||||
|
listen(fn) {
|
||||||
|
const listener = (event: $FlowFixMe) => {
|
||||||
|
if (
|
||||||
|
event.source !== window ||
|
||||||
|
!event.data ||
|
||||||
|
event.data.source !== 'react-devtools-content-script' ||
|
||||||
|
!event.data.payload
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
fn(event.data.payload);
|
||||||
|
};
|
||||||
|
window.addEventListener('message', listener);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('message', listener);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
send(event: string, payload: any, transferable?: Array<any>) {
|
||||||
|
window.postMessage(
|
||||||
|
{
|
||||||
|
source: 'react-devtools-bridge',
|
||||||
|
payload: {event, payload},
|
||||||
|
},
|
||||||
|
'*',
|
||||||
|
transferable,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const agent = new Agent(bridge);
|
||||||
|
agent.addListener('shutdown', () => {
|
||||||
|
// If we received 'shutdown' from `agent`, we assume the `bridge` is already shutting down,
|
||||||
|
// and that caused the 'shutdown' event on the `agent`, so we don't need to call `bridge.shutdown()` here.
|
||||||
|
hook.emit('shutdown');
|
||||||
|
});
|
||||||
|
|
||||||
|
initBackend(hook, agent, window);
|
||||||
|
|
||||||
|
// Setup React Native style editor if a renderer like react-native-web has injected it.
|
||||||
|
if (typeof setupNativeStyleEditor === 'function' && hook.resolveRNStyle) {
|
||||||
|
setupNativeStyleEditor(
|
||||||
|
bridge,
|
||||||
|
agent,
|
||||||
|
hook.resolveRNStyle,
|
||||||
|
hook.nativeStyleEditorValidAttributes,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Let the frontend know that the backend has attached listeners and is ready for messages.
|
||||||
|
// This covers the case of syncing saved values after reloading/navigating while DevTools remain open.
|
||||||
|
bridge.send('extensionBackendInitialized');
|
||||||
|
|
||||||
|
// this backend is activated
|
||||||
|
requiredBackends.delete(version);
|
||||||
|
}
|
||||||
|
|
||||||
|
// tell the service worker which versions of backends are needed for the current page
|
||||||
|
function updateRequiredBackends() {
|
||||||
|
if (requiredBackends.size === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.postMessage(
|
||||||
|
{
|
||||||
|
source: 'react-devtools-backend-manager',
|
||||||
|
payload: {
|
||||||
|
type: 'react-devtools-required-backends',
|
||||||
|
versions: Array.from(requiredBackends),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'*',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -2,34 +2,52 @@
|
||||||
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
import {IS_FIREFOX} from './utils';
|
import {IS_FIREFOX, EXTENSION_CONTAINED_VERSIONS} from './utils';
|
||||||
|
|
||||||
const ports = {};
|
const ports = {};
|
||||||
|
|
||||||
if (!IS_FIREFOX) {
|
async function dynamicallyInjectContentScripts() {
|
||||||
// Manifest V3 method of injecting content scripts (not yet supported in Firefox)
|
const contentScriptsToInject = [
|
||||||
// Note: the "world" option in registerContentScripts is only available in Chrome v102+
|
{
|
||||||
// It's critical since it allows us to directly run scripts on the "main" world on the page
|
id: 'hook',
|
||||||
// "document_start" allows it to run before the page's scripts
|
matches: ['<all_urls>'],
|
||||||
// so the hook can be detected by react reconciler
|
js: ['build/installHook.js'],
|
||||||
chrome.scripting.registerContentScripts(
|
runAt: 'document_start',
|
||||||
[
|
world: chrome.scripting.ExecutionWorld.MAIN,
|
||||||
{
|
|
||||||
id: 'hook',
|
|
||||||
matches: ['<all_urls>'],
|
|
||||||
js: ['build/installHook.js'],
|
|
||||||
runAt: 'document_start',
|
|
||||||
world: chrome.scripting.ExecutionWorld.MAIN,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
function () {
|
|
||||||
// When the content scripts are already registered, an error will be thrown.
|
|
||||||
// It happens when the service worker process is incorrectly duplicated.
|
|
||||||
if (chrome.runtime.lastError) {
|
|
||||||
console.error(chrome.runtime.lastError);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
);
|
{
|
||||||
|
id: 'renderer',
|
||||||
|
matches: ['<all_urls>'],
|
||||||
|
js: ['build/renderer.js'],
|
||||||
|
runAt: 'document_start',
|
||||||
|
world: chrome.scripting.ExecutionWorld.MAIN,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
try {
|
||||||
|
// For some reason dynamically injected scripts might be already registered
|
||||||
|
// Registering them again will fail, which will result into
|
||||||
|
// __REACT_DEVTOOLS_GLOBAL_HOOK__ hook not being injected
|
||||||
|
|
||||||
|
// Not specifying ids, because Chrome throws an error
|
||||||
|
// if id of non-injected script is provided
|
||||||
|
await chrome.scripting.unregisterContentScripts();
|
||||||
|
|
||||||
|
// equivalent logic for Firefox is in prepareInjection.js
|
||||||
|
// Manifest V3 method of injecting content script
|
||||||
|
// TODO(hoxyq): migrate Firefox to V3 manifests
|
||||||
|
// Note: the "world" option in registerContentScripts is only available in Chrome v102+
|
||||||
|
// It's critical since it allows us to directly run scripts on the "main" world on the page
|
||||||
|
// "document_start" allows it to run before the page's scripts
|
||||||
|
// so the hook can be detected by react reconciler
|
||||||
|
await chrome.scripting.registerContentScripts(contentScriptsToInject);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!IS_FIREFOX) {
|
||||||
|
dynamicallyInjectContentScripts();
|
||||||
}
|
}
|
||||||
|
|
||||||
chrome.runtime.onConnect.addListener(function (port) {
|
chrome.runtime.onConnect.addListener(function (port) {
|
||||||
|
|
@ -164,6 +182,7 @@ chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
|
||||||
|
|
||||||
chrome.runtime.onMessage.addListener((request, sender) => {
|
chrome.runtime.onMessage.addListener((request, sender) => {
|
||||||
const tab = sender.tab;
|
const tab = sender.tab;
|
||||||
|
// sender.tab.id from content script points to the tab that injected the content script
|
||||||
if (tab) {
|
if (tab) {
|
||||||
const id = tab.id;
|
const id = tab.id;
|
||||||
// This is sent from the hook content script.
|
// This is sent from the hook content script.
|
||||||
|
|
@ -171,16 +190,56 @@ chrome.runtime.onMessage.addListener((request, sender) => {
|
||||||
if (request.hasDetectedReact) {
|
if (request.hasDetectedReact) {
|
||||||
setIconAndPopup(request.reactBuildType, id);
|
setIconAndPopup(request.reactBuildType, id);
|
||||||
} else {
|
} else {
|
||||||
|
const devtools = ports[id]?.devtools;
|
||||||
switch (request.payload?.type) {
|
switch (request.payload?.type) {
|
||||||
case 'fetch-file-with-cache-complete':
|
case 'fetch-file-with-cache-complete':
|
||||||
case 'fetch-file-with-cache-error':
|
case 'fetch-file-with-cache-error':
|
||||||
// Forward the result of fetch-in-page requests back to the extension.
|
// Forward the result of fetch-in-page requests back to the extension.
|
||||||
const devtools = ports[id]?.devtools;
|
devtools?.postMessage(request);
|
||||||
if (devtools) {
|
break;
|
||||||
devtools.postMessage(request);
|
// This is sent from the backend manager running on a page
|
||||||
}
|
case 'react-devtools-required-backends':
|
||||||
|
const backendsToDownload = [];
|
||||||
|
request.payload.versions.forEach(version => {
|
||||||
|
if (EXTENSION_CONTAINED_VERSIONS.includes(version)) {
|
||||||
|
if (!IS_FIREFOX) {
|
||||||
|
// equivalent logic for Firefox is in prepareInjection.js
|
||||||
|
chrome.scripting.executeScript({
|
||||||
|
target: {tabId: id},
|
||||||
|
files: [`/build/react_devtools_backend_${version}.js`],
|
||||||
|
world: chrome.scripting.ExecutionWorld.MAIN,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
backendsToDownload.push(version);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// Request the necessary backends in the extension DevTools UI
|
||||||
|
// TODO: handle this message in main.js to build the UI
|
||||||
|
devtools?.postMessage({
|
||||||
|
payload: {
|
||||||
|
type: 'react-devtools-additional-backends',
|
||||||
|
versions: backendsToDownload,
|
||||||
|
},
|
||||||
|
});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// sender.tab.id from devtools page may not exist, or point to the undocked devtools window
|
||||||
|
// so we use the payload to get the tab id
|
||||||
|
if (request.payload?.tabId) {
|
||||||
|
const tabId = request.payload?.tabId;
|
||||||
|
// This is sent from the devtools page when it is ready for injecting the backend
|
||||||
|
if (request.payload.type === 'react-devtools-inject-backend-manager') {
|
||||||
|
if (!IS_FIREFOX) {
|
||||||
|
// equivalent logic for Firefox is in prepareInjection.js
|
||||||
|
chrome.scripting.executeScript({
|
||||||
|
target: {tabId},
|
||||||
|
files: ['/build/backendManager.js'],
|
||||||
|
world: chrome.scripting.ExecutionWorld.MAIN,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,16 @@
|
||||||
/* global chrome */
|
/* global chrome */
|
||||||
|
|
||||||
import nullthrows from 'nullthrows';
|
import nullthrows from 'nullthrows';
|
||||||
import {IS_FIREFOX} from '../utils';
|
import {SESSION_STORAGE_RELOAD_AND_PROFILE_KEY} from 'react-devtools-shared/src/constants';
|
||||||
|
import {sessionStorageGetItem} from 'react-devtools-shared/src/storage';
|
||||||
|
import {IS_FIREFOX, EXTENSION_CONTAINED_VERSIONS} from '../utils';
|
||||||
|
|
||||||
|
// We run scripts on the page via the service worker (backgroud.js) for
|
||||||
|
// Manifest V3 extensions (Chrome & Edge).
|
||||||
|
// We need to inject this code for Firefox only because it does not support ExecutionWorld.MAIN
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/scripting/ExecutionWorld
|
||||||
|
// In this content script we have access to DOM, but don't have access to the webpage's window,
|
||||||
|
// so we inject this inline script tag into the webpage (allowed in Manifest V2).
|
||||||
function injectScriptSync(src) {
|
function injectScriptSync(src) {
|
||||||
let code = '';
|
let code = '';
|
||||||
const request = new XMLHttpRequest();
|
const request = new XMLHttpRequest();
|
||||||
|
|
@ -21,13 +29,6 @@ function injectScriptSync(src) {
|
||||||
nullthrows(script.parentNode).removeChild(script);
|
nullthrows(script.parentNode).removeChild(script);
|
||||||
}
|
}
|
||||||
|
|
||||||
function injectScriptAsync(src) {
|
|
||||||
const script = document.createElement('script');
|
|
||||||
script.src = src;
|
|
||||||
nullthrows(document.documentElement).appendChild(script);
|
|
||||||
nullthrows(script.parentNode).removeChild(script);
|
|
||||||
}
|
|
||||||
|
|
||||||
let lastDetectionResult;
|
let lastDetectionResult;
|
||||||
|
|
||||||
// We want to detect when a renderer attaches, and notify the "background page"
|
// We want to detect when a renderer attaches, and notify the "background page"
|
||||||
|
|
@ -89,10 +90,23 @@ window.addEventListener('message', function onMessage({data, source}) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'react-devtools-inject-backend':
|
case 'react-devtools-inject-backend-manager':
|
||||||
injectScriptAsync(
|
if (IS_FIREFOX) {
|
||||||
chrome.runtime.getURL('build/react_devtools_backend.js'),
|
injectScriptSync(chrome.runtime.getURL('build/backendManager.js'));
|
||||||
);
|
}
|
||||||
|
break;
|
||||||
|
case 'react-devtools-backend-manager':
|
||||||
|
if (IS_FIREFOX) {
|
||||||
|
data.payload?.versions?.forEach(version => {
|
||||||
|
if (EXTENSION_CONTAINED_VERSIONS.includes(version)) {
|
||||||
|
injectScriptSync(
|
||||||
|
chrome.runtime.getURL(
|
||||||
|
`/build/react_devtools_backend_${version}.js`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -108,40 +122,20 @@ window.addEventListener('pageshow', function ({target}) {
|
||||||
chrome.runtime.sendMessage(lastDetectionResult);
|
chrome.runtime.sendMessage(lastDetectionResult);
|
||||||
});
|
});
|
||||||
|
|
||||||
// We create a "sync" script tag to page to inject the global hook on Manifest V2 extensions.
|
if (IS_FIREFOX) {
|
||||||
// To comply with the new security policy in V3, we use chrome.scripting.registerContentScripts instead (see background.js).
|
// If we have just reloaded to profile, we need to inject the renderer interface before the app loads.
|
||||||
// However, the new API only works for Chrome v102+.
|
if (
|
||||||
// We insert a "async" script tag as a fallback for older versions.
|
sessionStorageGetItem(SESSION_STORAGE_RELOAD_AND_PROFILE_KEY) === 'true'
|
||||||
// It has known issues if JS on the page is faster than the extension.
|
) {
|
||||||
// Users will see a notice in components tab when that happens (see <Tree>).
|
injectScriptSync(chrome.runtime.getURL('build/renderer.js'));
|
||||||
// For Firefox, V3 is not ready, so sync injection is still the best approach.
|
}
|
||||||
const injectScript = IS_FIREFOX ? injectScriptSync : injectScriptAsync;
|
// Inject a __REACT_DEVTOOLS_GLOBAL_HOOK__ global for React to interact with.
|
||||||
|
// Only do this for HTML documents though, to avoid e.g. breaking syntax highlighting for XML docs.
|
||||||
// Inject a __REACT_DEVTOOLS_GLOBAL_HOOK__ global for React to interact with.
|
switch (document.contentType) {
|
||||||
// Only do this for HTML documents though, to avoid e.g. breaking syntax highlighting for XML docs.
|
case 'text/html':
|
||||||
// We need to inject this code because content scripts (ie injectGlobalHook.js) don't have access
|
case 'application/xhtml+xml': {
|
||||||
// to the webpage's window, so in order to access front end settings
|
injectScriptSync(chrome.runtime.getURL('build/installHook.js'));
|
||||||
// and communicate with React, we must inject this code into the webpage
|
break;
|
||||||
switch (document.contentType) {
|
}
|
||||||
case 'text/html':
|
|
||||||
case 'application/xhtml+xml': {
|
|
||||||
injectScript(chrome.runtime.getURL('build/installHook.js'));
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof exportFunction === 'function') {
|
|
||||||
// eslint-disable-next-line no-undef
|
|
||||||
exportFunction(
|
|
||||||
text => {
|
|
||||||
// Call clipboard.writeText from the extension content script
|
|
||||||
// (as it has the clipboardWrite permission) and return a Promise
|
|
||||||
// accessible to the webpage js code.
|
|
||||||
return new window.Promise((resolve, reject) =>
|
|
||||||
window.navigator.clipboard.writeText(text).then(resolve, reject),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
window.wrappedJSObject.__REACT_DEVTOOLS_GLOBAL_HOOK__,
|
|
||||||
{defineAs: 'clipboardCopyText'},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
let backendDisconnected: boolean = false;
|
let backendDisconnected: boolean = false;
|
||||||
let backendInitialized: boolean = false;
|
let backendInitialized: boolean = false;
|
||||||
|
|
||||||
function sayHelloToBackend() {
|
function sayHelloToBackendManager() {
|
||||||
window.postMessage(
|
window.postMessage(
|
||||||
{
|
{
|
||||||
source: 'react-devtools-content-script',
|
source: 'react-devtools-content-script',
|
||||||
|
|
@ -26,14 +26,19 @@ function handleMessageFromDevtools(message) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleMessageFromPage(event) {
|
function handleMessageFromPage(event) {
|
||||||
if (
|
if (event.source === window && event.data) {
|
||||||
event.source === window &&
|
// This is a message from a bridge (initialized by a devtools backend)
|
||||||
event.data &&
|
if (event.data.source === 'react-devtools-bridge') {
|
||||||
event.data.source === 'react-devtools-bridge'
|
backendInitialized = true;
|
||||||
) {
|
|
||||||
backendInitialized = true;
|
|
||||||
|
|
||||||
port.postMessage(event.data.payload);
|
port.postMessage(event.data.payload);
|
||||||
|
}
|
||||||
|
// This is a message from the backend manager
|
||||||
|
if (event.data.source === 'react-devtools-backend-manager') {
|
||||||
|
chrome.runtime.sendMessage({
|
||||||
|
payload: event.data.payload,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -63,17 +68,17 @@ port.onDisconnect.addListener(handleDisconnect);
|
||||||
|
|
||||||
window.addEventListener('message', handleMessageFromPage);
|
window.addEventListener('message', handleMessageFromPage);
|
||||||
|
|
||||||
sayHelloToBackend();
|
sayHelloToBackendManager();
|
||||||
|
|
||||||
// The backend waits to install the global hook until notified by the content script.
|
// The backend waits to install the global hook until notified by the content script.
|
||||||
// In the event of a page reload, the content script might be loaded before the backend is injected.
|
// In the event of a page reload, the content script might be loaded before the backend manager is injected.
|
||||||
// Because of this we need to poll the backend until it has been initialized.
|
// Because of this we need to poll the backend manager until it has been initialized.
|
||||||
if (!backendInitialized) {
|
if (!backendInitialized) {
|
||||||
const intervalID = setInterval(() => {
|
const intervalID = setInterval(() => {
|
||||||
if (backendInitialized || backendDisconnected) {
|
if (backendInitialized || backendDisconnected) {
|
||||||
clearInterval(intervalID);
|
clearInterval(intervalID);
|
||||||
} else {
|
} else {
|
||||||
sayHelloToBackend();
|
sayHelloToBackendManager();
|
||||||
}
|
}
|
||||||
}, 500);
|
}, 500);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
/**
|
||||||
|
* In order to support reload-and-profile functionality, the renderer needs to be injected before any other scripts.
|
||||||
|
* Since it is a complex file (with imports) we can't just toString() it like we do with the hook itself,
|
||||||
|
* So this entry point (one of the web_accessible_resources) provides a way to eagerly inject it.
|
||||||
|
* The hook will look for the presence of a global __REACT_DEVTOOLS_ATTACH__ and attach an injected renderer early.
|
||||||
|
* The normal case (not a reload-and-profile) will not make use of this entry point though.
|
||||||
|
*
|
||||||
|
* @flow
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {attach} from 'react-devtools-shared/src/backend/renderer';
|
||||||
|
import {SESSION_STORAGE_RELOAD_AND_PROFILE_KEY} from 'react-devtools-shared/src/constants';
|
||||||
|
import {sessionStorageGetItem} from 'react-devtools-shared/src/storage';
|
||||||
|
|
||||||
|
if (
|
||||||
|
sessionStorageGetItem(SESSION_STORAGE_RELOAD_AND_PROFILE_KEY) === 'true' &&
|
||||||
|
!window.hasOwnProperty('__REACT_DEVTOOLS_ATTACH__')
|
||||||
|
) {
|
||||||
|
Object.defineProperty(
|
||||||
|
window,
|
||||||
|
'__REACT_DEVTOOLS_ATTACH__',
|
||||||
|
({
|
||||||
|
enumerable: false,
|
||||||
|
// This property needs to be configurable to allow third-party integrations
|
||||||
|
// to attach their own renderer. Note that using third-party integrations
|
||||||
|
// is not officially supported. Use at your own risk.
|
||||||
|
configurable: true,
|
||||||
|
get() {
|
||||||
|
return attach;
|
||||||
|
},
|
||||||
|
}: Object),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -5,8 +5,7 @@ import {flushSync} from 'react-dom';
|
||||||
import {createRoot} from 'react-dom/client';
|
import {createRoot} from 'react-dom/client';
|
||||||
import Bridge from 'react-devtools-shared/src/bridge';
|
import Bridge from 'react-devtools-shared/src/bridge';
|
||||||
import Store from 'react-devtools-shared/src/devtools/store';
|
import Store from 'react-devtools-shared/src/devtools/store';
|
||||||
import {getBrowserName, getBrowserTheme} from './utils';
|
import {IS_CHROME, IS_EDGE, getBrowserTheme} from './utils';
|
||||||
import {LOCAL_STORAGE_TRACE_UPDATES_ENABLED_KEY} from 'react-devtools-shared/src/constants';
|
|
||||||
import {registerDevToolsEventLogger} from 'react-devtools-shared/src/registerDevToolsEventLogger';
|
import {registerDevToolsEventLogger} from 'react-devtools-shared/src/registerDevToolsEventLogger';
|
||||||
import {
|
import {
|
||||||
getAppendComponentStack,
|
getAppendComponentStack,
|
||||||
|
|
@ -21,15 +20,15 @@ import {
|
||||||
localStorageSetItem,
|
localStorageSetItem,
|
||||||
} from 'react-devtools-shared/src/storage';
|
} from 'react-devtools-shared/src/storage';
|
||||||
import DevTools from 'react-devtools-shared/src/devtools/views/DevTools';
|
import DevTools from 'react-devtools-shared/src/devtools/views/DevTools';
|
||||||
import {__DEBUG__} from 'react-devtools-shared/src/constants';
|
import {
|
||||||
|
__DEBUG__,
|
||||||
|
LOCAL_STORAGE_TRACE_UPDATES_ENABLED_KEY,
|
||||||
|
} from 'react-devtools-shared/src/constants';
|
||||||
import {logEvent} from 'react-devtools-shared/src/Logger';
|
import {logEvent} from 'react-devtools-shared/src/Logger';
|
||||||
|
|
||||||
const LOCAL_STORAGE_SUPPORTS_PROFILING_KEY =
|
const LOCAL_STORAGE_SUPPORTS_PROFILING_KEY =
|
||||||
'React::DevTools::supportsProfiling';
|
'React::DevTools::supportsProfiling';
|
||||||
|
|
||||||
const isChrome = getBrowserName() === 'Chrome';
|
|
||||||
const isEdge = getBrowserName() === 'Edge';
|
|
||||||
|
|
||||||
// rAF never fires on devtools_page (because it's in the background)
|
// rAF never fires on devtools_page (because it's in the background)
|
||||||
// https://bugs.chromium.org/p/chromium/issues/detail?id=1241986#c31
|
// https://bugs.chromium.org/p/chromium/issues/detail?id=1241986#c31
|
||||||
// Since we render React elements here, we need to polyfill it with setTimeout
|
// Since we render React elements here, we need to polyfill it with setTimeout
|
||||||
|
|
@ -176,10 +175,10 @@ function createPanelIfReactLoaded() {
|
||||||
|
|
||||||
store = new Store(bridge, {
|
store = new Store(bridge, {
|
||||||
isProfiling,
|
isProfiling,
|
||||||
supportsReloadAndProfile: isChrome || isEdge,
|
supportsReloadAndProfile: IS_CHROME || IS_EDGE,
|
||||||
supportsProfiling,
|
supportsProfiling,
|
||||||
// At this time, the timeline can only parse Chrome performance profiles.
|
// At this time, the timeline can only parse Chrome performance profiles.
|
||||||
supportsTimeline: isChrome,
|
supportsTimeline: IS_CHROME,
|
||||||
supportsTraceUpdates: true,
|
supportsTraceUpdates: true,
|
||||||
});
|
});
|
||||||
if (!isProfiling) {
|
if (!isProfiling) {
|
||||||
|
|
@ -188,14 +187,26 @@ function createPanelIfReactLoaded() {
|
||||||
|
|
||||||
// Initialize the backend only once the Store has been initialized.
|
// Initialize the backend only once the Store has been initialized.
|
||||||
// Otherwise the Store may miss important initial tree op codes.
|
// Otherwise the Store may miss important initial tree op codes.
|
||||||
chrome.devtools.inspectedWindow.eval(
|
if (IS_CHROME || IS_EDGE) {
|
||||||
`window.postMessage({ source: 'react-devtools-inject-backend' }, '*');`,
|
chrome.runtime.sendMessage({
|
||||||
function (response, evalError) {
|
source: 'react-devtools-main',
|
||||||
if (evalError) {
|
payload: {
|
||||||
console.error(evalError);
|
type: 'react-devtools-inject-backend-manager',
|
||||||
}
|
tabId,
|
||||||
},
|
},
|
||||||
);
|
});
|
||||||
|
} else {
|
||||||
|
// Firefox does not support executing script in ExecutionWorld.MAIN from content script.
|
||||||
|
// see prepareInjection.js
|
||||||
|
chrome.devtools.inspectedWindow.eval(
|
||||||
|
`window.postMessage({ source: 'react-devtools-inject-backend-manager' }, '*');`,
|
||||||
|
function (response, evalError) {
|
||||||
|
if (evalError) {
|
||||||
|
console.error(evalError);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const viewAttributeSourceFunction = (id, path) => {
|
const viewAttributeSourceFunction = (id, path) => {
|
||||||
const rendererID = store.getRendererIDForElement(id);
|
const rendererID = store.getRendererIDForElement(id);
|
||||||
|
|
@ -255,7 +266,7 @@ function createPanelIfReactLoaded() {
|
||||||
// For some reason in Firefox, chrome.runtime.sendMessage() from a content script
|
// For some reason in Firefox, chrome.runtime.sendMessage() from a content script
|
||||||
// never reaches the chrome.runtime.onMessage event listener.
|
// never reaches the chrome.runtime.onMessage event listener.
|
||||||
let fetchFileWithCaching = null;
|
let fetchFileWithCaching = null;
|
||||||
if (isChrome) {
|
if (IS_CHROME) {
|
||||||
const fetchFromNetworkCache = (url, resolve, reject) => {
|
const fetchFromNetworkCache = (url, resolve, reject) => {
|
||||||
// Debug ID allows us to avoid re-logging (potentially long) URL strings below,
|
// Debug ID allows us to avoid re-logging (potentially long) URL strings below,
|
||||||
// while also still associating (potentially) interleaved logs with the original request.
|
// while also still associating (potentially) interleaved logs with the original request.
|
||||||
|
|
@ -463,8 +474,8 @@ function createPanelIfReactLoaded() {
|
||||||
let needsToSyncElementSelection = false;
|
let needsToSyncElementSelection = false;
|
||||||
|
|
||||||
chrome.devtools.panels.create(
|
chrome.devtools.panels.create(
|
||||||
isChrome || isEdge ? '⚛️ Components' : 'Components',
|
IS_CHROME || IS_EDGE ? '⚛️ Components' : 'Components',
|
||||||
'',
|
IS_EDGE ? 'icons/production.svg' : '',
|
||||||
'panel.html',
|
'panel.html',
|
||||||
extensionPanel => {
|
extensionPanel => {
|
||||||
extensionPanel.onShown.addListener(panel => {
|
extensionPanel.onShown.addListener(panel => {
|
||||||
|
|
@ -494,8 +505,8 @@ function createPanelIfReactLoaded() {
|
||||||
);
|
);
|
||||||
|
|
||||||
chrome.devtools.panels.create(
|
chrome.devtools.panels.create(
|
||||||
isChrome || isEdge ? '⚛️ Profiler' : 'Profiler',
|
IS_CHROME || IS_EDGE ? '⚛️ Profiler' : 'Profiler',
|
||||||
'',
|
IS_EDGE ? 'icons/production.svg' : '',
|
||||||
'panel.html',
|
'panel.html',
|
||||||
extensionPanel => {
|
extensionPanel => {
|
||||||
extensionPanel.onShown.addListener(panel => {
|
extensionPanel.onShown.addListener(panel => {
|
||||||
|
|
|
||||||
|
|
@ -41,3 +41,6 @@ export function getBrowserTheme(): BrowserTheme {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const COMPACT_VERSION_NAME = 'compact';
|
||||||
|
export const EXTENSION_CONTAINED_VERSIONS = [COMPACT_VERSION_NAME];
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,7 @@ module.exports = {
|
||||||
},
|
},
|
||||||
output: {
|
output: {
|
||||||
path: __dirname + '/build',
|
path: __dirname + '/build',
|
||||||
filename: 'react_devtools_backend.js',
|
filename: 'react_devtools_backend_compact.js',
|
||||||
},
|
},
|
||||||
node: {
|
node: {
|
||||||
// Don't define a polyfill on window.setImmediate
|
// Don't define a polyfill on window.setImmediate
|
||||||
|
|
|
||||||
|
|
@ -51,10 +51,12 @@ module.exports = {
|
||||||
devtool: __DEV__ ? 'cheap-module-source-map' : false,
|
devtool: __DEV__ ? 'cheap-module-source-map' : false,
|
||||||
entry: {
|
entry: {
|
||||||
background: './src/background.js',
|
background: './src/background.js',
|
||||||
|
backendManager: './src/backendManager.js',
|
||||||
main: './src/main.js',
|
main: './src/main.js',
|
||||||
panel: './src/panel.js',
|
panel: './src/panel.js',
|
||||||
proxy: './src/contentScripts/proxy.js',
|
proxy: './src/contentScripts/proxy.js',
|
||||||
prepareInjection: './src/contentScripts/prepareInjection.js',
|
prepareInjection: './src/contentScripts/prepareInjection.js',
|
||||||
|
renderer: './src/contentScripts/renderer.js',
|
||||||
installHook: './src/contentScripts/installHook.js',
|
installHook: './src/contentScripts/installHook.js',
|
||||||
},
|
},
|
||||||
output: {
|
output: {
|
||||||
|
|
|
||||||
|
|
@ -354,8 +354,6 @@ yarn
|
||||||
./download-experimental-build.js --commit=main
|
./download-experimental-build.js --commit=main
|
||||||
```
|
```
|
||||||
|
|
||||||
Please note that you might need to [create a Circle CI API Token](https://circleci.com/docs/2.0/managing-api-tokens/) and set up an environment variable `CIRCLE_CI_API_TOKEN`.
|
|
||||||
|
|
||||||
## Build steps
|
## Build steps
|
||||||
Once the above packages have been built or downloaded, you can watch for changes made to the source code and automatically rebuild by running:
|
Once the above packages have been built or downloaded, you can watch for changes made to the source code and automatically rebuild by running:
|
||||||
```sh
|
```sh
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "react-devtools-inline",
|
"name": "react-devtools-inline",
|
||||||
"version": "4.27.4",
|
"version": "4.27.8",
|
||||||
"description": "Embed react-devtools within a website",
|
"description": "Embed react-devtools within a website",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"main": "./dist/backend.js",
|
"main": "./dist/backend.js",
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
import Agent from 'react-devtools-shared/src/backend/agent';
|
import Agent from 'react-devtools-shared/src/backend/agent';
|
||||||
import Bridge from 'react-devtools-shared/src/bridge';
|
import Bridge from 'react-devtools-shared/src/bridge';
|
||||||
import {initBackend} from 'react-devtools-shared/src/backend';
|
import {initBackend} from 'react-devtools-shared/src/backend';
|
||||||
|
import {installConsoleFunctionsToWindow} from 'react-devtools-shared/src/backend/console';
|
||||||
import {installHook} from 'react-devtools-shared/src/hook';
|
import {installHook} from 'react-devtools-shared/src/hook';
|
||||||
import setupNativeStyleEditor from 'react-devtools-shared/src/backend/NativeStyleEditor/setupNativeStyleEditor';
|
import setupNativeStyleEditor from 'react-devtools-shared/src/backend/NativeStyleEditor/setupNativeStyleEditor';
|
||||||
|
|
||||||
|
|
@ -119,5 +120,8 @@ export function createBridge(contentWindow: any, wall?: Wall): BackendBridge {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function initialize(contentWindow: any): void {
|
export function initialize(contentWindow: any): void {
|
||||||
|
// Install a global variable to allow patching console early (during injection).
|
||||||
|
// This provides React Native developers with components stacks even if they don't run DevTools.
|
||||||
|
installConsoleFunctionsToWindow();
|
||||||
installHook(contentWindow);
|
installHook(contentWindow);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1243,10 +1243,9 @@ describe('Timeline profiler', () => {
|
||||||
function Example() {
|
function Example() {
|
||||||
const setHigh = React.useState(0)[1];
|
const setHigh = React.useState(0)[1];
|
||||||
const setLow = React.useState(0)[1];
|
const setLow = React.useState(0)[1];
|
||||||
const startTransition = React.useTransition()[1];
|
|
||||||
|
|
||||||
updaterFn = () => {
|
updaterFn = () => {
|
||||||
startTransition(() => {
|
React.startTransition(() => {
|
||||||
setLow(prevLow => prevLow + 1);
|
setLow(prevLow => prevLow + 1);
|
||||||
});
|
});
|
||||||
setHigh(prevHigh => prevHigh + 1);
|
setHigh(prevHigh => prevHigh + 1);
|
||||||
|
|
@ -1265,24 +1264,6 @@ describe('Timeline profiler', () => {
|
||||||
const timelineData = stopProfilingAndGetTimelineData();
|
const timelineData = stopProfilingAndGetTimelineData();
|
||||||
expect(timelineData.schedulingEvents).toMatchInlineSnapshot(`
|
expect(timelineData.schedulingEvents).toMatchInlineSnapshot(`
|
||||||
[
|
[
|
||||||
{
|
|
||||||
"componentName": "Example",
|
|
||||||
"componentStack": "
|
|
||||||
in Example (at **)",
|
|
||||||
"lanes": "0b0000000000000000000000000001000",
|
|
||||||
"timestamp": 10,
|
|
||||||
"type": "schedule-state-update",
|
|
||||||
"warning": null,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"componentName": "Example",
|
|
||||||
"componentStack": "
|
|
||||||
in Example (at **)",
|
|
||||||
"lanes": "0b0000000000000000000000010000000",
|
|
||||||
"timestamp": 10,
|
|
||||||
"type": "schedule-state-update",
|
|
||||||
"warning": null,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"componentName": "Example",
|
"componentName": "Example",
|
||||||
"componentStack": "
|
"componentStack": "
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ function serializeHook(hook) {
|
||||||
// Remove user-specific portions of this file path.
|
// Remove user-specific portions of this file path.
|
||||||
let fileName = hook.hookSource.fileName;
|
let fileName = hook.hookSource.fileName;
|
||||||
const index = fileName.lastIndexOf('/react-devtools-shared/');
|
const index = fileName.lastIndexOf('/react-devtools-shared/');
|
||||||
fileName = fileName.substring(index + 1);
|
fileName = fileName.slice(index + 1);
|
||||||
|
|
||||||
let subHooks = hook.subHooks;
|
let subHooks = hook.subHooks;
|
||||||
if (subHooks) {
|
if (subHooks) {
|
||||||
|
|
|
||||||
|
|
@ -1801,7 +1801,7 @@ describe('InspectedElement', () => {
|
||||||
jest.runOnlyPendingTimers();
|
jest.runOnlyPendingTimers();
|
||||||
expect(global.mockClipboardCopy).toHaveBeenCalledTimes(1);
|
expect(global.mockClipboardCopy).toHaveBeenCalledTimes(1);
|
||||||
expect(global.mockClipboardCopy).toHaveBeenCalledWith(
|
expect(global.mockClipboardCopy).toHaveBeenCalledWith(
|
||||||
JSON.stringify(nestedObject),
|
JSON.stringify(nestedObject, undefined, 2),
|
||||||
);
|
);
|
||||||
|
|
||||||
global.mockClipboardCopy.mockReset();
|
global.mockClipboardCopy.mockReset();
|
||||||
|
|
@ -1811,7 +1811,7 @@ describe('InspectedElement', () => {
|
||||||
jest.runOnlyPendingTimers();
|
jest.runOnlyPendingTimers();
|
||||||
expect(global.mockClipboardCopy).toHaveBeenCalledTimes(1);
|
expect(global.mockClipboardCopy).toHaveBeenCalledTimes(1);
|
||||||
expect(global.mockClipboardCopy).toHaveBeenCalledWith(
|
expect(global.mockClipboardCopy).toHaveBeenCalledWith(
|
||||||
JSON.stringify(nestedObject.a.b),
|
JSON.stringify(nestedObject.a.b, undefined, 2),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -1841,7 +1841,6 @@ describe('InspectedElement', () => {
|
||||||
xyz: 1,
|
xyz: 1,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
// $FlowFixMe
|
|
||||||
const bigInt = BigInt(123); // eslint-disable-line no-undef
|
const bigInt = BigInt(123); // eslint-disable-line no-undef
|
||||||
|
|
||||||
await utils.actAsync(() =>
|
await utils.actAsync(() =>
|
||||||
|
|
@ -1895,7 +1894,7 @@ describe('InspectedElement', () => {
|
||||||
jest.runOnlyPendingTimers();
|
jest.runOnlyPendingTimers();
|
||||||
expect(global.mockClipboardCopy).toHaveBeenCalledTimes(1);
|
expect(global.mockClipboardCopy).toHaveBeenCalledTimes(1);
|
||||||
expect(global.mockClipboardCopy).toHaveBeenCalledWith(
|
expect(global.mockClipboardCopy).toHaveBeenCalledWith(
|
||||||
JSON.stringify('123n'),
|
JSON.stringify('123n', undefined, 2),
|
||||||
);
|
);
|
||||||
|
|
||||||
global.mockClipboardCopy.mockReset();
|
global.mockClipboardCopy.mockReset();
|
||||||
|
|
@ -1905,7 +1904,7 @@ describe('InspectedElement', () => {
|
||||||
jest.runOnlyPendingTimers();
|
jest.runOnlyPendingTimers();
|
||||||
expect(global.mockClipboardCopy).toHaveBeenCalledTimes(1);
|
expect(global.mockClipboardCopy).toHaveBeenCalledTimes(1);
|
||||||
expect(global.mockClipboardCopy).toHaveBeenCalledWith(
|
expect(global.mockClipboardCopy).toHaveBeenCalledWith(
|
||||||
JSON.stringify({0: 100, 1: -100, 2: 0}),
|
JSON.stringify({0: 100, 1: -100, 2: 0}, undefined, 2),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ describe('InspectedElementContext', () => {
|
||||||
|
|
||||||
async function read(
|
async function read(
|
||||||
id: number,
|
id: number,
|
||||||
path?: Array<string | number> = null,
|
path: Array<string | number> = null,
|
||||||
): Promise<Object> {
|
): Promise<Object> {
|
||||||
const rendererID = ((store.getRendererIDForElement(id): any): number);
|
const rendererID = ((store.getRendererIDForElement(id): any): number);
|
||||||
const promise = backendAPI
|
const promise = backendAPI
|
||||||
|
|
@ -826,7 +826,7 @@ describe('InspectedElementContext', () => {
|
||||||
jest.runOnlyPendingTimers();
|
jest.runOnlyPendingTimers();
|
||||||
expect(global.mockClipboardCopy).toHaveBeenCalledTimes(1);
|
expect(global.mockClipboardCopy).toHaveBeenCalledTimes(1);
|
||||||
expect(global.mockClipboardCopy).toHaveBeenCalledWith(
|
expect(global.mockClipboardCopy).toHaveBeenCalledWith(
|
||||||
JSON.stringify(nestedObject),
|
JSON.stringify(nestedObject, undefined, 2),
|
||||||
);
|
);
|
||||||
|
|
||||||
global.mockClipboardCopy.mockReset();
|
global.mockClipboardCopy.mockReset();
|
||||||
|
|
@ -842,7 +842,7 @@ describe('InspectedElementContext', () => {
|
||||||
jest.runOnlyPendingTimers();
|
jest.runOnlyPendingTimers();
|
||||||
expect(global.mockClipboardCopy).toHaveBeenCalledTimes(1);
|
expect(global.mockClipboardCopy).toHaveBeenCalledTimes(1);
|
||||||
expect(global.mockClipboardCopy).toHaveBeenCalledWith(
|
expect(global.mockClipboardCopy).toHaveBeenCalledWith(
|
||||||
JSON.stringify(nestedObject.a.b),
|
JSON.stringify(nestedObject.a.b, undefined, 2),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -873,7 +873,6 @@ describe('InspectedElementContext', () => {
|
||||||
xyz: 1,
|
xyz: 1,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
// $FlowFixMe
|
|
||||||
const bigInt = BigInt(123); // eslint-disable-line no-undef
|
const bigInt = BigInt(123); // eslint-disable-line no-undef
|
||||||
|
|
||||||
act(() =>
|
act(() =>
|
||||||
|
|
@ -933,7 +932,7 @@ describe('InspectedElementContext', () => {
|
||||||
jest.runOnlyPendingTimers();
|
jest.runOnlyPendingTimers();
|
||||||
expect(global.mockClipboardCopy).toHaveBeenCalledTimes(1);
|
expect(global.mockClipboardCopy).toHaveBeenCalledTimes(1);
|
||||||
expect(global.mockClipboardCopy).toHaveBeenCalledWith(
|
expect(global.mockClipboardCopy).toHaveBeenCalledWith(
|
||||||
JSON.stringify({0: 100, 1: -100, 2: 0}),
|
JSON.stringify({0: 100, 1: -100, 2: 0}, undefined, 2),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -938,8 +938,10 @@ describe('ProfilingCache', () => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// @reactVersion >= 18.0
|
// @reactVersion = 17.0
|
||||||
it('should handle unexpectedly shallow suspense trees', () => {
|
it('should handle unexpectedly shallow suspense trees', () => {
|
||||||
|
// This test only runs in v17 because it's a regression test for legacy
|
||||||
|
// Suspense behavior, and the implementation details changed in v18.
|
||||||
const container = document.createElement('div');
|
const container = document.createElement('div');
|
||||||
|
|
||||||
utils.act(() => store.profilerStore.startProfiling());
|
utils.act(() => store.profilerStore.startProfiling());
|
||||||
|
|
@ -963,17 +965,9 @@ describe('ProfilingCache', () => {
|
||||||
2 => 0,
|
2 => 0,
|
||||||
},
|
},
|
||||||
"passiveEffectDuration": null,
|
"passiveEffectDuration": null,
|
||||||
"priorityLevel": "Immediate",
|
"priorityLevel": "Normal",
|
||||||
"timestamp": 0,
|
"timestamp": 0,
|
||||||
"updaters": [
|
"updaters": null,
|
||||||
{
|
|
||||||
"displayName": "render()",
|
|
||||||
"hocDisplayNames": null,
|
|
||||||
"id": 1,
|
|
||||||
"key": null,
|
|
||||||
"type": 11,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
`);
|
`);
|
||||||
|
|
|
||||||
|
|
@ -80,7 +80,6 @@ beforeEach(() => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const originalConsoleError = console.error;
|
const originalConsoleError = console.error;
|
||||||
// $FlowFixMe
|
|
||||||
console.error = (...args) => {
|
console.error = (...args) => {
|
||||||
const firstArg = args[0];
|
const firstArg = args[0];
|
||||||
if (
|
if (
|
||||||
|
|
@ -111,7 +110,6 @@ beforeEach(() => {
|
||||||
originalConsoleError.apply(console, args);
|
originalConsoleError.apply(console, args);
|
||||||
};
|
};
|
||||||
const originalConsoleWarn = console.warn;
|
const originalConsoleWarn = console.warn;
|
||||||
// $FlowFixMe
|
|
||||||
console.warn = (...args) => {
|
console.warn = (...args) => {
|
||||||
if (shouldIgnoreConsoleErrorOrWarn(args)) {
|
if (shouldIgnoreConsoleErrorOrWarn(args)) {
|
||||||
// Allows testing how DevTools behaves when it encounters console.warn without cluttering the test output.
|
// Allows testing how DevTools behaves when it encounters console.warn without cluttering the test output.
|
||||||
|
|
|
||||||
|
|
@ -67,7 +67,6 @@ describe('StoreStressConcurrent', () => {
|
||||||
|
|
||||||
// 1. Render a normal version of [a, b, c, d, e].
|
// 1. Render a normal version of [a, b, c, d, e].
|
||||||
let container = document.createElement('div');
|
let container = document.createElement('div');
|
||||||
// $FlowFixMe
|
|
||||||
let root = ReactDOMClient.createRoot(container);
|
let root = ReactDOMClient.createRoot(container);
|
||||||
act(() => root.render(<Parent>{[a, b, c, d, e]}</Parent>));
|
act(() => root.render(<Parent>{[a, b, c, d, e]}</Parent>));
|
||||||
expect(store).toMatchInlineSnapshot(
|
expect(store).toMatchInlineSnapshot(
|
||||||
|
|
@ -151,7 +150,6 @@ describe('StoreStressConcurrent', () => {
|
||||||
for (let i = 0; i < cases.length; i++) {
|
for (let i = 0; i < cases.length; i++) {
|
||||||
// Ensure fresh mount.
|
// Ensure fresh mount.
|
||||||
container = document.createElement('div');
|
container = document.createElement('div');
|
||||||
// $FlowFixMe
|
|
||||||
root = ReactDOMClient.createRoot(container);
|
root = ReactDOMClient.createRoot(container);
|
||||||
|
|
||||||
// Verify mounting 'abcde'.
|
// Verify mounting 'abcde'.
|
||||||
|
|
@ -181,7 +179,6 @@ describe('StoreStressConcurrent', () => {
|
||||||
// 6. Verify *updates* by reusing the container between iterations.
|
// 6. Verify *updates* by reusing the container between iterations.
|
||||||
// There'll be no unmounting until the very end.
|
// There'll be no unmounting until the very end.
|
||||||
container = document.createElement('div');
|
container = document.createElement('div');
|
||||||
// $FlowFixMe
|
|
||||||
root = ReactDOMClient.createRoot(container);
|
root = ReactDOMClient.createRoot(container);
|
||||||
for (let i = 0; i < cases.length; i++) {
|
for (let i = 0; i < cases.length; i++) {
|
||||||
// Verify mounting 'abcde'.
|
// Verify mounting 'abcde'.
|
||||||
|
|
@ -249,7 +246,6 @@ describe('StoreStressConcurrent', () => {
|
||||||
const snapshots = [];
|
const snapshots = [];
|
||||||
let container = document.createElement('div');
|
let container = document.createElement('div');
|
||||||
for (let i = 0; i < steps.length; i++) {
|
for (let i = 0; i < steps.length; i++) {
|
||||||
// $FlowFixMe
|
|
||||||
const root = ReactDOMClient.createRoot(container);
|
const root = ReactDOMClient.createRoot(container);
|
||||||
act(() => root.render(<Root>{steps[i]}</Root>));
|
act(() => root.render(<Root>{steps[i]}</Root>));
|
||||||
// We snapshot each step once so it doesn't regress.
|
// We snapshot each step once so it doesn't regress.
|
||||||
|
|
@ -321,7 +317,6 @@ describe('StoreStressConcurrent', () => {
|
||||||
for (let i = 0; i < steps.length; i++) {
|
for (let i = 0; i < steps.length; i++) {
|
||||||
for (let j = 0; j < steps.length; j++) {
|
for (let j = 0; j < steps.length; j++) {
|
||||||
container = document.createElement('div');
|
container = document.createElement('div');
|
||||||
// $FlowFixMe
|
|
||||||
const root = ReactDOMClient.createRoot(container);
|
const root = ReactDOMClient.createRoot(container);
|
||||||
act(() => root.render(<Root>{steps[i]}</Root>));
|
act(() => root.render(<Root>{steps[i]}</Root>));
|
||||||
expect(print(store)).toMatch(snapshots[i]);
|
expect(print(store)).toMatch(snapshots[i]);
|
||||||
|
|
@ -338,7 +333,6 @@ describe('StoreStressConcurrent', () => {
|
||||||
for (let i = 0; i < steps.length; i++) {
|
for (let i = 0; i < steps.length; i++) {
|
||||||
for (let j = 0; j < steps.length; j++) {
|
for (let j = 0; j < steps.length; j++) {
|
||||||
container = document.createElement('div');
|
container = document.createElement('div');
|
||||||
// $FlowFixMe
|
|
||||||
const root = ReactDOMClient.createRoot(container);
|
const root = ReactDOMClient.createRoot(container);
|
||||||
act(() =>
|
act(() =>
|
||||||
root.render(
|
root.render(
|
||||||
|
|
@ -412,7 +406,6 @@ describe('StoreStressConcurrent', () => {
|
||||||
const snapshots = [];
|
const snapshots = [];
|
||||||
let container = document.createElement('div');
|
let container = document.createElement('div');
|
||||||
for (let i = 0; i < steps.length; i++) {
|
for (let i = 0; i < steps.length; i++) {
|
||||||
// $FlowFixMe
|
|
||||||
const root = ReactDOMClient.createRoot(container);
|
const root = ReactDOMClient.createRoot(container);
|
||||||
act(() =>
|
act(() =>
|
||||||
root.render(
|
root.render(
|
||||||
|
|
@ -515,7 +508,6 @@ describe('StoreStressConcurrent', () => {
|
||||||
|
|
||||||
// 2. Verify check Suspense can render same steps as initial fallback content.
|
// 2. Verify check Suspense can render same steps as initial fallback content.
|
||||||
for (let i = 0; i < steps.length; i++) {
|
for (let i = 0; i < steps.length; i++) {
|
||||||
// $FlowFixMe
|
|
||||||
const root = ReactDOMClient.createRoot(container);
|
const root = ReactDOMClient.createRoot(container);
|
||||||
act(() =>
|
act(() =>
|
||||||
root.render(
|
root.render(
|
||||||
|
|
@ -540,7 +532,6 @@ describe('StoreStressConcurrent', () => {
|
||||||
for (let j = 0; j < steps.length; j++) {
|
for (let j = 0; j < steps.length; j++) {
|
||||||
// Always start with a fresh container and steps[i].
|
// Always start with a fresh container and steps[i].
|
||||||
container = document.createElement('div');
|
container = document.createElement('div');
|
||||||
// $FlowFixMe
|
|
||||||
const root = ReactDOMClient.createRoot(container);
|
const root = ReactDOMClient.createRoot(container);
|
||||||
act(() =>
|
act(() =>
|
||||||
root.render(
|
root.render(
|
||||||
|
|
@ -586,7 +577,6 @@ describe('StoreStressConcurrent', () => {
|
||||||
for (let j = 0; j < steps.length; j++) {
|
for (let j = 0; j < steps.length; j++) {
|
||||||
// Always start with a fresh container and steps[i].
|
// Always start with a fresh container and steps[i].
|
||||||
container = document.createElement('div');
|
container = document.createElement('div');
|
||||||
// $FlowFixMe
|
|
||||||
const root = ReactDOMClient.createRoot(container);
|
const root = ReactDOMClient.createRoot(container);
|
||||||
act(() =>
|
act(() =>
|
||||||
root.render(
|
root.render(
|
||||||
|
|
@ -644,7 +634,6 @@ describe('StoreStressConcurrent', () => {
|
||||||
for (let j = 0; j < steps.length; j++) {
|
for (let j = 0; j < steps.length; j++) {
|
||||||
// Always start with a fresh container and steps[i].
|
// Always start with a fresh container and steps[i].
|
||||||
container = document.createElement('div');
|
container = document.createElement('div');
|
||||||
// $FlowFixMe
|
|
||||||
const root = ReactDOMClient.createRoot(container);
|
const root = ReactDOMClient.createRoot(container);
|
||||||
act(() =>
|
act(() =>
|
||||||
root.render(
|
root.render(
|
||||||
|
|
@ -694,7 +683,6 @@ describe('StoreStressConcurrent', () => {
|
||||||
for (let j = 0; j < steps.length; j++) {
|
for (let j = 0; j < steps.length; j++) {
|
||||||
// Always start with a fresh container and steps[i].
|
// Always start with a fresh container and steps[i].
|
||||||
container = document.createElement('div');
|
container = document.createElement('div');
|
||||||
// $FlowFixMe
|
|
||||||
const root = ReactDOMClient.createRoot(container);
|
const root = ReactDOMClient.createRoot(container);
|
||||||
act(() =>
|
act(() =>
|
||||||
root.render(
|
root.render(
|
||||||
|
|
@ -748,7 +736,6 @@ describe('StoreStressConcurrent', () => {
|
||||||
for (let j = 0; j < steps.length; j++) {
|
for (let j = 0; j < steps.length; j++) {
|
||||||
// Always start with a fresh container and steps[i].
|
// Always start with a fresh container and steps[i].
|
||||||
container = document.createElement('div');
|
container = document.createElement('div');
|
||||||
// $FlowFixMe
|
|
||||||
const root = ReactDOMClient.createRoot(container);
|
const root = ReactDOMClient.createRoot(container);
|
||||||
act(() =>
|
act(() =>
|
||||||
root.render(
|
root.render(
|
||||||
|
|
@ -904,7 +891,6 @@ describe('StoreStressConcurrent', () => {
|
||||||
const snapshots = [];
|
const snapshots = [];
|
||||||
let container = document.createElement('div');
|
let container = document.createElement('div');
|
||||||
for (let i = 0; i < steps.length; i++) {
|
for (let i = 0; i < steps.length; i++) {
|
||||||
// $FlowFixMe
|
|
||||||
const root = ReactDOMClient.createRoot(container);
|
const root = ReactDOMClient.createRoot(container);
|
||||||
act(() =>
|
act(() =>
|
||||||
root.render(
|
root.render(
|
||||||
|
|
@ -928,7 +914,6 @@ describe('StoreStressConcurrent', () => {
|
||||||
// which is different from the snapshots above. So we take more snapshots.
|
// which is different from the snapshots above. So we take more snapshots.
|
||||||
const fallbackSnapshots = [];
|
const fallbackSnapshots = [];
|
||||||
for (let i = 0; i < steps.length; i++) {
|
for (let i = 0; i < steps.length; i++) {
|
||||||
// $FlowFixMe
|
|
||||||
const root = ReactDOMClient.createRoot(container);
|
const root = ReactDOMClient.createRoot(container);
|
||||||
act(() =>
|
act(() =>
|
||||||
root.render(
|
root.render(
|
||||||
|
|
@ -1062,7 +1047,6 @@ describe('StoreStressConcurrent', () => {
|
||||||
for (let j = 0; j < steps.length; j++) {
|
for (let j = 0; j < steps.length; j++) {
|
||||||
// Always start with a fresh container and steps[i].
|
// Always start with a fresh container and steps[i].
|
||||||
container = document.createElement('div');
|
container = document.createElement('div');
|
||||||
// $FlowFixMe
|
|
||||||
const root = ReactDOMClient.createRoot(container);
|
const root = ReactDOMClient.createRoot(container);
|
||||||
act(() =>
|
act(() =>
|
||||||
root.render(
|
root.render(
|
||||||
|
|
@ -1114,7 +1098,6 @@ describe('StoreStressConcurrent', () => {
|
||||||
for (let j = 0; j < steps.length; j++) {
|
for (let j = 0; j < steps.length; j++) {
|
||||||
// Always start with a fresh container and steps[i].
|
// Always start with a fresh container and steps[i].
|
||||||
container = document.createElement('div');
|
container = document.createElement('div');
|
||||||
// $FlowFixMe
|
|
||||||
const root = ReactDOMClient.createRoot(container);
|
const root = ReactDOMClient.createRoot(container);
|
||||||
act(() =>
|
act(() =>
|
||||||
root.render(
|
root.render(
|
||||||
|
|
@ -1181,7 +1164,6 @@ describe('StoreStressConcurrent', () => {
|
||||||
for (let j = 0; j < steps.length; j++) {
|
for (let j = 0; j < steps.length; j++) {
|
||||||
// Always start with a fresh container and steps[i].
|
// Always start with a fresh container and steps[i].
|
||||||
container = document.createElement('div');
|
container = document.createElement('div');
|
||||||
// $FlowFixMe
|
|
||||||
const root = ReactDOMClient.createRoot(container);
|
const root = ReactDOMClient.createRoot(container);
|
||||||
act(() =>
|
act(() =>
|
||||||
root.render(
|
root.render(
|
||||||
|
|
@ -1233,7 +1215,6 @@ describe('StoreStressConcurrent', () => {
|
||||||
for (let j = 0; j < steps.length; j++) {
|
for (let j = 0; j < steps.length; j++) {
|
||||||
// Always start with a fresh container and steps[i].
|
// Always start with a fresh container and steps[i].
|
||||||
container = document.createElement('div');
|
container = document.createElement('div');
|
||||||
// $FlowFixMe
|
|
||||||
const root = ReactDOMClient.createRoot(container);
|
const root = ReactDOMClient.createRoot(container);
|
||||||
act(() =>
|
act(() =>
|
||||||
root.render(
|
root.render(
|
||||||
|
|
@ -1285,7 +1266,6 @@ describe('StoreStressConcurrent', () => {
|
||||||
for (let j = 0; j < steps.length; j++) {
|
for (let j = 0; j < steps.length; j++) {
|
||||||
// Always start with a fresh container and steps[i].
|
// Always start with a fresh container and steps[i].
|
||||||
container = document.createElement('div');
|
container = document.createElement('div');
|
||||||
// $FlowFixMe
|
|
||||||
const root = ReactDOMClient.createRoot(container);
|
const root = ReactDOMClient.createRoot(container);
|
||||||
act(() =>
|
act(() =>
|
||||||
root.render(
|
root.render(
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@
|
||||||
import {
|
import {
|
||||||
getDisplayName,
|
getDisplayName,
|
||||||
getDisplayNameForReactElement,
|
getDisplayNameForReactElement,
|
||||||
|
isPlainObject,
|
||||||
} from 'react-devtools-shared/src/utils';
|
} from 'react-devtools-shared/src/utils';
|
||||||
import {stackToComponentSources} from 'react-devtools-shared/src/devtools/utils';
|
import {stackToComponentSources} from 'react-devtools-shared/src/devtools/utils';
|
||||||
import {
|
import {
|
||||||
|
|
@ -270,4 +271,30 @@ describe('utils', () => {
|
||||||
expect(gte('10.0.0', '9.0.0')).toBe(true);
|
expect(gte('10.0.0', '9.0.0')).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('isPlainObject', () => {
|
||||||
|
it('should return true for plain objects', () => {
|
||||||
|
expect(isPlainObject({})).toBe(true);
|
||||||
|
expect(isPlainObject({a: 1})).toBe(true);
|
||||||
|
expect(isPlainObject({a: {b: {c: 123}}})).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false if object is a class instance', () => {
|
||||||
|
expect(isPlainObject(new (class C {})())).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should retun false for objects, which have not only Object in its prototype chain', () => {
|
||||||
|
expect(isPlainObject([])).toBe(false);
|
||||||
|
expect(isPlainObject(Symbol())).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should retun false for primitives', () => {
|
||||||
|
expect(isPlainObject(5)).toBe(false);
|
||||||
|
expect(isPlainObject(true)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true for objects with no prototype', () => {
|
||||||
|
expect(isPlainObject(Object.create(null))).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,6 @@ export async function actAsync(
|
||||||
const {act: actTestRenderer} = require('react-test-renderer');
|
const {act: actTestRenderer} = require('react-test-renderer');
|
||||||
const {act: actDOM} = require('react-dom/test-utils');
|
const {act: actDOM} = require('react-dom/test-utils');
|
||||||
|
|
||||||
// $FlowFixMe Flow doesn't know about "await act()" yet
|
|
||||||
await actDOM(async () => {
|
await actDOM(async () => {
|
||||||
await actTestRenderer(async () => {
|
await actTestRenderer(async () => {
|
||||||
await cb();
|
await cb();
|
||||||
|
|
@ -55,7 +54,6 @@ export async function actAsync(
|
||||||
|
|
||||||
if (recursivelyFlush) {
|
if (recursivelyFlush) {
|
||||||
while (jest.getTimerCount() > 0) {
|
while (jest.getTimerCount() > 0) {
|
||||||
// $FlowFixMe Flow doesn't know about "await act()" yet
|
|
||||||
await actDOM(async () => {
|
await actDOM(async () => {
|
||||||
await actTestRenderer(async () => {
|
await actTestRenderer(async () => {
|
||||||
jest.runAllTimers();
|
jest.runAllTimers();
|
||||||
|
|
@ -63,7 +61,6 @@ export async function actAsync(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// $FlowFixMe Flow doesn't know about "await act()" yet
|
|
||||||
await actDOM(async () => {
|
await actDOM(async () => {
|
||||||
await actTestRenderer(async () => {
|
await actTestRenderer(async () => {
|
||||||
jest.runOnlyPendingTimers();
|
jest.runOnlyPendingTimers();
|
||||||
|
|
|
||||||
|
|
@ -77,7 +77,7 @@ export function describeNativeComponentFrame(
|
||||||
let control;
|
let control;
|
||||||
|
|
||||||
const previousPrepareStackTrace = Error.prepareStackTrace;
|
const previousPrepareStackTrace = Error.prepareStackTrace;
|
||||||
// $FlowFixMe It does accept undefined.
|
// $FlowFixMe[incompatible-type] It does accept undefined.
|
||||||
Error.prepareStackTrace = undefined;
|
Error.prepareStackTrace = undefined;
|
||||||
|
|
||||||
reentry = true;
|
reentry = true;
|
||||||
|
|
@ -98,7 +98,7 @@ export function describeNativeComponentFrame(
|
||||||
const Fake = function () {
|
const Fake = function () {
|
||||||
throw Error();
|
throw Error();
|
||||||
};
|
};
|
||||||
// $FlowFixMe
|
// $FlowFixMe[prop-missing]
|
||||||
Object.defineProperty(Fake.prototype, 'props', {
|
Object.defineProperty(Fake.prototype, 'props', {
|
||||||
set: function () {
|
set: function () {
|
||||||
// We use a throwing setter instead of frozen or non-writable props
|
// We use a throwing setter instead of frozen or non-writable props
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,7 @@ export function disableLogs(): void {
|
||||||
value: disabledLog,
|
value: disabledLog,
|
||||||
writable: true,
|
writable: true,
|
||||||
};
|
};
|
||||||
// $FlowFixMe Flow thinks console is immutable.
|
// $FlowFixMe[cannot-write] Flow thinks console is immutable.
|
||||||
Object.defineProperties(console, {
|
Object.defineProperties(console, {
|
||||||
info: props,
|
info: props,
|
||||||
log: props,
|
log: props,
|
||||||
|
|
@ -69,7 +69,7 @@ export function reenableLogs(): void {
|
||||||
enumerable: true,
|
enumerable: true,
|
||||||
writable: true,
|
writable: true,
|
||||||
};
|
};
|
||||||
// $FlowFixMe Flow thinks console is immutable.
|
// $FlowFixMe[cannot-write] Flow thinks console is immutable.
|
||||||
Object.defineProperties(console, {
|
Object.defineProperties(console, {
|
||||||
log: {...props, value: prevLog},
|
log: {...props, value: prevLog},
|
||||||
info: {...props, value: prevInfo},
|
info: {...props, value: prevInfo},
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ import type {RendererID} from '../types';
|
||||||
import type {StyleAndLayout} from './types';
|
import type {StyleAndLayout} from './types';
|
||||||
|
|
||||||
export type ResolveNativeStyle = (stylesheetID: any) => ?Object;
|
export type ResolveNativeStyle = (stylesheetID: any) => ?Object;
|
||||||
|
export type SetupNativeStyleEditor = typeof setupNativeStyleEditor;
|
||||||
|
|
||||||
export default function setupNativeStyleEditor(
|
export default function setupNativeStyleEditor(
|
||||||
bridge: BackendBridge,
|
bridge: BackendBridge,
|
||||||
|
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
/**
|
|
||||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
||||||
*
|
|
||||||
* This source code is licensed under the MIT license found in the
|
|
||||||
* LICENSE file in the root directory of this source tree.
|
|
||||||
*
|
|
||||||
* @flow
|
|
||||||
*/
|
|
||||||
|
|
||||||
// This list of flags must be synced with the following file:
|
|
||||||
// https://github.com/facebook/react/blob/main/packages/react-reconciler/src/ReactFiberFlags.js
|
|
||||||
|
|
||||||
export const NoFlags = /* */ 0b000000000000000000000000000;
|
|
||||||
export const PerformedWork = /* */ 0b000000000000000000000000001;
|
|
||||||
export const Placement = /* */ 0b000000000000000000000000010;
|
|
||||||
export const DidCapture = /* */ 0b000000000000000000010000000;
|
|
||||||
export const Hydrating = /* */ 0b000000000000001000000000000;
|
|
||||||
|
|
@ -25,7 +25,7 @@ import {
|
||||||
initialize as setupTraceUpdates,
|
initialize as setupTraceUpdates,
|
||||||
toggleEnabled as setTraceUpdatesEnabled,
|
toggleEnabled as setTraceUpdatesEnabled,
|
||||||
} from './views/TraceUpdates';
|
} from './views/TraceUpdates';
|
||||||
import {patch as patchConsole, type ConsolePatchSettings} from './console';
|
import {patch as patchConsole} from './console';
|
||||||
import {currentBridgeProtocol} from 'react-devtools-shared/src/bridge';
|
import {currentBridgeProtocol} from 'react-devtools-shared/src/bridge';
|
||||||
|
|
||||||
import type {BackendBridge} from 'react-devtools-shared/src/bridge';
|
import type {BackendBridge} from 'react-devtools-shared/src/bridge';
|
||||||
|
|
@ -37,10 +37,13 @@ import type {
|
||||||
PathMatch,
|
PathMatch,
|
||||||
RendererID,
|
RendererID,
|
||||||
RendererInterface,
|
RendererInterface,
|
||||||
|
ConsolePatchSettings,
|
||||||
} from './types';
|
} from './types';
|
||||||
import type {ComponentFilter} from '../types';
|
import type {
|
||||||
|
ComponentFilter,
|
||||||
|
BrowserTheme,
|
||||||
|
} from 'react-devtools-shared/src/types';
|
||||||
import {isSynchronousXHRSupported} from './utils';
|
import {isSynchronousXHRSupported} from './utils';
|
||||||
import type {BrowserTheme} from 'react-devtools-shared/src/devtools/views/DevTools';
|
|
||||||
|
|
||||||
const debug = (methodName: string, ...args: Array<string>) => {
|
const debug = (methodName: string, ...args: Array<string>) => {
|
||||||
if (__DEBUG__) {
|
if (__DEBUG__) {
|
||||||
|
|
@ -297,7 +300,13 @@ export default class Agent extends EventEmitter<{
|
||||||
if (renderer == null) {
|
if (renderer == null) {
|
||||||
console.warn(`Invalid renderer id "${rendererID}" for element "${id}"`);
|
console.warn(`Invalid renderer id "${rendererID}" for element "${id}"`);
|
||||||
} else {
|
} else {
|
||||||
renderer.copyElementPath(id, path);
|
const value = renderer.getSerializedElementValueByPath(id, path);
|
||||||
|
|
||||||
|
if (value != null) {
|
||||||
|
this._bridge.send('saveToClipboard', value);
|
||||||
|
} else {
|
||||||
|
console.warn(`Unable to obtain serialized value for element "${id}"`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,12 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type {Fiber} from 'react-reconciler/src/ReactInternalTypes';
|
import type {Fiber} from 'react-reconciler/src/ReactInternalTypes';
|
||||||
import type {CurrentDispatcherRef, ReactRenderer, WorkTagMap} from './types';
|
import type {
|
||||||
import type {BrowserTheme} from 'react-devtools-shared/src/devtools/views/DevTools';
|
CurrentDispatcherRef,
|
||||||
|
ReactRenderer,
|
||||||
|
WorkTagMap,
|
||||||
|
ConsolePatchSettings,
|
||||||
|
} from './types';
|
||||||
import {format, formatWithStyles} from './utils';
|
import {format, formatWithStyles} from './utils';
|
||||||
|
|
||||||
import {getInternalReactConstants} from './renderer';
|
import {getInternalReactConstants} from './renderer';
|
||||||
|
|
@ -136,7 +140,7 @@ export function registerRenderer(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const consoleSettingsRef = {
|
const consoleSettingsRef: ConsolePatchSettings = {
|
||||||
appendComponentStack: false,
|
appendComponentStack: false,
|
||||||
breakOnConsoleErrors: false,
|
breakOnConsoleErrors: false,
|
||||||
showInlineWarningsAndErrors: false,
|
showInlineWarningsAndErrors: false,
|
||||||
|
|
@ -144,14 +148,6 @@ const consoleSettingsRef = {
|
||||||
browserTheme: 'dark',
|
browserTheme: 'dark',
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ConsolePatchSettings = {
|
|
||||||
appendComponentStack: boolean,
|
|
||||||
breakOnConsoleErrors: boolean,
|
|
||||||
showInlineWarningsAndErrors: boolean,
|
|
||||||
hideConsoleLogsInStrictMode: boolean,
|
|
||||||
browserTheme: BrowserTheme,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Patches console methods to append component stack for the current fiber.
|
// Patches console methods to append component stack for the current fiber.
|
||||||
// Call unpatch() to remove the injected behavior.
|
// Call unpatch() to remove the injected behavior.
|
||||||
export function patch({
|
export function patch({
|
||||||
|
|
@ -413,3 +409,10 @@ export function writeConsolePatchSettingsToWindow(
|
||||||
settings.hideConsoleLogsInStrictMode;
|
settings.hideConsoleLogsInStrictMode;
|
||||||
window.__REACT_DEVTOOLS_BROWSER_THEME__ = settings.browserTheme;
|
window.__REACT_DEVTOOLS_BROWSER_THEME__ = settings.browserTheme;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function installConsoleFunctionsToWindow(): void {
|
||||||
|
window.__REACT_DEVTOOLS_CONSOLE_FUNCTIONS__ = {
|
||||||
|
patchConsoleUsingWindowValues,
|
||||||
|
registerRendererWithConsole: registerRenderer,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,9 +11,17 @@ import Agent from './agent';
|
||||||
|
|
||||||
import {attach} from './renderer';
|
import {attach} from './renderer';
|
||||||
import {attach as attachLegacy} from './legacy/renderer';
|
import {attach as attachLegacy} from './legacy/renderer';
|
||||||
|
import {hasAssignedBackend} from './utils';
|
||||||
|
|
||||||
import type {DevToolsHook, ReactRenderer, RendererInterface} from './types';
|
import type {DevToolsHook, ReactRenderer, RendererInterface} from './types';
|
||||||
|
|
||||||
|
// this is the backend that is compatible with all older React versions
|
||||||
|
function isMatchingRender(version: string): boolean {
|
||||||
|
return !hasAssignedBackend(version);
|
||||||
|
}
|
||||||
|
|
||||||
|
export type InitBackend = typeof initBackend;
|
||||||
|
|
||||||
export function initBackend(
|
export function initBackend(
|
||||||
hook: DevToolsHook,
|
hook: DevToolsHook,
|
||||||
agent: Agent,
|
agent: Agent,
|
||||||
|
|
@ -23,6 +31,7 @@ export function initBackend(
|
||||||
// DevTools didn't get injected into this page (maybe b'c of the contentType).
|
// DevTools didn't get injected into this page (maybe b'c of the contentType).
|
||||||
return () => {};
|
return () => {};
|
||||||
}
|
}
|
||||||
|
|
||||||
const subs = [
|
const subs = [
|
||||||
hook.sub(
|
hook.sub(
|
||||||
'renderer-attached',
|
'renderer-attached',
|
||||||
|
|
@ -56,6 +65,10 @@ export function initBackend(
|
||||||
];
|
];
|
||||||
|
|
||||||
const attachRenderer = (id: number, renderer: ReactRenderer) => {
|
const attachRenderer = (id: number, renderer: ReactRenderer) => {
|
||||||
|
// only attach if the renderer is compatible with the current version of the backend
|
||||||
|
if (!isMatchingRender(renderer.reconcilerVersion || renderer.version)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
let rendererInterface = hook.rendererInterfaces.get(id);
|
let rendererInterface = hook.rendererInterfaces.get(id);
|
||||||
|
|
||||||
// Inject any not-yet-injected renderers (if we didn't reload-and-profile)
|
// Inject any not-yet-injected renderers (if we didn't reload-and-profile)
|
||||||
|
|
@ -76,8 +89,7 @@ export function initBackend(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Notify the DevTools frontend about new renderers.
|
// Notify the DevTools frontend about new renderers.
|
||||||
// This includes any that were attached early
|
// This includes any that were attached early (via __REACT_DEVTOOLS_ATTACH__).
|
||||||
// (when SESSION_STORAGE_RELOAD_AND_PROFILE_KEY is set to true).
|
|
||||||
if (rendererInterface != null) {
|
if (rendererInterface != null) {
|
||||||
hook.emit('renderer-attached', {
|
hook.emit('renderer-attached', {
|
||||||
id,
|
id,
|
||||||
|
|
|
||||||
|
|
@ -17,10 +17,10 @@ import {
|
||||||
import {getUID, utfEncodeString, printOperationsArray} from '../../utils';
|
import {getUID, utfEncodeString, printOperationsArray} from '../../utils';
|
||||||
import {
|
import {
|
||||||
cleanForBridge,
|
cleanForBridge,
|
||||||
copyToClipboard,
|
|
||||||
copyWithDelete,
|
copyWithDelete,
|
||||||
copyWithRename,
|
copyWithRename,
|
||||||
copyWithSet,
|
copyWithSet,
|
||||||
|
serializeToString,
|
||||||
} from '../utils';
|
} from '../utils';
|
||||||
import {
|
import {
|
||||||
deletePathInObject,
|
deletePathInObject,
|
||||||
|
|
@ -244,7 +244,6 @@ export function attach(
|
||||||
parentIDStack.pop();
|
parentIDStack.pop();
|
||||||
return result;
|
return result;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// $FlowFixMe[incompatible-type] found when upgrading Flow
|
|
||||||
parentIDStack = [];
|
parentIDStack = [];
|
||||||
throw err;
|
throw err;
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -281,7 +280,6 @@ export function attach(
|
||||||
parentIDStack.pop();
|
parentIDStack.pop();
|
||||||
return result;
|
return result;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// $FlowFixMe[incompatible-type] found when upgrading Flow
|
|
||||||
parentIDStack = [];
|
parentIDStack = [];
|
||||||
throw err;
|
throw err;
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -318,7 +316,6 @@ export function attach(
|
||||||
parentIDStack.pop();
|
parentIDStack.pop();
|
||||||
return result;
|
return result;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// $FlowFixMe[incompatible-type] found when upgrading Flow
|
|
||||||
parentIDStack = [];
|
parentIDStack = [];
|
||||||
throw err;
|
throw err;
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -350,7 +347,6 @@ export function attach(
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// $FlowFixMe[incompatible-type] found when upgrading Flow
|
|
||||||
parentIDStack = [];
|
parentIDStack = [];
|
||||||
throw err;
|
throw err;
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -701,10 +697,15 @@ export function attach(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function copyElementPath(id: number, path: Array<string | number>): void {
|
function getSerializedElementValueByPath(
|
||||||
|
id: number,
|
||||||
|
path: Array<string | number>,
|
||||||
|
): ?string {
|
||||||
const inspectedElement = inspectElementRaw(id);
|
const inspectedElement = inspectElementRaw(id);
|
||||||
if (inspectedElement !== null) {
|
if (inspectedElement !== null) {
|
||||||
copyToClipboard(getInObject(inspectedElement, path));
|
const valueToCopy = getInObject(inspectedElement, path);
|
||||||
|
|
||||||
|
return serializeToString(valueToCopy);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1105,7 +1106,7 @@ export function attach(
|
||||||
clearErrorsForFiberID,
|
clearErrorsForFiberID,
|
||||||
clearWarningsForFiberID,
|
clearWarningsForFiberID,
|
||||||
cleanup,
|
cleanup,
|
||||||
copyElementPath,
|
getSerializedElementValueByPath,
|
||||||
deletePath,
|
deletePath,
|
||||||
flushInitialOperations,
|
flushInitialOperations,
|
||||||
getBestMatchForTrackedPath,
|
getBestMatchForTrackedPath,
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,10 @@ let supportsUserTiming =
|
||||||
let supportsUserTimingV3 = false;
|
let supportsUserTimingV3 = false;
|
||||||
if (supportsUserTiming) {
|
if (supportsUserTiming) {
|
||||||
const CHECK_V3_MARK = '__v3';
|
const CHECK_V3_MARK = '__v3';
|
||||||
const markOptions = ({}: {startTime?: number});
|
const markOptions: {
|
||||||
|
detail?: mixed,
|
||||||
|
startTime?: number,
|
||||||
|
} = {};
|
||||||
Object.defineProperty(markOptions, 'startTime', {
|
Object.defineProperty(markOptions, 'startTime', {
|
||||||
get: function () {
|
get: function () {
|
||||||
supportsUserTimingV3 = true;
|
supportsUserTimingV3 = true;
|
||||||
|
|
@ -64,7 +67,6 @@ if (supportsUserTiming) {
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// $FlowFixMe: Flow expects the User Timing level 2 API.
|
|
||||||
performance.mark(CHECK_V3_MARK, markOptions);
|
performance.mark(CHECK_V3_MARK, markOptions);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Ignore
|
// Ignore
|
||||||
|
|
@ -281,7 +283,7 @@ export function createProfilingHooks({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// $FlowFixMe This property should not be writable outside of this function.
|
// $FlowFixMe[cannot-write] This property should not be writable outside of this function.
|
||||||
top.duration = currentTime - top.timestamp;
|
top.duration = currentTime - top.timestamp;
|
||||||
|
|
||||||
if (currentTimelineData) {
|
if (currentTimelineData) {
|
||||||
|
|
@ -590,7 +592,7 @@ export function createProfilingHooks({
|
||||||
|
|
||||||
const PossiblyWeakMap = typeof WeakMap === 'function' ? WeakMap : Map;
|
const PossiblyWeakMap = typeof WeakMap === 'function' ? WeakMap : Map;
|
||||||
|
|
||||||
// $FlowFixMe: Flow cannot handle polymorphic WeakMaps
|
// $FlowFixMe[incompatible-type]: Flow cannot handle polymorphic WeakMaps
|
||||||
const wakeableIDs: WeakMap<Wakeable, number> = new PossiblyWeakMap();
|
const wakeableIDs: WeakMap<Wakeable, number> = new PossiblyWeakMap();
|
||||||
let wakeableID: number = 0;
|
let wakeableID: number = 0;
|
||||||
function getWakeableID(wakeable: Wakeable): number {
|
function getWakeableID(wakeable: Wakeable): number {
|
||||||
|
|
|
||||||
|
|
@ -38,10 +38,13 @@ import {
|
||||||
utfEncodeString,
|
utfEncodeString,
|
||||||
} from 'react-devtools-shared/src/utils';
|
} from 'react-devtools-shared/src/utils';
|
||||||
import {sessionStorageGetItem} from 'react-devtools-shared/src/storage';
|
import {sessionStorageGetItem} from 'react-devtools-shared/src/storage';
|
||||||
import {gt, gte} from 'react-devtools-shared/src/backend/utils';
|
import {
|
||||||
|
gt,
|
||||||
|
gte,
|
||||||
|
serializeToString,
|
||||||
|
} from 'react-devtools-shared/src/backend/utils';
|
||||||
import {
|
import {
|
||||||
cleanForBridge,
|
cleanForBridge,
|
||||||
copyToClipboard,
|
|
||||||
copyWithDelete,
|
copyWithDelete,
|
||||||
copyWithRename,
|
copyWithRename,
|
||||||
copyWithSet,
|
copyWithSet,
|
||||||
|
|
@ -88,18 +91,8 @@ import {
|
||||||
MEMO_SYMBOL_STRING,
|
MEMO_SYMBOL_STRING,
|
||||||
SERVER_CONTEXT_SYMBOL_STRING,
|
SERVER_CONTEXT_SYMBOL_STRING,
|
||||||
} from './ReactSymbols';
|
} from './ReactSymbols';
|
||||||
import {
|
|
||||||
DidCapture,
|
|
||||||
NoFlags,
|
|
||||||
PerformedWork,
|
|
||||||
Placement,
|
|
||||||
Hydrating,
|
|
||||||
} from './ReactFiberFlags';
|
|
||||||
import {format} from './utils';
|
import {format} from './utils';
|
||||||
import {
|
import {enableStyleXFeatures} from 'react-devtools-feature-flags';
|
||||||
enableProfilerChangedHookIndices,
|
|
||||||
enableStyleXFeatures,
|
|
||||||
} from 'react-devtools-feature-flags';
|
|
||||||
import is from 'shared/objectIs';
|
import is from 'shared/objectIs';
|
||||||
import hasOwnProperty from 'shared/hasOwnProperty';
|
import hasOwnProperty from 'shared/hasOwnProperty';
|
||||||
import {getStyleXData} from './StyleX/utils';
|
import {getStyleXData} from './StyleX/utils';
|
||||||
|
|
@ -384,7 +377,7 @@ export function getInternalReactConstants(version: string): {
|
||||||
typeof type === 'object' && type !== null ? type.$$typeof : type;
|
typeof type === 'object' && type !== null ? type.$$typeof : type;
|
||||||
|
|
||||||
return typeof symbolOrNumber === 'symbol'
|
return typeof symbolOrNumber === 'symbol'
|
||||||
? // $FlowFixMe `toString()` doesn't match the type signature?
|
? // $FlowFixMe[incompatible-return] `toString()` doesn't match the type signature?
|
||||||
symbolOrNumber.toString()
|
symbolOrNumber.toString()
|
||||||
: symbolOrNumber;
|
: symbolOrNumber;
|
||||||
}
|
}
|
||||||
|
|
@ -816,7 +809,7 @@ export function attach(
|
||||||
name: string,
|
name: string,
|
||||||
fiber: Fiber,
|
fiber: Fiber,
|
||||||
parentFiber: ?Fiber,
|
parentFiber: ?Fiber,
|
||||||
extraString?: string = '',
|
extraString: string = '',
|
||||||
): void => {
|
): void => {
|
||||||
if (__DEBUG__) {
|
if (__DEBUG__) {
|
||||||
const displayName =
|
const displayName =
|
||||||
|
|
@ -1269,19 +1262,12 @@ export function attach(
|
||||||
};
|
};
|
||||||
|
|
||||||
// Only traverse the hooks list once, depending on what info we're returning.
|
// Only traverse the hooks list once, depending on what info we're returning.
|
||||||
if (enableProfilerChangedHookIndices) {
|
const indices = getChangedHooksIndices(
|
||||||
const indices = getChangedHooksIndices(
|
prevFiber.memoizedState,
|
||||||
prevFiber.memoizedState,
|
nextFiber.memoizedState,
|
||||||
nextFiber.memoizedState,
|
);
|
||||||
);
|
data.hooks = indices;
|
||||||
data.hooks = indices;
|
data.didHooksChange = indices !== null && indices.length > 0;
|
||||||
data.didHooksChange = indices !== null && indices.length > 0;
|
|
||||||
} else {
|
|
||||||
data.didHooksChange = didHooksChange(
|
|
||||||
prevFiber.memoizedState,
|
|
||||||
nextFiber.memoizedState,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
@ -1462,12 +1448,13 @@ export function attach(
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function didHooksChange(prev: any, next: any): boolean {
|
function getChangedHooksIndices(prev: any, next: any): null | Array<number> {
|
||||||
if (prev == null || next == null) {
|
if (prev == null || next == null) {
|
||||||
return false;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// We can't report anything meaningful for hooks changes.
|
const indices = [];
|
||||||
|
let index = 0;
|
||||||
if (
|
if (
|
||||||
next.hasOwnProperty('baseState') &&
|
next.hasOwnProperty('baseState') &&
|
||||||
next.hasOwnProperty('memoizedState') &&
|
next.hasOwnProperty('memoizedState') &&
|
||||||
|
|
@ -1476,45 +1463,15 @@ export function attach(
|
||||||
) {
|
) {
|
||||||
while (next !== null) {
|
while (next !== null) {
|
||||||
if (didStatefulHookChange(prev, next)) {
|
if (didStatefulHookChange(prev, next)) {
|
||||||
return true;
|
indices.push(index);
|
||||||
} else {
|
|
||||||
next = next.next;
|
|
||||||
prev = prev.next;
|
|
||||||
}
|
}
|
||||||
|
next = next.next;
|
||||||
|
prev = prev.next;
|
||||||
|
index++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return indices;
|
||||||
}
|
|
||||||
|
|
||||||
function getChangedHooksIndices(prev: any, next: any): null | Array<number> {
|
|
||||||
if (enableProfilerChangedHookIndices) {
|
|
||||||
if (prev == null || next == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const indices = [];
|
|
||||||
let index = 0;
|
|
||||||
if (
|
|
||||||
next.hasOwnProperty('baseState') &&
|
|
||||||
next.hasOwnProperty('memoizedState') &&
|
|
||||||
next.hasOwnProperty('next') &&
|
|
||||||
next.hasOwnProperty('queue')
|
|
||||||
) {
|
|
||||||
while (next !== null) {
|
|
||||||
if (didStatefulHookChange(prev, next)) {
|
|
||||||
indices.push(index);
|
|
||||||
}
|
|
||||||
next = next.next;
|
|
||||||
prev = prev.next;
|
|
||||||
index++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return indices;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getChangedKeys(prev: any, next: any): null | Array<string> {
|
function getChangedKeys(prev: any, next: any): null | Array<string> {
|
||||||
|
|
@ -1555,7 +1512,10 @@ export function attach(
|
||||||
case ForwardRef:
|
case ForwardRef:
|
||||||
// For types that execute user code, we check PerformedWork effect.
|
// For types that execute user code, we check PerformedWork effect.
|
||||||
// We don't reflect bailouts (either referential or sCU) in DevTools.
|
// We don't reflect bailouts (either referential or sCU) in DevTools.
|
||||||
// eslint-disable-next-line no-bitwise
|
// TODO: This flag is a leaked implementation detail. Once we start
|
||||||
|
// releasing DevTools in lockstep with React, we should import a
|
||||||
|
// function from the reconciler instead.
|
||||||
|
const PerformedWork = 0b000000000000000000000000001;
|
||||||
return (getFiberFlags(nextFiber) & PerformedWork) === PerformedWork;
|
return (getFiberFlags(nextFiber) & PerformedWork) === PerformedWork;
|
||||||
// Note: ContextConsumer only gets PerformedWork effect in 16.3.3+
|
// Note: ContextConsumer only gets PerformedWork effect in 16.3.3+
|
||||||
// so it won't get highlighted with React 16.3.0 to 16.3.2.
|
// so it won't get highlighted with React 16.3.0 to 16.3.2.
|
||||||
|
|
@ -2843,7 +2803,12 @@ export function attach(
|
||||||
let nextNode: Fiber = node;
|
let nextNode: Fiber = node;
|
||||||
do {
|
do {
|
||||||
node = nextNode;
|
node = nextNode;
|
||||||
if ((node.flags & (Placement | Hydrating)) !== NoFlags) {
|
// TODO: This function, and these flags, are a leaked implementation
|
||||||
|
// detail. Once we start releasing DevTools in lockstep with React, we
|
||||||
|
// should import a function from the reconciler instead.
|
||||||
|
const Placement = 0b000000000000000000000000010;
|
||||||
|
const Hydrating = 0b000000000000001000000000000;
|
||||||
|
if ((node.flags & (Placement | Hydrating)) !== 0) {
|
||||||
// This is an insertion or in-progress hydration. The nearest possible
|
// This is an insertion or in-progress hydration. The nearest possible
|
||||||
// mounted fiber is the parent but we need to continue to figure out
|
// mounted fiber is the parent but we need to continue to figure out
|
||||||
// if that one is still mounted.
|
// if that one is still mounted.
|
||||||
|
|
@ -3300,16 +3265,21 @@ export function attach(
|
||||||
const errors = fiberIDToErrorsMap.get(id) || new Map();
|
const errors = fiberIDToErrorsMap.get(id) || new Map();
|
||||||
const warnings = fiberIDToWarningsMap.get(id) || new Map();
|
const warnings = fiberIDToWarningsMap.get(id) || new Map();
|
||||||
|
|
||||||
const isErrored =
|
let isErrored = false;
|
||||||
(fiber.flags & DidCapture) !== NoFlags ||
|
|
||||||
forceErrorForFiberIDs.get(id) === true;
|
|
||||||
|
|
||||||
let targetErrorBoundaryID;
|
let targetErrorBoundaryID;
|
||||||
if (isErrorBoundary(fiber)) {
|
if (isErrorBoundary(fiber)) {
|
||||||
// if the current inspected element is an error boundary,
|
// if the current inspected element is an error boundary,
|
||||||
// either that we want to use it to toggle off error state
|
// either that we want to use it to toggle off error state
|
||||||
// or that we allow to force error state on it if it's within another
|
// or that we allow to force error state on it if it's within another
|
||||||
// error boundary
|
// error boundary
|
||||||
|
//
|
||||||
|
// TODO: This flag is a leaked implementation detail. Once we start
|
||||||
|
// releasing DevTools in lockstep with React, we should import a function
|
||||||
|
// from the reconciler instead.
|
||||||
|
const DidCapture = 0b000000000000000000010000000;
|
||||||
|
isErrored =
|
||||||
|
(fiber.flags & DidCapture) !== 0 ||
|
||||||
|
forceErrorForFiberIDs.get(id) === true;
|
||||||
targetErrorBoundaryID = isErrored ? id : getNearestErrorBoundaryID(fiber);
|
targetErrorBoundaryID = isErrored ? id : getNearestErrorBoundaryID(fiber);
|
||||||
} else {
|
} else {
|
||||||
targetErrorBoundaryID = getNearestErrorBoundaryID(fiber);
|
targetErrorBoundaryID = getNearestErrorBoundaryID(fiber);
|
||||||
|
|
@ -3538,14 +3508,17 @@ export function attach(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function copyElementPath(id: number, path: Array<string | number>): void {
|
function getSerializedElementValueByPath(
|
||||||
|
id: number,
|
||||||
|
path: Array<string | number>,
|
||||||
|
): ?string {
|
||||||
if (isMostRecentlyInspectedElement(id)) {
|
if (isMostRecentlyInspectedElement(id)) {
|
||||||
copyToClipboard(
|
const valueToCopy = getInObject(
|
||||||
getInObject(
|
((mostRecentlyInspectedElement: any): InspectedElement),
|
||||||
((mostRecentlyInspectedElement: any): InspectedElement),
|
path,
|
||||||
path,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return serializeToString(valueToCopy);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -4342,7 +4315,7 @@ export function attach(
|
||||||
if (pseudoKey === undefined) {
|
if (pseudoKey === undefined) {
|
||||||
throw new Error('Expected root pseudo key to be known.');
|
throw new Error('Expected root pseudo key to be known.');
|
||||||
}
|
}
|
||||||
const name = pseudoKey.substring(0, pseudoKey.lastIndexOf(':'));
|
const name = pseudoKey.slice(0, pseudoKey.lastIndexOf(':'));
|
||||||
const counter = rootDisplayNameCounter.get(name);
|
const counter = rootDisplayNameCounter.get(name);
|
||||||
if (counter === undefined) {
|
if (counter === undefined) {
|
||||||
throw new Error('Expected counter to be known.');
|
throw new Error('Expected counter to be known.');
|
||||||
|
|
@ -4488,7 +4461,7 @@ export function attach(
|
||||||
clearErrorsAndWarnings,
|
clearErrorsAndWarnings,
|
||||||
clearErrorsForFiberID,
|
clearErrorsForFiberID,
|
||||||
clearWarningsForFiberID,
|
clearWarningsForFiberID,
|
||||||
copyElementPath,
|
getSerializedElementValueByPath,
|
||||||
deletePath,
|
deletePath,
|
||||||
findNativeNodesForFiberID,
|
findNativeNodesForFiberID,
|
||||||
flushInitialOperations,
|
flushInitialOperations,
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue