In vulnerable versions of ws, the issue can be mitigated in the
following ways:
Reduce the maximum allowed length of the request headers using the
[--max-http-header-size=size][] and/or the
[maxHeaderSize][] options so
that no more headers than the server.maxHeadersCount limit
can be sent.
[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=ws&package-manager=npm_and_yarn&previous-version=8.13.0&new-version=8.17.1)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/facebook/react/network/alerts).
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
compiler/yarn.lock | 44 ++++++++++++++++++++++++++++++++------------
1 file changed, 32 insertions(+), 12 deletions(-)
diff --git a/compiler/yarn.lock b/compiler/yarn.lock
index 4e4c82e1932fd..6df7539281795 100644
--- a/compiler/yarn.lock
+++ b/compiler/yarn.lock
@@ -9127,7 +9127,16 @@ string-length@^4.0.1:
char-regex "^1.0.2"
strip-ansi "^6.0.0"
-"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
+"string-width-cjs@npm:string-width@^4.2.0":
+ version "4.2.3"
+ resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
+ integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
+ dependencies:
+ emoji-regex "^8.0.0"
+ is-fullwidth-code-point "^3.0.0"
+ strip-ansi "^6.0.1"
+
+string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
@@ -9220,7 +9229,14 @@ string_decoder@^1.1.1:
dependencies:
safe-buffer "~5.2.0"
-"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
+"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
+ version "6.0.1"
+ resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
+ integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
+ dependencies:
+ ansi-regex "^5.0.1"
+
+strip-ansi@^6.0.0, strip-ansi@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
@@ -9871,7 +9887,7 @@ wordwrap@>=0.0.2:
resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb"
integrity sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==
-"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
+"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
@@ -9889,6 +9905,15 @@ wrap-ansi@^6.2.0:
string-width "^4.1.0"
strip-ansi "^6.0.0"
+wrap-ansi@^7.0.0:
+ version "7.0.0"
+ resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
+ integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
+ dependencies:
+ ansi-styles "^4.0.0"
+ string-width "^4.1.0"
+ strip-ansi "^6.0.0"
+
wrap-ansi@^8.1.0:
version "8.1.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"
@@ -9911,15 +9936,10 @@ write-file-atomic@^4.0.1, write-file-atomic@^4.0.2:
imurmurhash "^0.1.4"
signal-exit "^3.0.7"
-ws@^8.13.0:
- version "8.13.0"
- resolved "https://registry.yarnpkg.com/ws/-/ws-8.13.0.tgz#9a9fb92f93cf41512a0735c8f4dd09b8a1211cd0"
- integrity sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==
-
-ws@^8.8.0:
- version "8.9.0"
- resolved "https://registry.yarnpkg.com/ws/-/ws-8.9.0.tgz#2a994bb67144be1b53fe2d23c53c028adeb7f45e"
- integrity sha512-Ja7nszREasGaYUYCI2k4lCKIRTt+y7XuqVoHR44YpI49TtryyqbqvDMn5eqfW7e6HzTukDRIsXqzVHScqRcafg==
+ws@^8.13.0, ws@^8.8.0:
+ version "8.17.1"
+ resolved "https://registry.yarnpkg.com/ws/-/ws-8.17.1.tgz#9293da530bb548febc95371d90f9c878727d919b"
+ integrity sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==
xml-name-validator@^4.0.0:
version "4.0.0"
From 7baae65e767726e44a23c13203c05ae0d540e26f Mon Sep 17 00:00:00 2001
From: Sol Lee <82362278+saul-atomrigs@users.noreply.github.com>
Date: Wed, 26 Jun 2024 22:59:03 +0900
Subject: [PATCH 18/38] compiler: fix the `hookKind` for `useInsertionEffect`
(#30069)
Currently, the `hookKind` for `useInsertionEffect` is set to
`useLayoutEffect`. This pull request fixes it by adding a new `hookKind`
for `useInsertionEffect`.
---
.../packages/babel-plugin-react-compiler/src/HIR/Globals.ts | 2 +-
.../packages/babel-plugin-react-compiler/src/HIR/ObjectShape.ts | 1 +
2 files changed, 2 insertions(+), 1 deletion(-)
diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/Globals.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/Globals.ts
index 1c471930f620d..ae7079a6c53e4 100644
--- a/compiler/packages/babel-plugin-react-compiler/src/HIR/Globals.ts
+++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/Globals.ts
@@ -363,7 +363,7 @@ const REACT_APIS: Array<[string, BuiltInType]> = [
restParam: Effect.Freeze,
returnType: { kind: "Poly" },
calleeEffect: Effect.Read,
- hookKind: "useLayoutEffect",
+ hookKind: "useInsertionEffect",
returnValueKind: ValueKind.Frozen,
},
BuiltInUseInsertionEffectHookId
diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/ObjectShape.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/ObjectShape.ts
index 5aa19ed09b763..4f71c661549eb 100644
--- a/compiler/packages/babel-plugin-react-compiler/src/HIR/ObjectShape.ts
+++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/ObjectShape.ts
@@ -123,6 +123,7 @@ export type HookKind =
| "useRef"
| "useEffect"
| "useLayoutEffect"
+ | "useInsertionEffect"
| "useMemo"
| "useCallback"
| "Custom";
From ef0f44ecffc560011f4e7d2f610176231ecc8bb2 Mon Sep 17 00:00:00 2001
From: Timothy Yung
Date: Wed, 26 Jun 2024 08:50:06 -0700
Subject: [PATCH 19/38] Enable `enableDeferRootSchedulingToMicrotask` and
`enableInfiniteRenderLoopDetection` for React Native (Meta) (#30090)
---
packages/shared/forks/ReactFeatureFlags.native-fb-dynamic.js | 2 --
packages/shared/forks/ReactFeatureFlags.native-fb.js | 4 ++--
.../shared/forks/ReactFeatureFlags.test-renderer.native-fb.js | 4 ++--
3 files changed, 4 insertions(+), 6 deletions(-)
diff --git a/packages/shared/forks/ReactFeatureFlags.native-fb-dynamic.js b/packages/shared/forks/ReactFeatureFlags.native-fb-dynamic.js
index ecdb3755691d2..a0513f81e7bf7 100644
--- a/packages/shared/forks/ReactFeatureFlags.native-fb-dynamic.js
+++ b/packages/shared/forks/ReactFeatureFlags.native-fb-dynamic.js
@@ -21,8 +21,6 @@ export const alwaysThrottleRetries = __VARIANT__;
export const consoleManagedByDevToolsDuringStrictMode = __VARIANT__;
export const disableDefaultPropsExceptForClasses = __VARIANT__;
export const enableAddPropertiesFastPath = __VARIANT__;
-export const enableDeferRootSchedulingToMicrotask = __VARIANT__;
export const enableFastJSX = __VARIANT__;
-export const enableInfiniteRenderLoopDetection = __VARIANT__;
export const enableShallowPropDiffing = __VARIANT__;
export const passChildrenWhenCloningPersistedNodes = __VARIANT__;
diff --git a/packages/shared/forks/ReactFeatureFlags.native-fb.js b/packages/shared/forks/ReactFeatureFlags.native-fb.js
index de6086c33f257..353937a7eaed8 100644
--- a/packages/shared/forks/ReactFeatureFlags.native-fb.js
+++ b/packages/shared/forks/ReactFeatureFlags.native-fb.js
@@ -23,9 +23,7 @@ export const {
consoleManagedByDevToolsDuringStrictMode,
disableDefaultPropsExceptForClasses,
enableAddPropertiesFastPath,
- enableDeferRootSchedulingToMicrotask,
enableFastJSX,
- enableInfiniteRenderLoopDetection,
enableShallowPropDiffing,
passChildrenWhenCloningPersistedNodes,
} = dynamicFlags;
@@ -51,11 +49,13 @@ export const enableComponentStackLocations = true;
export const enableCPUSuspense = true;
export const enableCreateEventHandleAPI = false;
export const enableDebugTracing = false;
+export const enableDeferRootSchedulingToMicrotask = true;
export const enableDO_NOT_USE_disableStrictPassiveEffect = false;
export const enableFilterEmptyStringAttributesDOM = true;
export const enableFizzExternalRuntime = true;
export const enableFlightReadableStream = true;
export const enableGetInspectorDataForInstanceInProduction = true;
+export const enableInfiniteRenderLoopDetection = true;
export const enableLazyContextPropagation = false;
export const enableLegacyCache = false;
export const enableLegacyFBSupport = false;
diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js
index 63fa1a451459a..5d256ca9e06c1 100644
--- a/packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js
+++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js
@@ -34,14 +34,14 @@ export const enableComponentStackLocations = true;
export const enableCPUSuspense = true;
export const enableCreateEventHandleAPI = false;
export const enableDebugTracing = false;
-export const enableDeferRootSchedulingToMicrotask = false;
+export const enableDeferRootSchedulingToMicrotask = true;
export const enableDO_NOT_USE_disableStrictPassiveEffect = false;
export const enableFastJSX = true;
export const enableFilterEmptyStringAttributesDOM = true;
export const enableFizzExternalRuntime = true;
export const enableFlightReadableStream = true;
export const enableGetInspectorDataForInstanceInProduction = false;
-export const enableInfiniteRenderLoopDetection = false;
+export const enableInfiniteRenderLoopDetection = true;
export const enableLazyContextPropagation = false;
export const enableLegacyCache = false;
export const enableLegacyFBSupport = false;
From 7045700a6db5edaf8427310de09ce0f113e781dc Mon Sep 17 00:00:00 2001
From: Jan Kassens
Date: Wed, 26 Jun 2024 12:51:46 -0400
Subject: [PATCH 20/38] Run ReactFabric-test.internal.js in xplat variant
(#30101)
The explicit mock override in this test was causing it to always run as
native-oss instead of also as xplat. This moves the test to use `//
@gate persistent` instead to run it in all persistent configs.
---
.../__tests__/ReactFabric-test.internal.js | 128 ++++++++++++++----
.../ReactFabric-test.internal.js.snap | 122 -----------------
2 files changed, 105 insertions(+), 145 deletions(-)
delete mode 100644 packages/react-native-renderer/src/__tests__/__snapshots__/ReactFabric-test.internal.js.snap
diff --git a/packages/react-native-renderer/src/__tests__/ReactFabric-test.internal.js b/packages/react-native-renderer/src/__tests__/ReactFabric-test.internal.js
index 47ee3cb8f7434..8639e77f43772 100644
--- a/packages/react-native-renderer/src/__tests__/ReactFabric-test.internal.js
+++ b/packages/react-native-renderer/src/__tests__/ReactFabric-test.internal.js
@@ -25,14 +25,13 @@ const SEND_ACCESSIBILITY_EVENT_REQUIRES_HOST_COMPONENT =
"sendAccessibilityEvent was called with a ref that isn't a " +
'native component. Use React.forwardRef to get access to the underlying native component';
-jest.mock('shared/ReactFeatureFlags', () =>
- require('shared/forks/ReactFeatureFlags.native-oss'),
-);
-
describe('ReactFabric', () => {
beforeEach(() => {
jest.resetModules();
+ // TODO: migrate these tests off of the legacy API
+ require('shared/ReactFeatureFlags').disableLegacyMode = false;
+
require('react-native/Libraries/ReactPrivate/InitializeNativeFabricUIManager');
React = require('react');
@@ -198,9 +197,9 @@ describe('ReactFabric', () => {
).toEqual({
bar: 'b',
});
- expect(
- nativeFabricUIManager.__dumpHierarchyForJestTestsOnly(),
- ).toMatchSnapshot();
+ expect(nativeFabricUIManager.__dumpHierarchyForJestTestsOnly()).toBe(`11
+ RCTText {"foo":"a","bar":"b"}
+ RCTRawText {"text":"1"}`);
await act(() => {
ReactFabric.render(
@@ -220,9 +219,9 @@ describe('ReactFabric', () => {
).toEqual({
foo: 'b',
});
- expect(
- nativeFabricUIManager.__dumpHierarchyForJestTestsOnly(),
- ).toMatchSnapshot();
+ expect(nativeFabricUIManager.__dumpHierarchyForJestTestsOnly()).toBe(`11
+ RCTText {"foo":"b","bar":"b"}
+ RCTRawText {"text":"2"}`);
});
it('should not clone nodes without children when updating props', async () => {
@@ -443,16 +442,54 @@ describe('ReactFabric', () => {
await act(() => {
ReactFabric.render(, 11);
});
- expect(
- nativeFabricUIManager.__dumpHierarchyForJestTestsOnly(),
- ).toMatchSnapshot();
+ expect(nativeFabricUIManager.__dumpHierarchyForJestTestsOnly()).toBe(`11
+ RCTView null
+ RCTView {"title":"a"}
+ RCTView {"title":"b"}
+ RCTView {"title":"c"}
+ RCTView {"title":"d"}
+ RCTView {"title":"e"}
+ RCTView {"title":"f"}
+ RCTView {"title":"g"}
+ RCTView {"title":"h"}
+ RCTView {"title":"i"}
+ RCTView {"title":"j"}
+ RCTView {"title":"k"}
+ RCTView {"title":"l"}
+ RCTView {"title":"m"}
+ RCTView {"title":"n"}
+ RCTView {"title":"o"}
+ RCTView {"title":"p"}
+ RCTView {"title":"q"}
+ RCTView {"title":"r"}
+ RCTView {"title":"s"}
+ RCTView {"title":"t"}`);
await act(() => {
ReactFabric.render(, 11);
});
- expect(
- nativeFabricUIManager.__dumpHierarchyForJestTestsOnly(),
- ).toMatchSnapshot();
+ expect(nativeFabricUIManager.__dumpHierarchyForJestTestsOnly()).toBe(`11
+ RCTView null
+ RCTView {"title":"m"}
+ RCTView {"title":"x"}
+ RCTView {"title":"h"}
+ RCTView {"title":"p"}
+ RCTView {"title":"g"}
+ RCTView {"title":"w"}
+ RCTView {"title":"f"}
+ RCTView {"title":"r"}
+ RCTView {"title":"a"}
+ RCTView {"title":"l"}
+ RCTView {"title":"k"}
+ RCTView {"title":"e"}
+ RCTView {"title":"o"}
+ RCTView {"title":"i"}
+ RCTView {"title":"v"}
+ RCTView {"title":"c"}
+ RCTView {"title":"s"}
+ RCTView {"title":"t"}
+ RCTView {"title":"z"}
+ RCTView {"title":"y"}`);
});
it('recreates host parents even if only children changed', async () => {
@@ -490,18 +527,60 @@ describe('ReactFabric', () => {
11,
);
});
- expect(
- nativeFabricUIManager.__dumpHierarchyForJestTestsOnly(),
- ).toMatchSnapshot();
+ expect(nativeFabricUIManager.__dumpHierarchyForJestTestsOnly()).toBe(
+ `11
+ RCTView null
+ RCTView null
+ RCTView {"title":"a"}
+ RCTView {"title":"b"}
+ RCTView {"title":"c"}
+ RCTView {"title":"d"}
+ RCTView {"title":"e"}
+ RCTView {"title":"f"}
+ RCTView {"title":"g"}
+ RCTView {"title":"h"}
+ RCTView {"title":"i"}
+ RCTView {"title":"j"}
+ RCTView {"title":"k"}
+ RCTView {"title":"l"}
+ RCTView {"title":"m"}
+ RCTView {"title":"n"}
+ RCTView {"title":"o"}
+ RCTView {"title":"p"}
+ RCTView {"title":"q"}
+ RCTView {"title":"r"}
+ RCTView {"title":"s"}
+ RCTView {"title":"t"}`,
+ );
// Call setState() so that we skip over the top-level host node.
// It should still get recreated despite a bailout.
ref.current.setState({
chars: after,
});
- expect(
- nativeFabricUIManager.__dumpHierarchyForJestTestsOnly(),
- ).toMatchSnapshot();
+ expect(nativeFabricUIManager.__dumpHierarchyForJestTestsOnly()).toBe(`11
+ RCTView null
+ RCTView null
+ RCTView {"title":"m"}
+ RCTView {"title":"x"}
+ RCTView {"title":"h"}
+ RCTView {"title":"p"}
+ RCTView {"title":"g"}
+ RCTView {"title":"w"}
+ RCTView {"title":"f"}
+ RCTView {"title":"r"}
+ RCTView {"title":"a"}
+ RCTView {"title":"l"}
+ RCTView {"title":"k"}
+ RCTView {"title":"e"}
+ RCTView {"title":"o"}
+ RCTView {"title":"i"}
+ RCTView {"title":"v"}
+ RCTView {"title":"c"}
+ RCTView {"title":"s"}
+ RCTView {"title":"t"}
+ RCTView {"title":"z"}
+ RCTView {"title":"y"}`);
});
it('calls setState with no arguments', async () => {
@@ -544,7 +623,10 @@ describe('ReactFabric', () => {
22,
);
});
- expect(snapshots).toMatchSnapshot();
+ expect(snapshots).toEqual([
+ `RCTView {"foo":"a"}
+ RCTView {"foo":"b"}`,
+ ]);
});
it('should not throw when is used inside of a ancestor', async () => {
diff --git a/packages/react-native-renderer/src/__tests__/__snapshots__/ReactFabric-test.internal.js.snap b/packages/react-native-renderer/src/__tests__/__snapshots__/ReactFabric-test.internal.js.snap
deleted file mode 100644
index 90953ff2e6b31..0000000000000
--- a/packages/react-native-renderer/src/__tests__/__snapshots__/ReactFabric-test.internal.js.snap
+++ /dev/null
@@ -1,122 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`ReactFabric recreates host parents even if only children changed 1`] = `
-"11
- RCTView null
- RCTView null
- RCTView {"title":"a"}
- RCTView {"title":"b"}
- RCTView {"title":"c"}
- RCTView {"title":"d"}
- RCTView {"title":"e"}
- RCTView {"title":"f"}
- RCTView {"title":"g"}
- RCTView {"title":"h"}
- RCTView {"title":"i"}
- RCTView {"title":"j"}
- RCTView {"title":"k"}
- RCTView {"title":"l"}
- RCTView {"title":"m"}
- RCTView {"title":"n"}
- RCTView {"title":"o"}
- RCTView {"title":"p"}
- RCTView {"title":"q"}
- RCTView {"title":"r"}
- RCTView {"title":"s"}
- RCTView {"title":"t"}"
-`;
-
-exports[`ReactFabric recreates host parents even if only children changed 2`] = `
-"11
- RCTView null
- RCTView null
- RCTView {"title":"m"}
- RCTView {"title":"x"}
- RCTView {"title":"h"}
- RCTView {"title":"p"}
- RCTView {"title":"g"}
- RCTView {"title":"w"}
- RCTView {"title":"f"}
- RCTView {"title":"r"}
- RCTView {"title":"a"}
- RCTView {"title":"l"}
- RCTView {"title":"k"}
- RCTView {"title":"e"}
- RCTView {"title":"o"}
- RCTView {"title":"i"}
- RCTView {"title":"v"}
- RCTView {"title":"c"}
- RCTView {"title":"s"}
- RCTView {"title":"t"}
- RCTView {"title":"z"}
- RCTView {"title":"y"}"
-`;
-
-exports[`ReactFabric renders and reorders children 1`] = `
-"11
- RCTView null
- RCTView {"title":"a"}
- RCTView {"title":"b"}
- RCTView {"title":"c"}
- RCTView {"title":"d"}
- RCTView {"title":"e"}
- RCTView {"title":"f"}
- RCTView {"title":"g"}
- RCTView {"title":"h"}
- RCTView {"title":"i"}
- RCTView {"title":"j"}
- RCTView {"title":"k"}
- RCTView {"title":"l"}
- RCTView {"title":"m"}
- RCTView {"title":"n"}
- RCTView {"title":"o"}
- RCTView {"title":"p"}
- RCTView {"title":"q"}
- RCTView {"title":"r"}
- RCTView {"title":"s"}
- RCTView {"title":"t"}"
-`;
-
-exports[`ReactFabric renders and reorders children 2`] = `
-"11
- RCTView null
- RCTView {"title":"m"}
- RCTView {"title":"x"}
- RCTView {"title":"h"}
- RCTView {"title":"p"}
- RCTView {"title":"g"}
- RCTView {"title":"w"}
- RCTView {"title":"f"}
- RCTView {"title":"r"}
- RCTView {"title":"a"}
- RCTView {"title":"l"}
- RCTView {"title":"k"}
- RCTView {"title":"e"}
- RCTView {"title":"o"}
- RCTView {"title":"i"}
- RCTView {"title":"v"}
- RCTView {"title":"c"}
- RCTView {"title":"s"}
- RCTView {"title":"t"}
- RCTView {"title":"z"}
- RCTView {"title":"y"}"
-`;
-
-exports[`ReactFabric should call complete after inserting children 1`] = `
-[
- "RCTView {"foo":"a"}
- RCTView {"foo":"b"}",
-]
-`;
-
-exports[`ReactFabric should only pass props diffs to FabricUIManager.cloneNode 1`] = `
-"11
- RCTText {"foo":"a","bar":"b"}
- RCTRawText {"text":"1"}"
-`;
-
-exports[`ReactFabric should only pass props diffs to FabricUIManager.cloneNode 2`] = `
-"11
- RCTText {"foo":"b","bar":"b"}
- RCTRawText {"text":"2"}"
-`;
From 349a99a7a347f280ce40e9297cac5a3bd796901e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?=
Date: Wed, 26 Jun 2024 19:27:26 +0200
Subject: [PATCH 21/38] Badge Environment Name on Thrown Errors from the Server
(#29846)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
When we replay logs we badge them with e.g. `[Server]`. That way it's
easy to identify that the source of the log actually happened on the
Server (RSC). However, when we threw an error we didn't have any such
thing. The error was rethrown on the client and then handled just like
any other client error.
This transfers the `environmentName` in DEV to our restored Error
"sub-class" (conceptually) along with `digest`. That way you can read
`error.environmentName` to print this in your own UI.
I also updated our default for `onCaughtError` (and `onError` in Fizz)
to use the `printToConsole` helper that the Flight Client uses to log it
with the badge format. So by default you get the same experience as
console.error for caught errors:
Unfortunately I can't do the same thing for `onUncaughtError` nor
`onRecoverableError` because they use `reportError` which doesn't have
custom formatting (unless we also prevented default on window.onerror).
However maybe that's ok because 1) you should always have an error
boundary 2) it's not likely that an RSC error can actually recover
because it's not going to be rendered again so shouldn't really happen
outside some parent conditionally rendering maybe.
The other problem with this approach is that the default is no longer
trivial - so reimplementing the default in user space is trickier and
ideally we shouldn't expose our default to be called.
---
packages/internal-test-utils/consoleMock.js | 17 +++++----
.../shouldIgnoreConsoleError.js | 4 +++
....js => ReactClientConsoleConfigBrowser.js} | 13 +++++--
...in.js => ReactClientConsoleConfigPlain.js} | 13 +++++--
...r.js => ReactClientConsoleConfigServer.js} | 13 +++++--
.../react-client/src/ReactFlightClient.js | 5 +++
.../src/__tests__/ReactFlight-test.js | 2 ++
...ReactFlightClientConfig.dom-browser-esm.js | 2 +-
...lightClientConfig.dom-browser-turbopack.js | 2 +-
.../ReactFlightClientConfig.dom-browser.js | 2 +-
.../forks/ReactFlightClientConfig.dom-bun.js | 2 +-
...ctFlightClientConfig.dom-edge-turbopack.js | 2 +-
...eactFlightClientConfig.dom-edge-webpack.js | 2 +-
.../ReactFlightClientConfig.dom-legacy.js | 2 +-
.../ReactFlightClientConfig.dom-node-esm.js | 2 +-
...ClientConfig.dom-node-turbopack-bundled.js | 2 +-
...ctFlightClientConfig.dom-node-turbopack.js | 2 +-
...eactFlightClientConfig.dom-node-webpack.js | 2 +-
.../forks/ReactFlightClientConfig.dom-node.js | 2 +-
.../src/createReactNoop.js | 5 +++
.../src/ReactFiberErrorLogger.js | 36 +++++++++++++++----
.../src/forks/ReactFiberConfig.art.js | 1 +
.../src/forks/ReactFiberConfig.custom.js | 1 +
.../src/forks/ReactFiberConfig.dom.js | 1 +
.../src/forks/ReactFiberConfig.fabric.js | 1 +
.../src/forks/ReactFiberConfig.native.js | 1 +
.../src/forks/ReactFiberConfig.test.js | 1 +
packages/react-server/src/ReactFizzServer.js | 13 ++++++-
.../react-server/src/ReactFlightServer.js | 9 ++++-
.../src/forks/ReactFizzConfig.custom.js | 2 ++
.../src/forks/ReactFizzConfig.dom-edge.js | 2 ++
.../src/forks/ReactFizzConfig.dom-legacy.js | 2 ++
.../src/forks/ReactFizzConfig.dom-node.js | 2 ++
.../src/forks/ReactFizzConfig.dom.js | 2 ++
34 files changed, 134 insertions(+), 36 deletions(-)
rename packages/react-client/src/{ReactFlightClientConsoleConfigBrowser.js => ReactClientConsoleConfigBrowser.js} (82%)
rename packages/react-client/src/{ReactFlightClientConsoleConfigPlain.js => ReactClientConsoleConfigPlain.js} (76%)
rename packages/react-client/src/{ReactFlightClientConsoleConfigServer.js => ReactClientConsoleConfigServer.js} (83%)
diff --git a/packages/internal-test-utils/consoleMock.js b/packages/internal-test-utils/consoleMock.js
index 45a59d2b4cb29..328cf3d90d0c4 100644
--- a/packages/internal-test-utils/consoleMock.js
+++ b/packages/internal-test-utils/consoleMock.js
@@ -418,13 +418,18 @@ export function createLogAssertion(
let argIndex = 0;
// console.* could have been called with a non-string e.g. `console.error(new Error())`
// eslint-disable-next-line react-internal/safe-string-coercion
- String(format).replace(/%s/g, () => argIndex++);
+ String(format).replace(/%s|%c/g, () => argIndex++);
if (argIndex !== args.length) {
- logsMismatchingFormat.push({
- format,
- args,
- expectedArgCount: argIndex,
- });
+ if (format.includes('%c%s')) {
+ // We intentionally use mismatching formatting when printing badging because we don't know
+ // the best default to use for different types because the default varies by platform.
+ } else {
+ logsMismatchingFormat.push({
+ format,
+ args,
+ expectedArgCount: argIndex,
+ });
+ }
}
// Check for extra component stacks
diff --git a/packages/internal-test-utils/shouldIgnoreConsoleError.js b/packages/internal-test-utils/shouldIgnoreConsoleError.js
index 383650d25a0b7..0b5798d241a1d 100644
--- a/packages/internal-test-utils/shouldIgnoreConsoleError.js
+++ b/packages/internal-test-utils/shouldIgnoreConsoleError.js
@@ -3,6 +3,10 @@
module.exports = function shouldIgnoreConsoleError(format, args) {
if (__DEV__) {
if (typeof format === 'string') {
+ if (format.startsWith('%c%s')) {
+ // Looks like a badged error message
+ args.splice(0, 3);
+ }
if (
args[0] != null &&
((typeof args[0] === 'object' &&
diff --git a/packages/react-client/src/ReactFlightClientConsoleConfigBrowser.js b/packages/react-client/src/ReactClientConsoleConfigBrowser.js
similarity index 82%
rename from packages/react-client/src/ReactFlightClientConsoleConfigBrowser.js
rename to packages/react-client/src/ReactClientConsoleConfigBrowser.js
index cc934685c8f7b..da87324b6df27 100644
--- a/packages/react-client/src/ReactFlightClientConsoleConfigBrowser.js
+++ b/packages/react-client/src/ReactClientConsoleConfigBrowser.js
@@ -7,6 +7,8 @@
* @flow
*/
+import {warn, error} from 'shared/consoleWithStackDev';
+
const badgeFormat = '%c%s%c ';
// Same badge styling as DevTools.
const badgeStyle =
@@ -63,7 +65,12 @@ export function printToConsole(
);
}
- // eslint-disable-next-line react-internal/no-production-logging
- console[methodName].apply(console, newArgs);
- return;
+ if (methodName === 'error') {
+ error.apply(console, newArgs);
+ } else if (methodName === 'warn') {
+ warn.apply(console, newArgs);
+ } else {
+ // eslint-disable-next-line react-internal/no-production-logging
+ console[methodName].apply(console, newArgs);
+ }
}
diff --git a/packages/react-client/src/ReactFlightClientConsoleConfigPlain.js b/packages/react-client/src/ReactClientConsoleConfigPlain.js
similarity index 76%
rename from packages/react-client/src/ReactFlightClientConsoleConfigPlain.js
rename to packages/react-client/src/ReactClientConsoleConfigPlain.js
index 1dbdec54cd078..a4e7c3c6d7e3c 100644
--- a/packages/react-client/src/ReactFlightClientConsoleConfigPlain.js
+++ b/packages/react-client/src/ReactClientConsoleConfigPlain.js
@@ -7,6 +7,8 @@
* @flow
*/
+import {warn, error} from 'shared/consoleWithStackDev';
+
const badgeFormat = '[%s] ';
const pad = ' ';
@@ -44,7 +46,12 @@ export function printToConsole(
newArgs.splice(offset, 0, badgeFormat, pad + badgeName + pad);
}
- // eslint-disable-next-line react-internal/no-production-logging
- console[methodName].apply(console, newArgs);
- return;
+ if (methodName === 'error') {
+ error.apply(console, newArgs);
+ } else if (methodName === 'warn') {
+ warn.apply(console, newArgs);
+ } else {
+ // eslint-disable-next-line react-internal/no-production-logging
+ console[methodName].apply(console, newArgs);
+ }
}
diff --git a/packages/react-client/src/ReactFlightClientConsoleConfigServer.js b/packages/react-client/src/ReactClientConsoleConfigServer.js
similarity index 83%
rename from packages/react-client/src/ReactFlightClientConsoleConfigServer.js
rename to packages/react-client/src/ReactClientConsoleConfigServer.js
index 7567483245fa6..f6ecad92f3eef 100644
--- a/packages/react-client/src/ReactFlightClientConsoleConfigServer.js
+++ b/packages/react-client/src/ReactClientConsoleConfigServer.js
@@ -7,6 +7,8 @@
* @flow
*/
+import {warn, error} from 'shared/consoleWithStackDev';
+
// This flips color using ANSI, then sets a color styling, then resets.
const badgeFormat = '\x1b[0m\x1b[7m%c%s\x1b[0m%c ';
// Same badge styling as DevTools.
@@ -64,7 +66,12 @@ export function printToConsole(
);
}
- // eslint-disable-next-line react-internal/no-production-logging
- console[methodName].apply(console, newArgs);
- return;
+ if (methodName === 'error') {
+ error.apply(console, newArgs);
+ } else if (methodName === 'warn') {
+ warn.apply(console, newArgs);
+ } else {
+ // eslint-disable-next-line react-internal/no-production-logging
+ console[methodName].apply(console, newArgs);
+ }
}
diff --git a/packages/react-client/src/ReactFlightClient.js b/packages/react-client/src/ReactFlightClient.js
index 866805b5578c1..7d421f0422b56 100644
--- a/packages/react-client/src/ReactFlightClient.js
+++ b/packages/react-client/src/ReactFlightClient.js
@@ -1730,6 +1730,7 @@ function resolveErrorDev(
digest: string,
message: string,
stack: string,
+ env: string,
): void {
if (!__DEV__) {
// These errors should never make it into a build so we don't need to encode them in codes.json
@@ -1769,6 +1770,7 @@ function resolveErrorDev(
}
(error: any).digest = digest;
+ (error: any).environmentName = env;
const errorWithDigest: ErrorWithDigest = (error: any);
const chunks = response._chunks;
const chunk = chunks.get(id);
@@ -2056,6 +2058,8 @@ function resolveConsoleEntry(
task.run(callStack);
return;
}
+ // TODO: Set the current owner so that consoleWithStackDev adds the component
+ // stack during the replay - if needed.
}
const rootTask = response._debugRootTask;
if (rootTask != null) {
@@ -2198,6 +2202,7 @@ function processFullRow(
errorInfo.digest,
errorInfo.message,
errorInfo.stack,
+ errorInfo.env,
);
} else {
resolveErrorProd(response, id, errorInfo.digest);
diff --git a/packages/react-client/src/__tests__/ReactFlight-test.js b/packages/react-client/src/__tests__/ReactFlight-test.js
index bd2d98736addd..8a652dab4aaa8 100644
--- a/packages/react-client/src/__tests__/ReactFlight-test.js
+++ b/packages/react-client/src/__tests__/ReactFlight-test.js
@@ -127,6 +127,7 @@ describe('ReactFlight', () => {
this.props.expectedMessage,
);
expect(this.state.error.digest).toBe('a dev digest');
+ expect(this.state.error.environmentName).toBe('Server');
} else {
expect(this.state.error.message).toBe(
'An error occurred in the Server Components render. The specific message is omitted in production' +
@@ -143,6 +144,7 @@ describe('ReactFlight', () => {
expectedDigest = '[]';
}
expect(this.state.error.digest).toContain(expectedDigest);
+ expect(this.state.error.environmentName).toBe(undefined);
expect(this.state.error.stack).toBe(
'Error: ' + this.state.error.message,
);
diff --git a/packages/react-client/src/forks/ReactFlightClientConfig.dom-browser-esm.js b/packages/react-client/src/forks/ReactFlightClientConfig.dom-browser-esm.js
index 87d87ea523e59..7ae8d5f5cdc7d 100644
--- a/packages/react-client/src/forks/ReactFlightClientConfig.dom-browser-esm.js
+++ b/packages/react-client/src/forks/ReactFlightClientConfig.dom-browser-esm.js
@@ -8,7 +8,7 @@
*/
export * from 'react-client/src/ReactFlightClientStreamConfigWeb';
-export * from 'react-client/src/ReactFlightClientConsoleConfigBrowser';
+export * from 'react-client/src/ReactClientConsoleConfigBrowser';
export * from 'react-server-dom-esm/src/ReactFlightClientConfigBundlerESM';
export * from 'react-server-dom-esm/src/ReactFlightClientConfigTargetESMBrowser';
export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM';
diff --git a/packages/react-client/src/forks/ReactFlightClientConfig.dom-browser-turbopack.js b/packages/react-client/src/forks/ReactFlightClientConfig.dom-browser-turbopack.js
index 97b4afd13a835..28e2489cf22bb 100644
--- a/packages/react-client/src/forks/ReactFlightClientConfig.dom-browser-turbopack.js
+++ b/packages/react-client/src/forks/ReactFlightClientConfig.dom-browser-turbopack.js
@@ -8,7 +8,7 @@
*/
export * from 'react-client/src/ReactFlightClientStreamConfigWeb';
-export * from 'react-client/src/ReactFlightClientConsoleConfigBrowser';
+export * from 'react-client/src/ReactClientConsoleConfigBrowser';
export * from 'react-server-dom-turbopack/src/ReactFlightClientConfigBundlerTurbopack';
export * from 'react-server-dom-turbopack/src/ReactFlightClientConfigBundlerTurbopackBrowser';
export * from 'react-server-dom-turbopack/src/ReactFlightClientConfigTargetTurbopackBrowser';
diff --git a/packages/react-client/src/forks/ReactFlightClientConfig.dom-browser.js b/packages/react-client/src/forks/ReactFlightClientConfig.dom-browser.js
index 51c832bff43a3..3a5ec6800fd57 100644
--- a/packages/react-client/src/forks/ReactFlightClientConfig.dom-browser.js
+++ b/packages/react-client/src/forks/ReactFlightClientConfig.dom-browser.js
@@ -8,7 +8,7 @@
*/
export * from 'react-client/src/ReactFlightClientStreamConfigWeb';
-export * from 'react-client/src/ReactFlightClientConsoleConfigBrowser';
+export * from 'react-client/src/ReactClientConsoleConfigBrowser';
export * from 'react-server-dom-webpack/src/ReactFlightClientConfigBundlerWebpack';
export * from 'react-server-dom-webpack/src/ReactFlightClientConfigBundlerWebpackBrowser';
export * from 'react-server-dom-webpack/src/ReactFlightClientConfigTargetWebpackBrowser';
diff --git a/packages/react-client/src/forks/ReactFlightClientConfig.dom-bun.js b/packages/react-client/src/forks/ReactFlightClientConfig.dom-bun.js
index 50713ae8e8e68..461996a2e0887 100644
--- a/packages/react-client/src/forks/ReactFlightClientConfig.dom-bun.js
+++ b/packages/react-client/src/forks/ReactFlightClientConfig.dom-bun.js
@@ -8,7 +8,7 @@
*/
export * from 'react-client/src/ReactFlightClientStreamConfigWeb';
-export * from 'react-client/src/ReactFlightClientConsoleConfigPlain';
+export * from 'react-client/src/ReactClientConsoleConfigPlain';
export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM';
export type Response = any;
diff --git a/packages/react-client/src/forks/ReactFlightClientConfig.dom-edge-turbopack.js b/packages/react-client/src/forks/ReactFlightClientConfig.dom-edge-turbopack.js
index 269f8ec0c2313..c08af0a6531de 100644
--- a/packages/react-client/src/forks/ReactFlightClientConfig.dom-edge-turbopack.js
+++ b/packages/react-client/src/forks/ReactFlightClientConfig.dom-edge-turbopack.js
@@ -8,7 +8,7 @@
*/
export * from 'react-client/src/ReactFlightClientStreamConfigWeb';
-export * from 'react-client/src/ReactFlightClientConsoleConfigServer';
+export * from 'react-client/src/ReactClientConsoleConfigServer';
export * from 'react-server-dom-turbopack/src/ReactFlightClientConfigBundlerTurbopack';
export * from 'react-server-dom-turbopack/src/ReactFlightClientConfigBundlerTurbopackServer';
export * from 'react-server-dom-turbopack/src/ReactFlightClientConfigTargetTurbopackServer';
diff --git a/packages/react-client/src/forks/ReactFlightClientConfig.dom-edge-webpack.js b/packages/react-client/src/forks/ReactFlightClientConfig.dom-edge-webpack.js
index cafa02b686214..db8da42686215 100644
--- a/packages/react-client/src/forks/ReactFlightClientConfig.dom-edge-webpack.js
+++ b/packages/react-client/src/forks/ReactFlightClientConfig.dom-edge-webpack.js
@@ -8,7 +8,7 @@
*/
export * from 'react-client/src/ReactFlightClientStreamConfigWeb';
-export * from 'react-client/src/ReactFlightClientConsoleConfigServer';
+export * from 'react-client/src/ReactClientConsoleConfigServer';
export * from 'react-server-dom-webpack/src/ReactFlightClientConfigBundlerWebpack';
export * from 'react-server-dom-webpack/src/ReactFlightClientConfigBundlerWebpackServer';
export * from 'react-server-dom-webpack/src/ReactFlightClientConfigTargetWebpackServer';
diff --git a/packages/react-client/src/forks/ReactFlightClientConfig.dom-legacy.js b/packages/react-client/src/forks/ReactFlightClientConfig.dom-legacy.js
index 017dc33081d5f..ddf6440a20e48 100644
--- a/packages/react-client/src/forks/ReactFlightClientConfig.dom-legacy.js
+++ b/packages/react-client/src/forks/ReactFlightClientConfig.dom-legacy.js
@@ -8,7 +8,7 @@
*/
export * from 'react-client/src/ReactFlightClientStreamConfigWeb';
-export * from 'react-client/src/ReactFlightClientConsoleConfigBrowser';
+export * from 'react-client/src/ReactClientConsoleConfigBrowser';
export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM';
export type Response = any;
diff --git a/packages/react-client/src/forks/ReactFlightClientConfig.dom-node-esm.js b/packages/react-client/src/forks/ReactFlightClientConfig.dom-node-esm.js
index 6c68ae163bb51..bf2071d6fc864 100644
--- a/packages/react-client/src/forks/ReactFlightClientConfig.dom-node-esm.js
+++ b/packages/react-client/src/forks/ReactFlightClientConfig.dom-node-esm.js
@@ -8,7 +8,7 @@
*/
export * from 'react-client/src/ReactFlightClientStreamConfigNode';
-export * from 'react-client/src/ReactFlightClientConsoleConfigServer';
+export * from 'react-client/src/ReactClientConsoleConfigServer';
export * from 'react-server-dom-esm/src/ReactFlightClientConfigBundlerESM';
export * from 'react-server-dom-esm/src/ReactFlightClientConfigTargetESMServer';
export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM';
diff --git a/packages/react-client/src/forks/ReactFlightClientConfig.dom-node-turbopack-bundled.js b/packages/react-client/src/forks/ReactFlightClientConfig.dom-node-turbopack-bundled.js
index f1e7d66ee8117..16f649249dd37 100644
--- a/packages/react-client/src/forks/ReactFlightClientConfig.dom-node-turbopack-bundled.js
+++ b/packages/react-client/src/forks/ReactFlightClientConfig.dom-node-turbopack-bundled.js
@@ -8,7 +8,7 @@
*/
export * from 'react-client/src/ReactFlightClientStreamConfigNode';
-export * from 'react-client/src/ReactFlightClientConsoleConfigServer';
+export * from 'react-client/src/ReactClientConsoleConfigServer';
export * from 'react-server-dom-turbopack/src/ReactFlightClientConfigBundlerTurbopack';
export * from 'react-server-dom-turbopack/src/ReactFlightClientConfigBundlerTurbopackServer';
export * from 'react-server-dom-turbopack/src/ReactFlightClientConfigTargetTurbopackServer';
diff --git a/packages/react-client/src/forks/ReactFlightClientConfig.dom-node-turbopack.js b/packages/react-client/src/forks/ReactFlightClientConfig.dom-node-turbopack.js
index c6da80ef6060f..68047af97b9f8 100644
--- a/packages/react-client/src/forks/ReactFlightClientConfig.dom-node-turbopack.js
+++ b/packages/react-client/src/forks/ReactFlightClientConfig.dom-node-turbopack.js
@@ -8,7 +8,7 @@
*/
export * from 'react-client/src/ReactFlightClientStreamConfigNode';
-export * from 'react-client/src/ReactFlightClientConsoleConfigServer';
+export * from 'react-client/src/ReactClientConsoleConfigServer';
export * from 'react-server-dom-turbopack/src/ReactFlightClientConfigBundlerNode';
export * from 'react-server-dom-turbopack/src/ReactFlightClientConfigTargetTurbopackServer';
export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM';
diff --git a/packages/react-client/src/forks/ReactFlightClientConfig.dom-node-webpack.js b/packages/react-client/src/forks/ReactFlightClientConfig.dom-node-webpack.js
index 95fd1590ab5c6..37a5322140a8a 100644
--- a/packages/react-client/src/forks/ReactFlightClientConfig.dom-node-webpack.js
+++ b/packages/react-client/src/forks/ReactFlightClientConfig.dom-node-webpack.js
@@ -8,7 +8,7 @@
*/
export * from 'react-client/src/ReactFlightClientStreamConfigNode';
-export * from 'react-client/src/ReactFlightClientConsoleConfigServer';
+export * from 'react-client/src/ReactClientConsoleConfigServer';
export * from 'react-server-dom-webpack/src/ReactFlightClientConfigBundlerWebpack';
export * from 'react-server-dom-webpack/src/ReactFlightClientConfigBundlerWebpackServer';
export * from 'react-server-dom-webpack/src/ReactFlightClientConfigTargetWebpackServer';
diff --git a/packages/react-client/src/forks/ReactFlightClientConfig.dom-node.js b/packages/react-client/src/forks/ReactFlightClientConfig.dom-node.js
index 41c7e8e1d4706..867612c0ac2d7 100644
--- a/packages/react-client/src/forks/ReactFlightClientConfig.dom-node.js
+++ b/packages/react-client/src/forks/ReactFlightClientConfig.dom-node.js
@@ -8,7 +8,7 @@
*/
export * from 'react-client/src/ReactFlightClientStreamConfigNode';
-export * from 'react-client/src/ReactFlightClientConsoleConfigServer';
+export * from 'react-client/src/ReactClientConsoleConfigServer';
export * from 'react-server-dom-webpack/src/ReactFlightClientConfigBundlerNode';
export * from 'react-server-dom-webpack/src/ReactFlightClientConfigTargetWebpackServer';
export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM';
diff --git a/packages/react-noop-renderer/src/createReactNoop.js b/packages/react-noop-renderer/src/createReactNoop.js
index e512fba13fc5d..a7cfc94e00a15 100644
--- a/packages/react-noop-renderer/src/createReactNoop.js
+++ b/packages/react-noop-renderer/src/createReactNoop.js
@@ -635,6 +635,11 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
NotPendingTransition: (null: TransitionStatus),
resetFormInstance(form: Instance) {},
+
+ printToConsole(methodName, args, badgeName) {
+ // eslint-disable-next-line react-internal/no-production-logging
+ console[methodName].apply(console, args);
+ },
};
const hostConfig = useMutation
diff --git a/packages/react-reconciler/src/ReactFiberErrorLogger.js b/packages/react-reconciler/src/ReactFiberErrorLogger.js
index a948e5c79b694..addb0aea43dcd 100644
--- a/packages/react-reconciler/src/ReactFiberErrorLogger.js
+++ b/packages/react-reconciler/src/ReactFiberErrorLogger.js
@@ -20,6 +20,8 @@ import ReactSharedInternals from 'shared/ReactSharedInternals';
import {enableOwnerStacks} from 'shared/ReactFeatureFlags';
+import {printToConsole} from './ReactFiberConfig';
+
// Side-channel since I'm not sure we want to make this part of the public API
let componentName: null | string = null;
let errorBoundaryName: null | string = null;
@@ -94,13 +96,33 @@ export function defaultOnCaughtError(
}.`;
if (enableOwnerStacks) {
- console.error(
- '%o\n\n%s\n\n%s\n',
- error,
- componentNameMessage,
- recreateMessage,
- // We let our consoleWithStackDev wrapper add the component stack to the end.
- );
+ if (
+ typeof error === 'object' &&
+ error !== null &&
+ typeof error.environmentName === 'string'
+ ) {
+ // This was a Server error. We print the environment name in a badge just like we do with
+ // replays of console logs to indicate that the source of this throw as actually the Server.
+ printToConsole(
+ 'error',
+ [
+ '%o\n\n%s\n\n%s\n',
+ error,
+ componentNameMessage,
+ recreateMessage,
+ // We let our consoleWithStackDev wrapper add the component stack to the end.
+ ],
+ error.environmentName,
+ );
+ } else {
+ console.error(
+ '%o\n\n%s\n\n%s\n',
+ error,
+ componentNameMessage,
+ recreateMessage,
+ // We let our consoleWithStackDev wrapper add the component stack to the end.
+ );
+ }
} else {
// The current Fiber is disconnected at this point which means that console printing
// cannot add a component stack since it terminates at the deletion node. This is not
diff --git a/packages/react-reconciler/src/forks/ReactFiberConfig.art.js b/packages/react-reconciler/src/forks/ReactFiberConfig.art.js
index 867ca996ec5af..1fb43b5ade8d3 100644
--- a/packages/react-reconciler/src/forks/ReactFiberConfig.art.js
+++ b/packages/react-reconciler/src/forks/ReactFiberConfig.art.js
@@ -8,3 +8,4 @@
*/
export * from 'react-art/src/ReactFiberConfigART';
+export * from 'react-client/src/ReactClientConsoleConfigBrowser';
diff --git a/packages/react-reconciler/src/forks/ReactFiberConfig.custom.js b/packages/react-reconciler/src/forks/ReactFiberConfig.custom.js
index 0bfe93a008cf1..24c80469c72a5 100644
--- a/packages/react-reconciler/src/forks/ReactFiberConfig.custom.js
+++ b/packages/react-reconciler/src/forks/ReactFiberConfig.custom.js
@@ -80,6 +80,7 @@ export const suspendInstance = $$$config.suspendInstance;
export const waitForCommitToBeReady = $$$config.waitForCommitToBeReady;
export const NotPendingTransition = $$$config.NotPendingTransition;
export const resetFormInstance = $$$config.resetFormInstance;
+export const printToConsole = $$$config.printToConsole;
// -------------------
// Microtasks
diff --git a/packages/react-reconciler/src/forks/ReactFiberConfig.dom.js b/packages/react-reconciler/src/forks/ReactFiberConfig.dom.js
index 4932b1a787bb9..fa7dbe3123284 100644
--- a/packages/react-reconciler/src/forks/ReactFiberConfig.dom.js
+++ b/packages/react-reconciler/src/forks/ReactFiberConfig.dom.js
@@ -8,3 +8,4 @@
*/
export * from 'react-dom-bindings/src/client/ReactFiberConfigDOM';
+export * from 'react-client/src/ReactClientConsoleConfigBrowser';
diff --git a/packages/react-reconciler/src/forks/ReactFiberConfig.fabric.js b/packages/react-reconciler/src/forks/ReactFiberConfig.fabric.js
index f1787a68e845b..2fb8768972da4 100644
--- a/packages/react-reconciler/src/forks/ReactFiberConfig.fabric.js
+++ b/packages/react-reconciler/src/forks/ReactFiberConfig.fabric.js
@@ -8,3 +8,4 @@
*/
export * from 'react-native-renderer/src/ReactFiberConfigFabric';
+export * from 'react-client/src/ReactClientConsoleConfigPlain';
diff --git a/packages/react-reconciler/src/forks/ReactFiberConfig.native.js b/packages/react-reconciler/src/forks/ReactFiberConfig.native.js
index 3f8a28688b716..3e06abc660ef7 100644
--- a/packages/react-reconciler/src/forks/ReactFiberConfig.native.js
+++ b/packages/react-reconciler/src/forks/ReactFiberConfig.native.js
@@ -8,3 +8,4 @@
*/
export * from 'react-native-renderer/src/ReactFiberConfigNative';
+export * from 'react-client/src/ReactClientConsoleConfigPlain';
diff --git a/packages/react-reconciler/src/forks/ReactFiberConfig.test.js b/packages/react-reconciler/src/forks/ReactFiberConfig.test.js
index 85020417c2c7d..238434a50df8c 100644
--- a/packages/react-reconciler/src/forks/ReactFiberConfig.test.js
+++ b/packages/react-reconciler/src/forks/ReactFiberConfig.test.js
@@ -8,3 +8,4 @@
*/
export * from 'react-test-renderer/src/ReactFiberConfigTestHost';
+export * from 'react-client/src/ReactClientConsoleConfigPlain';
diff --git a/packages/react-server/src/ReactFizzServer.js b/packages/react-server/src/ReactFizzServer.js
index eedae9a46f1b0..75b195e1e25fb 100644
--- a/packages/react-server/src/ReactFizzServer.js
+++ b/packages/react-server/src/ReactFizzServer.js
@@ -78,6 +78,7 @@ import {
resetResumableState,
completeResumableState,
emitEarlyPreloads,
+ printToConsole,
} from './ReactFizzConfig';
import {
constructClassInstance,
@@ -363,7 +364,17 @@ export opaque type Request = {
const DEFAULT_PROGRESSIVE_CHUNK_SIZE = 12800;
function defaultErrorHandler(error: mixed) {
- console['error'](error); // Don't transform to our wrapper
+ if (
+ typeof error === 'object' &&
+ error !== null &&
+ typeof error.environmentName === 'string'
+ ) {
+ // This was a Server error. We print the environment name in a badge just like we do with
+ // replays of console logs to indicate that the source of this throw as actually the Server.
+ printToConsole('error', [error], error.environmentName);
+ } else {
+ console['error'](error); // Don't transform to our wrapper
+ }
return null;
}
diff --git a/packages/react-server/src/ReactFlightServer.js b/packages/react-server/src/ReactFlightServer.js
index cb138fd30a710..02847f204bc3e 100644
--- a/packages/react-server/src/ReactFlightServer.js
+++ b/packages/react-server/src/ReactFlightServer.js
@@ -2774,11 +2774,18 @@ function emitErrorChunk(
if (__DEV__) {
let message;
let stack = '';
+ let env = request.environmentName();
try {
if (error instanceof Error) {
// eslint-disable-next-line react-internal/safe-string-coercion
message = String(error.message);
stack = getStack(error);
+ const errorEnv = (error: any).environmentName;
+ if (typeof errorEnv === 'string') {
+ // This probably came from another FlightClient as a pass through.
+ // Keep the environment name.
+ env = errorEnv;
+ }
} else if (typeof error === 'object' && error !== null) {
message = describeObjectForErrorMessage(error);
} else {
@@ -2788,7 +2795,7 @@ function emitErrorChunk(
} catch (x) {
message = 'An error occurred but serializing the error message failed.';
}
- errorInfo = {digest, message, stack};
+ errorInfo = {digest, message, stack, env};
} else {
errorInfo = {digest};
}
diff --git a/packages/react-server/src/forks/ReactFizzConfig.custom.js b/packages/react-server/src/forks/ReactFizzConfig.custom.js
index 07ebb3295f32f..c7964f187d42c 100644
--- a/packages/react-server/src/forks/ReactFizzConfig.custom.js
+++ b/packages/react-server/src/forks/ReactFizzConfig.custom.js
@@ -40,6 +40,8 @@ export const isPrimaryRenderer = false;
export const supportsRequestStorage = false;
export const requestStorage: AsyncLocalStorage = (null: any);
+export const printToConsole = $$$config.printToConsole;
+
export const resetResumableState = $$$config.resetResumableState;
export const completeResumableState = $$$config.completeResumableState;
export const getChildFormatContext = $$$config.getChildFormatContext;
diff --git a/packages/react-server/src/forks/ReactFizzConfig.dom-edge.js b/packages/react-server/src/forks/ReactFizzConfig.dom-edge.js
index 7c5ba9bce7e27..244202002edd7 100644
--- a/packages/react-server/src/forks/ReactFizzConfig.dom-edge.js
+++ b/packages/react-server/src/forks/ReactFizzConfig.dom-edge.js
@@ -10,6 +10,8 @@ import type {Request} from 'react-server/src/ReactFizzServer';
export * from 'react-dom-bindings/src/server/ReactFizzConfigDOM';
+export * from 'react-client/src/ReactClientConsoleConfigServer';
+
// For now, we get this from the global scope, but this will likely move to a module.
export const supportsRequestStorage = typeof AsyncLocalStorage === 'function';
export const requestStorage: AsyncLocalStorage =
diff --git a/packages/react-server/src/forks/ReactFizzConfig.dom-legacy.js b/packages/react-server/src/forks/ReactFizzConfig.dom-legacy.js
index 84d49396efcdf..5695669839f81 100644
--- a/packages/react-server/src/forks/ReactFizzConfig.dom-legacy.js
+++ b/packages/react-server/src/forks/ReactFizzConfig.dom-legacy.js
@@ -10,5 +10,7 @@ import type {Request} from 'react-server/src/ReactFizzServer';
export * from 'react-dom-bindings/src/server/ReactFizzConfigDOMLegacy';
+export * from 'react-client/src/ReactClientConsoleConfigPlain';
+
export const supportsRequestStorage = false;
export const requestStorage: AsyncLocalStorage = (null: any);
diff --git a/packages/react-server/src/forks/ReactFizzConfig.dom-node.js b/packages/react-server/src/forks/ReactFizzConfig.dom-node.js
index 8c9718e8234c3..5ee4566ad09ba 100644
--- a/packages/react-server/src/forks/ReactFizzConfig.dom-node.js
+++ b/packages/react-server/src/forks/ReactFizzConfig.dom-node.js
@@ -13,6 +13,8 @@ import type {Request} from 'react-server/src/ReactFizzServer';
export * from 'react-dom-bindings/src/server/ReactFizzConfigDOM';
+export * from 'react-client/src/ReactClientConsoleConfigServer';
+
export const supportsRequestStorage = true;
export const requestStorage: AsyncLocalStorage =
new AsyncLocalStorage();
diff --git a/packages/react-server/src/forks/ReactFizzConfig.dom.js b/packages/react-server/src/forks/ReactFizzConfig.dom.js
index 2bf9be13273d6..17ddc166a7922 100644
--- a/packages/react-server/src/forks/ReactFizzConfig.dom.js
+++ b/packages/react-server/src/forks/ReactFizzConfig.dom.js
@@ -10,5 +10,7 @@ import type {Request} from 'react-server/src/ReactFizzServer';
export * from 'react-dom-bindings/src/server/ReactFizzConfigDOM';
+export * from 'react-client/src/ReactClientConsoleConfigBrowser';
+
export const supportsRequestStorage = false;
export const requestStorage: AsyncLocalStorage = (null: any);
From 609d0cc3c8d484cd16378f4ba0e2e9eedbf48358 Mon Sep 17 00:00:00 2001
From: Jiachi Liu
Date: Wed, 26 Jun 2024 19:47:39 +0200
Subject: [PATCH 22/38] Add new error message for awaiting the client export
(#29853)
## Summary
`Cannot access .then on server` is not an ideal message when you try to
await or do promise chain to the properties of client reference. The
below example will let `.then` get accessed by native code while
handling the promise chain but the access is not clearly visible in user
code.
```
import('./client-module').then((mod) => mod.Component)
```
This PR chnage the error message of module reference proxy '.then'
property to show more kinds of usage, then it can be pretty clearly for
helping users to avoid the bad usage
## How did you test this change?
Unit test
---
.../src/ReactFlightTurbopackReferences.js | 5 +++++
.../src/ReactFlightWebpackReferences.js | 5 +++++
.../src/__tests__/ReactFlightDOM-test.js | 14 ++++++++++++++
3 files changed, 24 insertions(+)
diff --git a/packages/react-server-dom-turbopack/src/ReactFlightTurbopackReferences.js b/packages/react-server-dom-turbopack/src/ReactFlightTurbopackReferences.js
index 26ee500ce5026..ecf6a35dfa6ef 100644
--- a/packages/react-server-dom-turbopack/src/ReactFlightTurbopackReferences.js
+++ b/packages/react-server-dom-turbopack/src/ReactFlightTurbopackReferences.js
@@ -133,6 +133,11 @@ const deepProxyHandlers = {
`Instead, you can export a Client Component wrapper ` +
`that itself renders a Client Context Provider.`,
);
+ case 'then':
+ throw new Error(
+ `Cannot await or return from a thenable. ` +
+ `You cannot await a client module from a server component.`,
+ );
}
// eslint-disable-next-line react-internal/safe-string-coercion
const expression = String(target.name) + '.' + String(name);
diff --git a/packages/react-server-dom-webpack/src/ReactFlightWebpackReferences.js b/packages/react-server-dom-webpack/src/ReactFlightWebpackReferences.js
index 78e8fa359426f..6d14f412063c1 100644
--- a/packages/react-server-dom-webpack/src/ReactFlightWebpackReferences.js
+++ b/packages/react-server-dom-webpack/src/ReactFlightWebpackReferences.js
@@ -141,6 +141,11 @@ const deepProxyHandlers = {
`Instead, you can export a Client Component wrapper ` +
`that itself renders a Client Context Provider.`,
);
+ case 'then':
+ throw new Error(
+ `Cannot await or return from a thenable. ` +
+ `You cannot await a client module from a server component.`,
+ );
}
// eslint-disable-next-line react-internal/safe-string-coercion
const expression = String(target.name) + '.' + String(name);
diff --git a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js
index 3bf8e02e0f687..96c63f63c6a41 100644
--- a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js
+++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js
@@ -623,6 +623,20 @@ describe('ReactFlightDOM', () => {
);
});
+ it('throws when await a client module prop of client exports', async () => {
+ const ClientModule = clientExports({
+ Component: {deep: 'thing'},
+ });
+ async function awaitExport() {
+ const mod = await ClientModule;
+ return await Promise.resolve(mod.Component);
+ }
+ await expect(awaitExport()).rejects.toThrowError(
+ `Cannot await or return from a thenable. ` +
+ `You cannot await a client module from a server component.`,
+ );
+ });
+
it('throws when accessing a symbol prop from client exports', () => {
const symbol = Symbol('test');
const ClientModule = clientExports({
From 8416ebee38b1712611d8987ba64ce8fd87c3a540 Mon Sep 17 00:00:00 2001
From: Jan Kassens
Date: Wed, 26 Jun 2024 16:04:38 -0400
Subject: [PATCH 23/38] Update ReactFabric-test.internal to concurrent root
(#30103)
---
.../__tests__/ReactFabric-test.internal.js | 130 ++++++++++++++----
1 file changed, 103 insertions(+), 27 deletions(-)
diff --git a/packages/react-native-renderer/src/__tests__/ReactFabric-test.internal.js b/packages/react-native-renderer/src/__tests__/ReactFabric-test.internal.js
index 8639e77f43772..99d21ea933ecd 100644
--- a/packages/react-native-renderer/src/__tests__/ReactFabric-test.internal.js
+++ b/packages/react-native-renderer/src/__tests__/ReactFabric-test.internal.js
@@ -29,9 +29,6 @@ describe('ReactFabric', () => {
beforeEach(() => {
jest.resetModules();
- // TODO: migrate these tests off of the legacy API
- require('shared/ReactFeatureFlags').disableLegacyMode = false;
-
require('react-native/Libraries/ReactPrivate/InitializeNativeFabricUIManager');
React = require('react');
@@ -51,7 +48,7 @@ describe('ReactFabric', () => {
}));
await act(() => {
- ReactFabric.render(, 1);
+ ReactFabric.render(, 1, null, true);
});
expect(nativeFabricUIManager.createNode).toBeCalled();
expect(nativeFabricUIManager.appendChild).not.toBeCalled();
@@ -69,13 +66,13 @@ describe('ReactFabric', () => {
nativeFabricUIManager.createNode.mockReturnValue(firstNode);
await act(() => {
- ReactFabric.render(, 11);
+ ReactFabric.render(, 11, null, true);
});
expect(nativeFabricUIManager.createNode).toHaveBeenCalledTimes(1);
await act(() => {
- ReactFabric.render(, 11);
+ ReactFabric.render(, 11, null, true);
});
expect(nativeFabricUIManager.createNode).toHaveBeenCalledTimes(1);
@@ -99,7 +96,7 @@ describe('ReactFabric', () => {
}));
await act(() => {
- ReactFabric.render(1, 11);
+ ReactFabric.render(1, 11, null, true);
});
expect(nativeFabricUIManager.cloneNode).not.toBeCalled();
expect(nativeFabricUIManager.cloneNodeWithNewChildren).not.toBeCalled();
@@ -110,7 +107,7 @@ describe('ReactFabric', () => {
// If no properties have changed, we shouldn't call cloneNode.
await act(() => {
- ReactFabric.render(1, 11);
+ ReactFabric.render(1, 11, null, true);
});
expect(nativeFabricUIManager.cloneNode).not.toBeCalled();
expect(nativeFabricUIManager.cloneNodeWithNewChildren).not.toBeCalled();
@@ -121,7 +118,7 @@ describe('ReactFabric', () => {
// Only call cloneNode for the changed property (and not for text).
await act(() => {
- ReactFabric.render(1, 11);
+ ReactFabric.render(1, 11, null, true);
});
expect(nativeFabricUIManager.cloneNode).not.toBeCalled();
expect(nativeFabricUIManager.cloneNodeWithNewChildren).not.toBeCalled();
@@ -134,7 +131,7 @@ describe('ReactFabric', () => {
// Only call cloneNode for the changed text (and no other properties).
await act(() => {
- ReactFabric.render(2, 11);
+ ReactFabric.render(2, 11, null, true);
});
expect(nativeFabricUIManager.cloneNode).not.toBeCalled();
expect(
@@ -149,7 +146,7 @@ describe('ReactFabric', () => {
// Call cloneNode for both changed text and properties.
await act(() => {
- ReactFabric.render(3, 11);
+ ReactFabric.render(3, 11, null, true);
});
expect(nativeFabricUIManager.cloneNode).not.toBeCalled();
expect(
@@ -175,6 +172,8 @@ describe('ReactFabric', () => {
1
,
11,
+ null,
+ true,
);
});
expect(nativeFabricUIManager.cloneNode).not.toBeCalled();
@@ -190,6 +189,8 @@ describe('ReactFabric', () => {
1
,
11,
+ null,
+ true,
);
});
expect(
@@ -207,6 +208,8 @@ describe('ReactFabric', () => {
2
,
11,
+ null,
+ true,
);
});
const argIndex = gate(flags => flags.passChildrenWhenCloningPersistedNodes)
@@ -236,11 +239,15 @@ describe('ReactFabric', () => {
);
- await act(() => ReactFabric.render(, 11));
+ await act(() =>
+ ReactFabric.render(, 11, null, true),
+ );
expect(nativeFabricUIManager.completeRoot).toBeCalled();
jest.clearAllMocks();
- await act(() => ReactFabric.render(, 11));
+ await act(() =>
+ ReactFabric.render(, 11, null, true),
+ );
expect(nativeFabricUIManager.cloneNode).not.toBeCalled();
expect(nativeFabricUIManager.cloneNodeWithNewProps).toHaveBeenCalledTimes(
1,
@@ -289,6 +296,8 @@ describe('ReactFabric', () => {
}}
/>,
11,
+ null,
+ true,
);
});
@@ -320,6 +329,8 @@ describe('ReactFabric', () => {
}}
/>,
11,
+ null,
+ true,
);
});
@@ -350,6 +361,8 @@ describe('ReactFabric', () => {
}}
/>,
11,
+ null,
+ true,
);
});
@@ -382,6 +395,8 @@ describe('ReactFabric', () => {
}}
/>,
11,
+ null,
+ true,
);
});
@@ -395,7 +410,33 @@ describe('ReactFabric', () => {
expect(nativeFabricUIManager.sendAccessibilityEvent).not.toBeCalled();
});
- it('returns the correct instance and calls it in the callback', () => {
+ it('calls the callback with the correct instance and returns null', async () => {
+ const View = createReactNativeComponentClass('RCTView', () => ({
+ validAttributes: {foo: true},
+ uiViewClassName: 'RCTView',
+ }));
+
+ let a;
+ let b;
+ let c;
+ await act(() => {
+ c = ReactFabric.render(
+ (a = v)} />,
+ 11,
+ function () {
+ b = this;
+ },
+ true,
+ );
+ });
+
+ expect(a).toBeTruthy();
+ expect(a).toBe(b);
+ expect(c).toBe(null);
+ });
+
+ // @gate !disableLegacyMode
+ it('returns the instance in legacy mode and calls the callback with it', () => {
const View = createReactNativeComponentClass('RCTView', () => ({
validAttributes: {foo: true},
uiViewClassName: 'RCTView',
@@ -404,7 +445,12 @@ describe('ReactFabric', () => {
let a;
let b;
const c = ReactFabric.render(
- (a = v)} />,
+ {
+ a = v;
+ }}
+ />,
11,
function () {
b = this;
@@ -440,7 +486,7 @@ describe('ReactFabric', () => {
const after = 'mxhpgwfralkeoivcstzy';
await act(() => {
- ReactFabric.render(, 11);
+ ReactFabric.render(, 11, null, true);
});
expect(nativeFabricUIManager.__dumpHierarchyForJestTestsOnly()).toBe(`11
RCTView null
@@ -466,7 +512,7 @@ describe('ReactFabric', () => {
RCTView {"title":"t"}`);
await act(() => {
- ReactFabric.render(, 11);
+ ReactFabric.render(, 11, null, true);
});
expect(nativeFabricUIManager.__dumpHierarchyForJestTestsOnly()).toBe(`11
RCTView null
@@ -525,6 +571,8 @@ describe('ReactFabric', () => {
,
11,
+ null,
+ true,
);
});
expect(nativeFabricUIManager.__dumpHierarchyForJestTestsOnly()).toBe(
@@ -555,8 +603,10 @@ describe('ReactFabric', () => {
// Call setState() so that we skip over the top-level host node.
// It should still get recreated despite a bailout.
- ref.current.setState({
- chars: after,
+ await act(() => {
+ ref.current.setState({
+ chars: after,
+ });
});
expect(nativeFabricUIManager.__dumpHierarchyForJestTestsOnly()).toBe(`11
RCTView null
@@ -595,7 +645,7 @@ describe('ReactFabric', () => {
}
await act(() => {
- ReactFabric.render(, 11);
+ ReactFabric.render(, 11, null, true);
});
expect(mockArgs.length).toEqual(0);
});
@@ -621,6 +671,8 @@ describe('ReactFabric', () => {
,
22,
+ null,
+ true,
);
});
expect(snapshots).toEqual([
@@ -649,6 +701,8 @@ describe('ReactFabric', () => {
,
11,
+ null,
+ true,
);
});
@@ -658,6 +712,8 @@ describe('ReactFabric', () => {
,
11,
+ null,
+ true,
);
});
});
@@ -678,7 +734,7 @@ describe('ReactFabric', () => {
await expect(async () => {
await act(() => {
- ReactFabric.render(this should warn, 11);
+ ReactFabric.render(this should warn, 11, null, true);
});
}).toErrorDev(['Text strings must be rendered within a component.']);
@@ -689,6 +745,8 @@ describe('ReactFabric', () => {
hi hello hi,
11,
+ null,
+ true,
);
});
}).toErrorDev(['Text strings must be rendered within a component.']);
@@ -708,6 +766,8 @@ describe('ReactFabric', () => {
,
11,
+ null,
+ true,
);
});
});
@@ -727,7 +787,7 @@ describe('ReactFabric', () => {
const touchStart2 = jest.fn();
await act(() => {
- ReactFabric.render(, 11);
+ ReactFabric.render(, 11, null, true);
});
expect(nativeFabricUIManager.createNode.mock.calls.length).toBe(1);
@@ -753,7 +813,7 @@ describe('ReactFabric', () => {
expect(touchStart2).not.toBeCalled();
await act(() => {
- ReactFabric.render(, 11);
+ ReactFabric.render(, 11, null, true);
});
// Intentionally dispatch to the same instanceHandle again.
@@ -819,6 +879,8 @@ describe('ReactFabric', () => {
/>
,
11,
+ null,
+ true,
);
});
@@ -914,6 +976,8 @@ describe('ReactFabric', () => {
/>
,
1,
+ null,
+ true,
);
});
@@ -973,6 +1037,8 @@ describe('ReactFabric', () => {
ReactFabric.render(
(parent = n)} />,
11,
+ null,
+ true,
);
});
@@ -1012,6 +1078,8 @@ describe('ReactFabric', () => {
(parent = n)} />
,
11,
+ null,
+ true,
);
});
@@ -1053,6 +1121,8 @@ describe('ReactFabric', () => {
ReactFabric.render(
(parent = n)} />,
11,
+ null,
+ true,
);
});
@@ -1092,6 +1162,8 @@ describe('ReactFabric', () => {
(parent = n)} />
,
11,
+ null,
+ true,
);
});
@@ -1127,6 +1199,8 @@ describe('ReactFabric', () => {
}}
/>,
11,
+ null,
+ true,
);
});
const dangerouslyRetainedViewRef = viewRef;
@@ -1149,7 +1223,7 @@ describe('ReactFabric', () => {
}));
await act(() => {
- ReactFabric.render(, 1);
+ ReactFabric.render(, 1, null, true);
});
const internalInstanceHandle =
@@ -1182,6 +1256,8 @@ describe('ReactFabric', () => {
}}
/>,
1,
+ null,
+ true,
);
});
@@ -1196,7 +1272,7 @@ describe('ReactFabric', () => {
expect(publicInstance).toBe(viewRef);
await act(() => {
- ReactFabric.render(null, 1);
+ ReactFabric.render(null, 1, null, true);
});
const publicInstanceAfterUnmount =
@@ -1215,7 +1291,7 @@ describe('ReactFabric', () => {
}));
await act(() => {
- ReactFabric.render(Text content, 1);
+ ReactFabric.render(Text content, 1, null, true);
});
// Access the internal instance handle used to create the text node.
@@ -1247,7 +1323,7 @@ describe('ReactFabric', () => {
expect(publicInstance).toBe(expectedPublicInstance);
await act(() => {
- ReactFabric.render(null, 1);
+ ReactFabric.render(null, 1, null, true);
});
const publicInstanceAfterUnmount =
From b1d10c8a45629fe70c82c85167b01830e115b9d4 Mon Sep 17 00:00:00 2001
From: Joe Savona
Date: Wed, 26 Jun 2024 15:09:06 -0700
Subject: [PATCH 24/38] [compiler] Examples of invalid code reassigning locals
outside of render
ghstack-source-id: c48d0d0b86e47e2449982263c88cc2cfd44e326b
Pull Request resolved: https://github.com/facebook/react/pull/30106
---
...eassign-local-variable-in-effect.expect.md | 98 +++++++++++++++++++
...valid-reassign-local-variable-in-effect.js | 38 +++++++
...-local-variable-in-hook-argument.expect.md | 96 ++++++++++++++++++
...eassign-local-variable-in-hook-argument.js | 39 ++++++++
...n-local-variable-in-jsx-callback.expect.md | 77 +++++++++++++++
...reassign-local-variable-in-jsx-callback.js | 32 ++++++
.../packages/snap/src/SproutTodoFilter.ts | 5 +
7 files changed, 385 insertions(+)
create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/todo.invalid-reassign-local-variable-in-effect.expect.md
create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/todo.invalid-reassign-local-variable-in-effect.js
create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/todo.invalid-reassign-local-variable-in-hook-argument.expect.md
create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/todo.invalid-reassign-local-variable-in-hook-argument.js
create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/todo.invalid-reassign-local-variable-in-jsx-callback.expect.md
create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/todo.invalid-reassign-local-variable-in-jsx-callback.js
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/todo.invalid-reassign-local-variable-in-effect.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/todo.invalid-reassign-local-variable-in-effect.expect.md
new file mode 100644
index 0000000000000..ede6507782402
--- /dev/null
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/todo.invalid-reassign-local-variable-in-effect.expect.md
@@ -0,0 +1,98 @@
+
+## Input
+
+```javascript
+import { useEffect } from "react";
+
+function Component() {
+ let local;
+
+ const reassignLocal = (newValue) => {
+ local = newValue;
+ };
+
+ const onMount = (newValue) => {
+ reassignLocal("hello");
+
+ if (local === newValue) {
+ // Without React Compiler, `reassignLocal` is freshly created
+ // on each render, capturing a binding to the latest `local`,
+ // such that invoking reassignLocal will reassign the same
+ // binding that we are observing in the if condition, and
+ // we reach this branch
+ console.log("`local` was updated!");
+ } else {
+ // With React Compiler enabled, `reassignLocal` is only created
+ // once, capturing a binding to `local` in that render pass.
+ // Therefore, calling `reassignLocal` will reassign the wrong
+ // version of `local`, and not update the binding we are checking
+ // in the if condition.
+ //
+ // To protect against this, we disallow reassigning locals from
+ // functions that escape
+ throw new Error("`local` not updated!");
+ }
+ };
+
+ useEffect(() => {
+ onMount();
+ }, [onMount]);
+
+ return "ok";
+}
+
+```
+
+## Code
+
+```javascript
+import { c as _c } from "react/compiler-runtime";
+import { useEffect } from "react";
+
+function Component() {
+ const $ = _c(4);
+ let local;
+ let t0;
+ if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
+ t0 = (newValue) => {
+ local = newValue;
+ };
+ $[0] = t0;
+ } else {
+ t0 = $[0];
+ }
+ const reassignLocal = t0;
+ let t1;
+ if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
+ t1 = (newValue_0) => {
+ reassignLocal("hello");
+ if (local === newValue_0) {
+ console.log("`local` was updated!");
+ } else {
+ throw new Error("`local` not updated!");
+ }
+ };
+ $[1] = t1;
+ } else {
+ t1 = $[1];
+ }
+ const onMount = t1;
+ let t2;
+ let t3;
+ if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
+ t2 = () => {
+ onMount();
+ };
+ t3 = [onMount];
+ $[2] = t2;
+ $[3] = t3;
+ } else {
+ t2 = $[2];
+ t3 = $[3];
+ }
+ useEffect(t2, t3);
+ return "ok";
+}
+
+```
+
\ No newline at end of file
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/todo.invalid-reassign-local-variable-in-effect.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/todo.invalid-reassign-local-variable-in-effect.js
new file mode 100644
index 0000000000000..9d9f3b7f3206e
--- /dev/null
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/todo.invalid-reassign-local-variable-in-effect.js
@@ -0,0 +1,38 @@
+import { useEffect } from "react";
+
+function Component() {
+ let local;
+
+ const reassignLocal = (newValue) => {
+ local = newValue;
+ };
+
+ const onMount = (newValue) => {
+ reassignLocal("hello");
+
+ if (local === newValue) {
+ // Without React Compiler, `reassignLocal` is freshly created
+ // on each render, capturing a binding to the latest `local`,
+ // such that invoking reassignLocal will reassign the same
+ // binding that we are observing in the if condition, and
+ // we reach this branch
+ console.log("`local` was updated!");
+ } else {
+ // With React Compiler enabled, `reassignLocal` is only created
+ // once, capturing a binding to `local` in that render pass.
+ // Therefore, calling `reassignLocal` will reassign the wrong
+ // version of `local`, and not update the binding we are checking
+ // in the if condition.
+ //
+ // To protect against this, we disallow reassigning locals from
+ // functions that escape
+ throw new Error("`local` not updated!");
+ }
+ };
+
+ useEffect(() => {
+ onMount();
+ }, [onMount]);
+
+ return "ok";
+}
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/todo.invalid-reassign-local-variable-in-hook-argument.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/todo.invalid-reassign-local-variable-in-hook-argument.expect.md
new file mode 100644
index 0000000000000..e0da1fd2a0fd0
--- /dev/null
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/todo.invalid-reassign-local-variable-in-hook-argument.expect.md
@@ -0,0 +1,96 @@
+
+## Input
+
+```javascript
+import { useEffect } from "react";
+import { useIdentity } from "shared-runtime";
+
+function Component() {
+ let local;
+
+ const reassignLocal = (newValue) => {
+ local = newValue;
+ };
+
+ const callback = (newValue) => {
+ reassignLocal("hello");
+
+ if (local === newValue) {
+ // Without React Compiler, `reassignLocal` is freshly created
+ // on each render, capturing a binding to the latest `local`,
+ // such that invoking reassignLocal will reassign the same
+ // binding that we are observing in the if condition, and
+ // we reach this branch
+ console.log("`local` was updated!");
+ } else {
+ // With React Compiler enabled, `reassignLocal` is only created
+ // once, capturing a binding to `local` in that render pass.
+ // Therefore, calling `reassignLocal` will reassign the wrong
+ // version of `local`, and not update the binding we are checking
+ // in the if condition.
+ //
+ // To protect against this, we disallow reassigning locals from
+ // functions that escape
+ throw new Error("`local` not updated!");
+ }
+ };
+
+ useIdentity(() => {
+ callback();
+ });
+
+ return "ok";
+}
+
+```
+
+## Code
+
+```javascript
+import { c as _c } from "react/compiler-runtime";
+import { useEffect } from "react";
+import { useIdentity } from "shared-runtime";
+
+function Component() {
+ const $ = _c(3);
+ let local;
+ let t0;
+ if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
+ t0 = (newValue) => {
+ local = newValue;
+ };
+ $[0] = t0;
+ } else {
+ t0 = $[0];
+ }
+ const reassignLocal = t0;
+ let t1;
+ if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
+ t1 = (newValue_0) => {
+ reassignLocal("hello");
+ if (local === newValue_0) {
+ console.log("`local` was updated!");
+ } else {
+ throw new Error("`local` not updated!");
+ }
+ };
+ $[1] = t1;
+ } else {
+ t1 = $[1];
+ }
+ const callback = t1;
+ let t2;
+ if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
+ t2 = () => {
+ callback();
+ };
+ $[2] = t2;
+ } else {
+ t2 = $[2];
+ }
+ useIdentity(t2);
+ return "ok";
+}
+
+```
+
\ No newline at end of file
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/todo.invalid-reassign-local-variable-in-hook-argument.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/todo.invalid-reassign-local-variable-in-hook-argument.js
new file mode 100644
index 0000000000000..a46e58608bd3c
--- /dev/null
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/todo.invalid-reassign-local-variable-in-hook-argument.js
@@ -0,0 +1,39 @@
+import { useEffect } from "react";
+import { useIdentity } from "shared-runtime";
+
+function Component() {
+ let local;
+
+ const reassignLocal = (newValue) => {
+ local = newValue;
+ };
+
+ const callback = (newValue) => {
+ reassignLocal("hello");
+
+ if (local === newValue) {
+ // Without React Compiler, `reassignLocal` is freshly created
+ // on each render, capturing a binding to the latest `local`,
+ // such that invoking reassignLocal will reassign the same
+ // binding that we are observing in the if condition, and
+ // we reach this branch
+ console.log("`local` was updated!");
+ } else {
+ // With React Compiler enabled, `reassignLocal` is only created
+ // once, capturing a binding to `local` in that render pass.
+ // Therefore, calling `reassignLocal` will reassign the wrong
+ // version of `local`, and not update the binding we are checking
+ // in the if condition.
+ //
+ // To protect against this, we disallow reassigning locals from
+ // functions that escape
+ throw new Error("`local` not updated!");
+ }
+ };
+
+ useIdentity(() => {
+ callback();
+ });
+
+ return "ok";
+}
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/todo.invalid-reassign-local-variable-in-jsx-callback.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/todo.invalid-reassign-local-variable-in-jsx-callback.expect.md
new file mode 100644
index 0000000000000..af2b89700abce
--- /dev/null
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/todo.invalid-reassign-local-variable-in-jsx-callback.expect.md
@@ -0,0 +1,77 @@
+
+## Input
+
+```javascript
+function Component() {
+ let local;
+
+ const reassignLocal = (newValue) => {
+ local = newValue;
+ };
+
+ const onClick = (newValue) => {
+ reassignLocal("hello");
+
+ if (local === newValue) {
+ // Without React Compiler, `reassignLocal` is freshly created
+ // on each render, capturing a binding to the latest `local`,
+ // such that invoking reassignLocal will reassign the same
+ // binding that we are observing in the if condition, and
+ // we reach this branch
+ console.log("`local` was updated!");
+ } else {
+ // With React Compiler enabled, `reassignLocal` is only created
+ // once, capturing a binding to `local` in that render pass.
+ // Therefore, calling `reassignLocal` will reassign the wrong
+ // version of `local`, and not update the binding we are checking
+ // in the if condition.
+ //
+ // To protect against this, we disallow reassigning locals from
+ // functions that escape
+ throw new Error("`local` not updated!");
+ }
+ };
+
+ return ;
+}
+
+```
+
+## Code
+
+```javascript
+import { c as _c } from "react/compiler-runtime";
+function Component() {
+ const $ = _c(2);
+ let local;
+ let t0;
+ if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
+ t0 = (newValue) => {
+ local = newValue;
+ };
+ $[0] = t0;
+ } else {
+ t0 = $[0];
+ }
+ const reassignLocal = t0;
+ let t1;
+ if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
+ const onClick = (newValue_0) => {
+ reassignLocal("hello");
+ if (local === newValue_0) {
+ console.log("`local` was updated!");
+ } else {
+ throw new Error("`local` not updated!");
+ }
+ };
+
+ t1 = ;
+ $[1] = t1;
+ } else {
+ t1 = $[1];
+ }
+ return t1;
+}
+
+```
+
\ No newline at end of file
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/todo.invalid-reassign-local-variable-in-jsx-callback.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/todo.invalid-reassign-local-variable-in-jsx-callback.js
new file mode 100644
index 0000000000000..47cefa6708e95
--- /dev/null
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/todo.invalid-reassign-local-variable-in-jsx-callback.js
@@ -0,0 +1,32 @@
+function Component() {
+ let local;
+
+ const reassignLocal = (newValue) => {
+ local = newValue;
+ };
+
+ const onClick = (newValue) => {
+ reassignLocal("hello");
+
+ if (local === newValue) {
+ // Without React Compiler, `reassignLocal` is freshly created
+ // on each render, capturing a binding to the latest `local`,
+ // such that invoking reassignLocal will reassign the same
+ // binding that we are observing in the if condition, and
+ // we reach this branch
+ console.log("`local` was updated!");
+ } else {
+ // With React Compiler enabled, `reassignLocal` is only created
+ // once, capturing a binding to `local` in that render pass.
+ // Therefore, calling `reassignLocal` will reassign the wrong
+ // version of `local`, and not update the binding we are checking
+ // in the if condition.
+ //
+ // To protect against this, we disallow reassigning locals from
+ // functions that escape
+ throw new Error("`local` not updated!");
+ }
+ };
+
+ return ;
+}
diff --git a/compiler/packages/snap/src/SproutTodoFilter.ts b/compiler/packages/snap/src/SproutTodoFilter.ts
index 815285fed0a7a..c4bc6776da784 100644
--- a/compiler/packages/snap/src/SproutTodoFilter.ts
+++ b/compiler/packages/snap/src/SproutTodoFilter.ts
@@ -483,6 +483,11 @@ const skipFilter = new Set([
"rules-of-hooks/rules-of-hooks-93dc5d5e538a",
"rules-of-hooks/rules-of-hooks-69521d94fa03",
+ // should error
+ "todo.invalid-reassign-local-variable-in-jsx-callback",
+ "todo.invalid-reassign-local-variable-in-hook-argument",
+ "todo.invalid-reassign-local-variable-in-effect",
+
// bugs
"bug-invalid-hoisting-functionexpr",
"original-reactive-scopes-fork/bug-nonmutating-capture-in-unsplittable-memo-block",
From 4c9a2d2ddf1bc8205a426b14d8a37804b5beb57a Mon Sep 17 00:00:00 2001
From: Joe Savona
Date: Wed, 26 Jun 2024 16:02:09 -0700
Subject: [PATCH 25/38] [compiler] Validate against locals being reassigned
after render
Adds a pass which validates that local variables are not reassigned by functions which may be called after render. This is a straightforward forward data-flow analysis, where we:
1. Build up a mapping of context variables in the outer component/hook
2. Find ObjectMethod/FunctionExpressions which may reassign those context variables
3. Propagate aliases of those functions via StoreLocal/LoadLocal
4. Disallow passing those functions with a Freeze effect. This includes JSX arguments, hook arguments, hook return types, etc.
Conceptually, a function that reassigns a local is inherently mutable. Frozen functions must be side-effect free, so these two categories are incompatible and we can use the freeze effect to find all instances of where such functions are disallowed rather than special-casing eg hook calls and JSX.
ghstack-source-id: c2b22e3d62a1ab490a6a2150e28b934b9dc8676b
Pull Request resolved: https://github.com/facebook/react/pull/30107
---
.../src/Entrypoint/Pipeline.ts | 3 +
.../ValidateLocalsNotReassignedAfterRender.ts | 146 ++++++++++++++++++
...ssign-local-in-hook-return-value.expect.md | 27 ++++
...lid-reassign-local-in-hook-return-value.js | 6 +
...assign-local-variable-in-effect.expect.md} | 59 ++-----
...alid-reassign-local-variable-in-effect.js} | 0
...local-variable-in-hook-argument.expect.md} | 56 ++-----
...assign-local-variable-in-hook-argument.js} | 0
...-local-variable-in-jsx-callback.expect.md} | 44 ++----
...eassign-local-variable-in-jsx-callback.js} | 0
...ange-shared-inner-outer-function.expect.md | 8 +-
...table-range-shared-inner-outer-function.js | 2 +-
12 files changed, 217 insertions(+), 134 deletions(-)
create mode 100644 compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateLocalsNotReassignedAfterRender.ts
create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-reassign-local-in-hook-return-value.expect.md
create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-reassign-local-in-hook-return-value.js
rename compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/{todo.invalid-reassign-local-variable-in-effect.expect.md => error.invalid-reassign-local-variable-in-effect.expect.md} (54%)
rename compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/{todo.invalid-reassign-local-variable-in-effect.js => error.invalid-reassign-local-variable-in-effect.js} (100%)
rename compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/{todo.invalid-reassign-local-variable-in-hook-argument.expect.md => error.invalid-reassign-local-variable-in-hook-argument.expect.md} (56%)
rename compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/{todo.invalid-reassign-local-variable-in-hook-argument.js => error.invalid-reassign-local-variable-in-hook-argument.js} (100%)
rename compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/{todo.invalid-reassign-local-variable-in-jsx-callback.expect.md => error.invalid-reassign-local-variable-in-jsx-callback.expect.md} (60%)
rename compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/{todo.invalid-reassign-local-variable-in-jsx-callback.js => error.invalid-reassign-local-variable-in-jsx-callback.js} (100%)
diff --git a/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Pipeline.ts b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Pipeline.ts
index 2e7613f0a2adf..762f1a4112ab0 100644
--- a/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Pipeline.ts
+++ b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Pipeline.ts
@@ -96,6 +96,7 @@ import {
validatePreservedManualMemoization,
validateUseMemo,
} from "../Validation";
+import { validateLocalsNotReassignedAfterRender } from "../Validation/ValidateLocalsNotReassignedAfterRender";
export type CompilerPipelineValue =
| { kind: "ast"; name: string; value: CodegenFunction }
@@ -202,6 +203,8 @@ function* runWithEnvironment(
inferReferenceEffects(hir);
yield log({ kind: "hir", name: "InferReferenceEffects", value: hir });
+ validateLocalsNotReassignedAfterRender(hir);
+
// Note: Has to come after infer reference effects because "dead" code may still affect inference
deadCodeElimination(hir);
yield log({ kind: "hir", name: "DeadCodeElimination", value: hir });
diff --git a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateLocalsNotReassignedAfterRender.ts b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateLocalsNotReassignedAfterRender.ts
new file mode 100644
index 0000000000000..e83e5e69b5d21
--- /dev/null
+++ b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateLocalsNotReassignedAfterRender.ts
@@ -0,0 +1,146 @@
+/**
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+import { CompilerError, Effect } from "..";
+import { HIRFunction, IdentifierId, Place } from "../HIR";
+import {
+ eachInstructionValueOperand,
+ eachTerminalOperand,
+} from "../HIR/visitors";
+
+/**
+ * Validates that local variables cannot be reassigned after render.
+ * This prevents a category of bugs in which a closure captures a
+ * binding from one render but does not update
+ */
+export function validateLocalsNotReassignedAfterRender(fn: HIRFunction): void {
+ const contextVariables = new Set();
+ const reassignment = getContextReassignment(fn, contextVariables, false);
+ if (reassignment !== null) {
+ CompilerError.throwInvalidReact({
+ reason:
+ "Reassigning a variable after render has completed can cause inconsistent behavior on subsequent renders. Consider using state instead",
+ description:
+ reassignment.identifier.name !== null &&
+ reassignment.identifier.name.kind === "named"
+ ? `Variable \`${reassignment.identifier.name.value}\` cannot be reassigned after render`
+ : "",
+ loc: reassignment.loc,
+ });
+ }
+}
+
+function getContextReassignment(
+ fn: HIRFunction,
+ contextVariables: Set,
+ isFunctionExpression: boolean
+): Place | null {
+ const reassigningFunctions = new Map();
+ for (const [, block] of fn.body.blocks) {
+ for (const instr of block.instructions) {
+ const { lvalue, value } = instr;
+ switch (value.kind) {
+ case "FunctionExpression":
+ case "ObjectMethod": {
+ let reassignment = getContextReassignment(
+ value.loweredFunc.func,
+ contextVariables,
+ true
+ );
+ if (reassignment === null) {
+ // If the function itself doesn't reassign, does one of its dependencies?
+ for (const operand of eachInstructionValueOperand(value)) {
+ const reassignmentFromOperand = reassigningFunctions.get(
+ operand.identifier.id
+ );
+ if (reassignmentFromOperand !== undefined) {
+ reassignment = reassignmentFromOperand;
+ break;
+ }
+ }
+ }
+ // if the function or its depends reassign, propagate that fact on the lvalue
+ if (reassignment !== null) {
+ reassigningFunctions.set(lvalue.identifier.id, reassignment);
+ }
+ break;
+ }
+ case "StoreLocal": {
+ const reassignment = reassigningFunctions.get(
+ value.value.identifier.id
+ );
+ if (reassignment !== undefined) {
+ reassigningFunctions.set(
+ value.lvalue.place.identifier.id,
+ reassignment
+ );
+ reassigningFunctions.set(lvalue.identifier.id, reassignment);
+ }
+ break;
+ }
+ case "LoadLocal": {
+ const reassignment = reassigningFunctions.get(
+ value.place.identifier.id
+ );
+ if (reassignment !== undefined) {
+ reassigningFunctions.set(lvalue.identifier.id, reassignment);
+ }
+ break;
+ }
+ case "DeclareContext": {
+ if (!isFunctionExpression) {
+ contextVariables.add(value.lvalue.place.identifier.id);
+ }
+ break;
+ }
+ case "StoreContext": {
+ if (isFunctionExpression) {
+ if (contextVariables.has(value.lvalue.place.identifier.id)) {
+ return value.lvalue.place;
+ }
+ } else {
+ /*
+ * We only track reassignments of variables defined in the outer
+ * component or hook.
+ */
+ contextVariables.add(value.lvalue.place.identifier.id);
+ }
+ break;
+ }
+ default: {
+ for (const operand of eachInstructionValueOperand(value)) {
+ CompilerError.invariant(operand.effect !== Effect.Unknown, {
+ reason: `Expected effects to be inferred prior to ValidateLocalsNotReassignedAfterRender`,
+ loc: operand.loc,
+ });
+ const reassignment = reassigningFunctions.get(
+ operand.identifier.id
+ );
+ if (
+ reassignment !== undefined &&
+ operand.effect === Effect.Freeze
+ ) {
+ /*
+ * Functions that reassign local variables are inherently mutable and are unsafe to pass
+ * to a place that expects a frozen value. Propagate the reassignment upward.
+ */
+ return reassignment;
+ }
+ }
+ break;
+ }
+ }
+ }
+ for (const operand of eachTerminalOperand(block.terminal)) {
+ const reassignment = reassigningFunctions.get(operand.identifier.id);
+ if (reassignment !== undefined) {
+ return reassignment;
+ }
+ }
+ }
+ return null;
+}
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-reassign-local-in-hook-return-value.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-reassign-local-in-hook-return-value.expect.md
new file mode 100644
index 0000000000000..e090697b6ed35
--- /dev/null
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-reassign-local-in-hook-return-value.expect.md
@@ -0,0 +1,27 @@
+
+## Input
+
+```javascript
+function useFoo() {
+ let x = 0;
+ return (value) => {
+ x = value;
+ };
+}
+
+```
+
+
+## Error
+
+```
+ 2 | let x = 0;
+ 3 | return (value) => {
+> 4 | x = value;
+ | ^ InvalidReact: Reassigning a variable after render has completed can cause inconsistent behavior on subsequent renders. Consider using state instead. Variable `x` cannot be reassigned after render (4:4)
+ 5 | };
+ 6 | }
+ 7 |
+```
+
+
\ No newline at end of file
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-reassign-local-in-hook-return-value.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-reassign-local-in-hook-return-value.js
new file mode 100644
index 0000000000000..47aa81548e957
--- /dev/null
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-reassign-local-in-hook-return-value.js
@@ -0,0 +1,6 @@
+function useFoo() {
+ let x = 0;
+ return (value) => {
+ x = value;
+ };
+}
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/todo.invalid-reassign-local-variable-in-effect.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-reassign-local-variable-in-effect.expect.md
similarity index 54%
rename from compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/todo.invalid-reassign-local-variable-in-effect.expect.md
rename to compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-reassign-local-variable-in-effect.expect.md
index ede6507782402..96706d5f463ac 100644
--- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/todo.invalid-reassign-local-variable-in-effect.expect.md
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-reassign-local-variable-in-effect.expect.md
@@ -43,56 +43,17 @@ function Component() {
```
-## Code
-```javascript
-import { c as _c } from "react/compiler-runtime";
-import { useEffect } from "react";
-
-function Component() {
- const $ = _c(4);
- let local;
- let t0;
- if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
- t0 = (newValue) => {
- local = newValue;
- };
- $[0] = t0;
- } else {
- t0 = $[0];
- }
- const reassignLocal = t0;
- let t1;
- if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
- t1 = (newValue_0) => {
- reassignLocal("hello");
- if (local === newValue_0) {
- console.log("`local` was updated!");
- } else {
- throw new Error("`local` not updated!");
- }
- };
- $[1] = t1;
- } else {
- t1 = $[1];
- }
- const onMount = t1;
- let t2;
- let t3;
- if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
- t2 = () => {
- onMount();
- };
- t3 = [onMount];
- $[2] = t2;
- $[3] = t3;
- } else {
- t2 = $[2];
- t3 = $[3];
- }
- useEffect(t2, t3);
- return "ok";
-}
+## Error
```
+ 5 |
+ 6 | const reassignLocal = (newValue) => {
+> 7 | local = newValue;
+ | ^^^^^ InvalidReact: Reassigning a variable after render has completed can cause inconsistent behavior on subsequent renders. Consider using state instead. Variable `local` cannot be reassigned after render (7:7)
+ 8 | };
+ 9 |
+ 10 | const onMount = (newValue) => {
+```
+
\ No newline at end of file
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/todo.invalid-reassign-local-variable-in-effect.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-reassign-local-variable-in-effect.js
similarity index 100%
rename from compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/todo.invalid-reassign-local-variable-in-effect.js
rename to compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-reassign-local-variable-in-effect.js
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/todo.invalid-reassign-local-variable-in-hook-argument.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-reassign-local-variable-in-hook-argument.expect.md
similarity index 56%
rename from compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/todo.invalid-reassign-local-variable-in-hook-argument.expect.md
rename to compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-reassign-local-variable-in-hook-argument.expect.md
index e0da1fd2a0fd0..5d56c2605bc9e 100644
--- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/todo.invalid-reassign-local-variable-in-hook-argument.expect.md
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-reassign-local-variable-in-hook-argument.expect.md
@@ -44,53 +44,17 @@ function Component() {
```
-## Code
-```javascript
-import { c as _c } from "react/compiler-runtime";
-import { useEffect } from "react";
-import { useIdentity } from "shared-runtime";
-
-function Component() {
- const $ = _c(3);
- let local;
- let t0;
- if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
- t0 = (newValue) => {
- local = newValue;
- };
- $[0] = t0;
- } else {
- t0 = $[0];
- }
- const reassignLocal = t0;
- let t1;
- if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
- t1 = (newValue_0) => {
- reassignLocal("hello");
- if (local === newValue_0) {
- console.log("`local` was updated!");
- } else {
- throw new Error("`local` not updated!");
- }
- };
- $[1] = t1;
- } else {
- t1 = $[1];
- }
- const callback = t1;
- let t2;
- if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
- t2 = () => {
- callback();
- };
- $[2] = t2;
- } else {
- t2 = $[2];
- }
- useIdentity(t2);
- return "ok";
-}
+## Error
```
+ 6 |
+ 7 | const reassignLocal = (newValue) => {
+> 8 | local = newValue;
+ | ^^^^^ InvalidReact: Reassigning a variable after render has completed can cause inconsistent behavior on subsequent renders. Consider using state instead. Variable `local` cannot be reassigned after render (8:8)
+ 9 | };
+ 10 |
+ 11 | const callback = (newValue) => {
+```
+
\ No newline at end of file
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/todo.invalid-reassign-local-variable-in-hook-argument.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-reassign-local-variable-in-hook-argument.js
similarity index 100%
rename from compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/todo.invalid-reassign-local-variable-in-hook-argument.js
rename to compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-reassign-local-variable-in-hook-argument.js
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/todo.invalid-reassign-local-variable-in-jsx-callback.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-reassign-local-variable-in-jsx-callback.expect.md
similarity index 60%
rename from compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/todo.invalid-reassign-local-variable-in-jsx-callback.expect.md
rename to compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-reassign-local-variable-in-jsx-callback.expect.md
index af2b89700abce..8a00785cf98ff 100644
--- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/todo.invalid-reassign-local-variable-in-jsx-callback.expect.md
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-reassign-local-variable-in-jsx-callback.expect.md
@@ -37,41 +37,17 @@ function Component() {
```
-## Code
-```javascript
-import { c as _c } from "react/compiler-runtime";
-function Component() {
- const $ = _c(2);
- let local;
- let t0;
- if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
- t0 = (newValue) => {
- local = newValue;
- };
- $[0] = t0;
- } else {
- t0 = $[0];
- }
- const reassignLocal = t0;
- let t1;
- if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
- const onClick = (newValue_0) => {
- reassignLocal("hello");
- if (local === newValue_0) {
- console.log("`local` was updated!");
- } else {
- throw new Error("`local` not updated!");
- }
- };
-
- t1 = ;
- $[1] = t1;
- } else {
- t1 = $[1];
- }
- return t1;
-}
+## Error
```
+ 3 |
+ 4 | const reassignLocal = (newValue) => {
+> 5 | local = newValue;
+ | ^^^^^ InvalidReact: Reassigning a variable after render has completed can cause inconsistent behavior on subsequent renders. Consider using state instead. Variable `local` cannot be reassigned after render (5:5)
+ 6 | };
+ 7 |
+ 8 | const onClick = (newValue) => {
+```
+
\ No newline at end of file
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/todo.invalid-reassign-local-variable-in-jsx-callback.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-reassign-local-variable-in-jsx-callback.js
similarity index 100%
rename from compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/todo.invalid-reassign-local-variable-in-jsx-callback.js
rename to compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-reassign-local-variable-in-jsx-callback.js
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-mutable-range-shared-inner-outer-function.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-mutable-range-shared-inner-outer-function.expect.md
index 583148ceb75d1..945f68d4be107 100644
--- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-mutable-range-shared-inner-outer-function.expect.md
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-mutable-range-shared-inner-outer-function.expect.md
@@ -18,7 +18,7 @@ function Component(props) {
a.property = true;
b.push(false);
};
- return ;
+ return ;
}
export const FIXTURE_ENTRYPOINT = {
@@ -35,10 +35,10 @@ import { c as _c } from "react/compiler-runtime"; // @enableAssumeHooksFollowRul
let cond = true;
function Component(props) {
const $ = _c(1);
- let a;
- let b;
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
+ let a;
+ let b;
const f = () => {
if (cond) {
a = {};
@@ -52,7 +52,7 @@ function Component(props) {
b.push(false);
};
- t0 = ;
+ t0 = ;
$[0] = t0;
} else {
t0 = $[0];
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-mutable-range-shared-inner-outer-function.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-mutable-range-shared-inner-outer-function.js
index ac7299181ed5f..b975527138f3b 100644
--- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-mutable-range-shared-inner-outer-function.js
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-mutable-range-shared-inner-outer-function.js
@@ -14,7 +14,7 @@ function Component(props) {
a.property = true;
b.push(false);
};
- return ;
+ return ;
}
export const FIXTURE_ENTRYPOINT = {
From 00775d94dc32c5136787e3ca60065b9a5e2df9b8 Mon Sep 17 00:00:00 2001
From: Joe Savona
Date: Wed, 26 Jun 2024 16:06:16 -0700
Subject: [PATCH 26/38] [compiler] Add todo fixtures for local reassignment in
an async callback
ghstack-source-id: eca878f62a2149af76a72b59acd1820d0df86f30
Pull Request resolved: https://github.com/facebook/react/pull/30109
---
...local-variable-in-async-callback.expect.md | 58 +++++++++++++++++++
...assign-local-variable-in-async-callback.js | 16 +++++
.../packages/snap/src/SproutTodoFilter.ts | 1 +
3 files changed, 75 insertions(+)
create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/todo.invalid-reassign-local-variable-in-async-callback.expect.md
create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/todo.invalid-reassign-local-variable-in-async-callback.js
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/todo.invalid-reassign-local-variable-in-async-callback.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/todo.invalid-reassign-local-variable-in-async-callback.expect.md
new file mode 100644
index 0000000000000..aa07cce436136
--- /dev/null
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/todo.invalid-reassign-local-variable-in-async-callback.expect.md
@@ -0,0 +1,58 @@
+
+## Input
+
+```javascript
+function Component() {
+ let value = null;
+ const reassign = async () => {
+ await foo().then((result) => {
+ // Reassigning a local variable in an async function is *always* mutating
+ // after render, so this should error regardless of where this ends up
+ // getting called
+ value = result;
+ });
+ };
+
+ const onClick = async () => {
+ await reassign();
+ };
+ return
Click
;
+}
+
+```
+
+## Code
+
+```javascript
+import { c as _c } from "react/compiler-runtime";
+function Component() {
+ const $ = _c(2);
+ let value;
+ if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
+ value = null;
+ $[0] = value;
+ } else {
+ value = $[0];
+ }
+ let t0;
+ if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
+ const reassign = async () => {
+ await foo().then((result) => {
+ value = result;
+ });
+ };
+
+ const onClick = async () => {
+ await reassign();
+ };
+
+ t0 =
Click
;
+ $[1] = t0;
+ } else {
+ t0 = $[1];
+ }
+ return t0;
+}
+
+```
+
\ No newline at end of file
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/todo.invalid-reassign-local-variable-in-async-callback.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/todo.invalid-reassign-local-variable-in-async-callback.js
new file mode 100644
index 0000000000000..1ba759e3b8ae7
--- /dev/null
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/todo.invalid-reassign-local-variable-in-async-callback.js
@@ -0,0 +1,16 @@
+function Component() {
+ let value = null;
+ const reassign = async () => {
+ await foo().then((result) => {
+ // Reassigning a local variable in an async function is *always* mutating
+ // after render, so this should error regardless of where this ends up
+ // getting called
+ value = result;
+ });
+ };
+
+ const onClick = async () => {
+ await reassign();
+ };
+ return
Click
;
+}
diff --git a/compiler/packages/snap/src/SproutTodoFilter.ts b/compiler/packages/snap/src/SproutTodoFilter.ts
index c4bc6776da784..8d28ba239a368 100644
--- a/compiler/packages/snap/src/SproutTodoFilter.ts
+++ b/compiler/packages/snap/src/SproutTodoFilter.ts
@@ -487,6 +487,7 @@ const skipFilter = new Set([
"todo.invalid-reassign-local-variable-in-jsx-callback",
"todo.invalid-reassign-local-variable-in-hook-argument",
"todo.invalid-reassign-local-variable-in-effect",
+ "todo.invalid-reassign-local-variable-in-async-callback",
// bugs
"bug-invalid-hoisting-functionexpr",
From 1f59d07042466fa958d74062592cc31890e88661 Mon Sep 17 00:00:00 2001
From: Mike Vitousek
Date: Wed, 26 Jun 2024 17:07:38 -0700
Subject: [PATCH 27/38] [compiler] Repro for nested function local reassignment
issue
Summary: Additional repro demonstrating a case that still exists after #30106
ghstack-source-id: f03176fd88b29098d0263f86c1a7126e214997e5
Pull Request resolved: https://github.com/facebook/react/pull/30110
---
...eassign-local-variable-in-effect.expect.md | 101 ++++++++++++++++++
...ction-reassign-local-variable-in-effect.js | 37 +++++++
.../packages/snap/src/SproutTodoFilter.ts | 2 +
3 files changed, 140 insertions(+)
create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/todo.invalid-nested-function-reassign-local-variable-in-effect.expect.md
create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/todo.invalid-nested-function-reassign-local-variable-in-effect.js
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/todo.invalid-nested-function-reassign-local-variable-in-effect.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/todo.invalid-nested-function-reassign-local-variable-in-effect.expect.md
new file mode 100644
index 0000000000000..b863dfdcdbccb
--- /dev/null
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/todo.invalid-nested-function-reassign-local-variable-in-effect.expect.md
@@ -0,0 +1,101 @@
+
+## Input
+
+```javascript
+import { useEffect } from "react";
+function Component() {
+ let local;
+ const mk_reassignlocal = () => {
+ // Create the reassignment function inside another function, then return it
+ const reassignLocal = (newValue) => {
+ local = newValue;
+ };
+ return reassignLocal;
+ };
+ const reassignLocal = mk_reassignlocal();
+ const onMount = (newValue) => {
+ reassignLocal("hello");
+ if (local === newValue) {
+ // Without React Compiler, `reassignLocal` is freshly created
+ // on each render, capturing a binding to the latest `local`,
+ // such that invoking reassignLocal will reassign the same
+ // binding that we are observing in the if condition, and
+ // we reach this branch
+ console.log("`local` was updated!");
+ } else {
+ // With React Compiler enabled, `reassignLocal` is only created
+ // once, capturing a binding to `local` in that render pass.
+ // Therefore, calling `reassignLocal` will reassign the wrong
+ // version of `local`, and not update the binding we are checking
+ // in the if condition.
+ //
+ // To protect against this, we disallow reassigning locals from
+ // functions that escape
+ throw new Error("`local` not updated!");
+ }
+ };
+ useEffect(() => {
+ onMount();
+ }, [onMount]);
+ return "ok";
+}
+
+```
+
+## Code
+
+```javascript
+import { c as _c } from "react/compiler-runtime";
+import { useEffect } from "react";
+function Component() {
+ const $ = _c(4);
+ let local;
+ let t0;
+ if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
+ const mk_reassignlocal = () => {
+ const reassignLocal = (newValue) => {
+ local = newValue;
+ };
+ return reassignLocal;
+ };
+
+ t0 = mk_reassignlocal();
+ $[0] = t0;
+ } else {
+ t0 = $[0];
+ }
+ const reassignLocal_0 = t0;
+ let t1;
+ if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
+ t1 = (newValue_0) => {
+ reassignLocal_0("hello");
+ if (local === newValue_0) {
+ console.log("`local` was updated!");
+ } else {
+ throw new Error("`local` not updated!");
+ }
+ };
+ $[1] = t1;
+ } else {
+ t1 = $[1];
+ }
+ const onMount = t1;
+ let t2;
+ let t3;
+ if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
+ t2 = () => {
+ onMount();
+ };
+ t3 = [onMount];
+ $[2] = t2;
+ $[3] = t3;
+ } else {
+ t2 = $[2];
+ t3 = $[3];
+ }
+ useEffect(t2, t3);
+ return "ok";
+}
+
+```
+
\ No newline at end of file
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/todo.invalid-nested-function-reassign-local-variable-in-effect.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/todo.invalid-nested-function-reassign-local-variable-in-effect.js
new file mode 100644
index 0000000000000..e754870acce67
--- /dev/null
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/todo.invalid-nested-function-reassign-local-variable-in-effect.js
@@ -0,0 +1,37 @@
+import { useEffect } from "react";
+function Component() {
+ let local;
+ const mk_reassignlocal = () => {
+ // Create the reassignment function inside another function, then return it
+ const reassignLocal = (newValue) => {
+ local = newValue;
+ };
+ return reassignLocal;
+ };
+ const reassignLocal = mk_reassignlocal();
+ const onMount = (newValue) => {
+ reassignLocal("hello");
+ if (local === newValue) {
+ // Without React Compiler, `reassignLocal` is freshly created
+ // on each render, capturing a binding to the latest `local`,
+ // such that invoking reassignLocal will reassign the same
+ // binding that we are observing in the if condition, and
+ // we reach this branch
+ console.log("`local` was updated!");
+ } else {
+ // With React Compiler enabled, `reassignLocal` is only created
+ // once, capturing a binding to `local` in that render pass.
+ // Therefore, calling `reassignLocal` will reassign the wrong
+ // version of `local`, and not update the binding we are checking
+ // in the if condition.
+ //
+ // To protect against this, we disallow reassigning locals from
+ // functions that escape
+ throw new Error("`local` not updated!");
+ }
+ };
+ useEffect(() => {
+ onMount();
+ }, [onMount]);
+ return "ok";
+}
diff --git a/compiler/packages/snap/src/SproutTodoFilter.ts b/compiler/packages/snap/src/SproutTodoFilter.ts
index 8d28ba239a368..1bc208a53ed07 100644
--- a/compiler/packages/snap/src/SproutTodoFilter.ts
+++ b/compiler/packages/snap/src/SproutTodoFilter.ts
@@ -506,6 +506,8 @@ const skipFilter = new Set([
// needs to be executed as a module
"meta-property",
+
+ "todo.invalid-nested-function-reassign-local-variable-in-effect",
]);
export default skipFilter;
From fcfbfc1d1e3e350e1ea3fbc4a2418f647dc28a31 Mon Sep 17 00:00:00 2001
From: Mike Vitousek
Date: Wed, 26 Jun 2024 17:07:40 -0700
Subject: [PATCH 28/38] [compiler] Always error on async reassignments
Summary: Addresses the issue in #30109: any mutation of a local in an async function may occur after rendering has finished.
ghstack-source-id: 9f15cf0f144c0badd6009ceb51df43a50399d82b
Pull Request resolved: https://github.com/facebook/react/pull/30111
---
.../ValidateLocalsNotReassignedAfterRender.ts | 25 +++++++-
...local-variable-in-async-callback.expect.md | 37 ++++++++++++
...ssign-local-variable-in-async-callback.js} | 0
...local-variable-in-async-callback.expect.md | 58 -------------------
.../packages/snap/src/SproutTodoFilter.ts | 6 --
5 files changed, 59 insertions(+), 67 deletions(-)
create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-reassign-local-variable-in-async-callback.expect.md
rename compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/{todo.invalid-reassign-local-variable-in-async-callback.js => error.invalid-reassign-local-variable-in-async-callback.js} (100%)
delete mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/todo.invalid-reassign-local-variable-in-async-callback.expect.md
diff --git a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateLocalsNotReassignedAfterRender.ts b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateLocalsNotReassignedAfterRender.ts
index e83e5e69b5d21..f36f7adfd901f 100644
--- a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateLocalsNotReassignedAfterRender.ts
+++ b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateLocalsNotReassignedAfterRender.ts
@@ -19,7 +19,12 @@ import {
*/
export function validateLocalsNotReassignedAfterRender(fn: HIRFunction): void {
const contextVariables = new Set();
- const reassignment = getContextReassignment(fn, contextVariables, false);
+ const reassignment = getContextReassignment(
+ fn,
+ contextVariables,
+ false,
+ false
+ );
if (reassignment !== null) {
CompilerError.throwInvalidReact({
reason:
@@ -37,7 +42,8 @@ export function validateLocalsNotReassignedAfterRender(fn: HIRFunction): void {
function getContextReassignment(
fn: HIRFunction,
contextVariables: Set,
- isFunctionExpression: boolean
+ isFunctionExpression: boolean,
+ isAsync: boolean
): Place | null {
const reassigningFunctions = new Map();
for (const [, block] of fn.body.blocks) {
@@ -49,7 +55,8 @@ function getContextReassignment(
let reassignment = getContextReassignment(
value.loweredFunc.func,
contextVariables,
- true
+ true,
+ isAsync || value.loweredFunc.func.async
);
if (reassignment === null) {
// If the function itself doesn't reassign, does one of its dependencies?
@@ -65,6 +72,18 @@ function getContextReassignment(
}
// if the function or its depends reassign, propagate that fact on the lvalue
if (reassignment !== null) {
+ if (isAsync || value.loweredFunc.func.async) {
+ CompilerError.throwInvalidReact({
+ reason:
+ "Reassigning a variable in an async function can cause inconsistent behavior on subsequent renders. Consider using state instead",
+ description:
+ reassignment.identifier.name !== null &&
+ reassignment.identifier.name.kind === "named"
+ ? `Variable \`${reassignment.identifier.name.value}\` cannot be reassigned after render`
+ : "",
+ loc: reassignment.loc,
+ });
+ }
reassigningFunctions.set(lvalue.identifier.id, reassignment);
}
break;
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-reassign-local-variable-in-async-callback.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-reassign-local-variable-in-async-callback.expect.md
new file mode 100644
index 0000000000000..15c631f420584
--- /dev/null
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-reassign-local-variable-in-async-callback.expect.md
@@ -0,0 +1,37 @@
+
+## Input
+
+```javascript
+function Component() {
+ let value = null;
+ const reassign = async () => {
+ await foo().then((result) => {
+ // Reassigning a local variable in an async function is *always* mutating
+ // after render, so this should error regardless of where this ends up
+ // getting called
+ value = result;
+ });
+ };
+
+ const onClick = async () => {
+ await reassign();
+ };
+ return
Click
;
+}
+
+```
+
+
+## Error
+
+```
+ 6 | // after render, so this should error regardless of where this ends up
+ 7 | // getting called
+> 8 | value = result;
+ | ^^^^^ InvalidReact: Reassigning a variable in an async function can cause inconsistent behavior on subsequent renders. Consider using state instead. Variable `value` cannot be reassigned after render (8:8)
+ 9 | });
+ 10 | };
+ 11 |
+```
+
+
\ No newline at end of file
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/todo.invalid-reassign-local-variable-in-async-callback.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-reassign-local-variable-in-async-callback.js
similarity index 100%
rename from compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/todo.invalid-reassign-local-variable-in-async-callback.js
rename to compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-reassign-local-variable-in-async-callback.js
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/todo.invalid-reassign-local-variable-in-async-callback.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/todo.invalid-reassign-local-variable-in-async-callback.expect.md
deleted file mode 100644
index aa07cce436136..0000000000000
--- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/todo.invalid-reassign-local-variable-in-async-callback.expect.md
+++ /dev/null
@@ -1,58 +0,0 @@
-
-## Input
-
-```javascript
-function Component() {
- let value = null;
- const reassign = async () => {
- await foo().then((result) => {
- // Reassigning a local variable in an async function is *always* mutating
- // after render, so this should error regardless of where this ends up
- // getting called
- value = result;
- });
- };
-
- const onClick = async () => {
- await reassign();
- };
- return
Click
;
-}
-
-```
-
-## Code
-
-```javascript
-import { c as _c } from "react/compiler-runtime";
-function Component() {
- const $ = _c(2);
- let value;
- if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
- value = null;
- $[0] = value;
- } else {
- value = $[0];
- }
- let t0;
- if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
- const reassign = async () => {
- await foo().then((result) => {
- value = result;
- });
- };
-
- const onClick = async () => {
- await reassign();
- };
-
- t0 =
Click
;
- $[1] = t0;
- } else {
- t0 = $[1];
- }
- return t0;
-}
-
-```
-
\ No newline at end of file
diff --git a/compiler/packages/snap/src/SproutTodoFilter.ts b/compiler/packages/snap/src/SproutTodoFilter.ts
index 1bc208a53ed07..85f7413534bbe 100644
--- a/compiler/packages/snap/src/SproutTodoFilter.ts
+++ b/compiler/packages/snap/src/SproutTodoFilter.ts
@@ -483,12 +483,6 @@ const skipFilter = new Set([
"rules-of-hooks/rules-of-hooks-93dc5d5e538a",
"rules-of-hooks/rules-of-hooks-69521d94fa03",
- // should error
- "todo.invalid-reassign-local-variable-in-jsx-callback",
- "todo.invalid-reassign-local-variable-in-hook-argument",
- "todo.invalid-reassign-local-variable-in-effect",
- "todo.invalid-reassign-local-variable-in-async-callback",
-
// bugs
"bug-invalid-hoisting-functionexpr",
"original-reactive-scopes-fork/bug-nonmutating-capture-in-unsplittable-memo-block",
From f5a22fa27d54d8a5eb898416436be4ca87850d4d Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Thu, 27 Jun 2024 10:07:09 -0400
Subject: [PATCH 29/38] Bump ws from 7.2.1 to 7.5.10 (#29919)
Bumps [ws](https://github.com/websockets/ws) from 7.2.1 to 7.5.10.
Release notes
[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=ws&package-manager=npm_and_yarn&previous-version=7.2.1&new-version=7.5.10)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/facebook/react/network/alerts).
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
yarn.lock | 111 +++++++++++++++++++++++++++++++++++++++++++-----------
1 file changed, 88 insertions(+), 23 deletions(-)
diff --git a/yarn.lock b/yarn.lock
index 1f3a2dd01d434..b4a5640f3ca87 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -7202,7 +7202,6 @@ eslint-plugin-no-unsanitized@3.1.2:
"eslint-plugin-react-internal@link:./scripts/eslint-rules":
version "0.0.0"
- uid ""
eslint-plugin-react@^6.7.1:
version "6.10.3"
@@ -7274,7 +7273,7 @@ eslint-utils@^2.0.0, eslint-utils@^2.1.0:
dependencies:
eslint-visitor-keys "^1.1.0"
-"eslint-v7@npm:eslint@^7.7.0", eslint@^7.7.0:
+"eslint-v7@npm:eslint@^7.7.0":
version "7.32.0"
resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.32.0.tgz#c6d328a14be3fb08c8d1d21e12c02fdb7a2a812d"
integrity sha512-VHZ8gX+EDfz+97jGcgyGCyRia/dPOd6Xh9yPv8Bl1+SoaIwD+a/vlrOmGRUyOYu7MwUhc7CxqeaDZU13S4+EpA==
@@ -7437,6 +7436,52 @@ eslint@5.16.0:
table "^5.2.3"
text-table "^0.2.0"
+eslint@^7.7.0:
+ version "7.32.0"
+ resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.32.0.tgz#c6d328a14be3fb08c8d1d21e12c02fdb7a2a812d"
+ integrity sha512-VHZ8gX+EDfz+97jGcgyGCyRia/dPOd6Xh9yPv8Bl1+SoaIwD+a/vlrOmGRUyOYu7MwUhc7CxqeaDZU13S4+EpA==
+ dependencies:
+ "@babel/code-frame" "7.12.11"
+ "@eslint/eslintrc" "^0.4.3"
+ "@humanwhocodes/config-array" "^0.5.0"
+ ajv "^6.10.0"
+ chalk "^4.0.0"
+ cross-spawn "^7.0.2"
+ debug "^4.0.1"
+ doctrine "^3.0.0"
+ enquirer "^2.3.5"
+ escape-string-regexp "^4.0.0"
+ eslint-scope "^5.1.1"
+ eslint-utils "^2.1.0"
+ eslint-visitor-keys "^2.0.0"
+ espree "^7.3.1"
+ esquery "^1.4.0"
+ esutils "^2.0.2"
+ fast-deep-equal "^3.1.3"
+ file-entry-cache "^6.0.1"
+ functional-red-black-tree "^1.0.1"
+ glob-parent "^5.1.2"
+ globals "^13.6.0"
+ ignore "^4.0.6"
+ import-fresh "^3.0.0"
+ imurmurhash "^0.1.4"
+ is-glob "^4.0.0"
+ js-yaml "^3.13.1"
+ json-stable-stringify-without-jsonify "^1.0.1"
+ levn "^0.4.1"
+ lodash.merge "^4.6.2"
+ minimatch "^3.0.4"
+ natural-compare "^1.4.0"
+ optionator "^0.9.1"
+ progress "^2.0.0"
+ regexpp "^3.1.0"
+ semver "^7.2.1"
+ strip-ansi "^6.0.0"
+ strip-json-comments "^3.1.0"
+ table "^6.0.9"
+ text-table "^0.2.0"
+ v8-compile-cache "^2.0.3"
+
espree@6.2.1:
version "6.2.1"
resolved "https://registry.yarnpkg.com/espree/-/espree-6.2.1.tgz#77fc72e1fd744a2052c20f38a5b575832e82734a"
@@ -12116,9 +12161,9 @@ nullthrows@^1.0.0, nullthrows@^1.1.1:
integrity sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw==
nwsapi@^2.2.4:
- version "2.2.9"
- resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.9.tgz#7f3303218372db2e9f27c27766bcfc59ae7e61c6"
- integrity sha512-2f3F0SEEer8bBu0dsNCFF50N0cTThV1nWFYcEYFZttdW0lDAoybv9cQoK7X7/68Z89S7FoRrVjP1LPX4XRf9vg==
+ version "2.2.10"
+ resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.10.tgz#0b77a68e21a0b483db70b11fad055906e867cda8"
+ integrity sha512-QK0sRs7MKv0tKe1+5uZIQk/C8XGza4DAnztJG8iD+TpJIORARrCxczA738awHrZoHeTjSSoHqao2teO0dC/gFQ==
oauth-sign@~0.9.0:
version "0.9.0"
@@ -13555,9 +13600,9 @@ rc@^1.0.1, rc@^1.1.6, rc@^1.2.8:
scheduler "^0.20.2"
react-is@^16.8.1, react-is@^17.0.1, react-is@^18.0.0, react-is@^18.2.0, "react-is@npm:react-is":
- version "17.0.2"
- resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0"
- integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==
+ version "18.3.1"
+ resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.3.1.tgz#e83557dc12eae63a99e003a46388b1dcbb44db7e"
+ integrity sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==
react-lifecycles-compat@^3.0.4:
version "3.0.4"
@@ -15045,7 +15090,7 @@ string-natural-compare@^3.0.1:
resolved "https://registry.yarnpkg.com/string-natural-compare/-/string-natural-compare-3.0.1.tgz#7a42d58474454963759e8e8b7ae63d71c1e7fdf4"
integrity sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw==
-"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
+"string-width-cjs@npm:string-width@^4.2.0":
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
@@ -15080,6 +15125,15 @@ string-width@^4.0.0:
is-fullwidth-code-point "^3.0.0"
strip-ansi "^6.0.0"
+string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
+ version "4.2.3"
+ resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
+ integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
+ dependencies:
+ emoji-regex "^8.0.0"
+ is-fullwidth-code-point "^3.0.0"
+ strip-ansi "^6.0.1"
+
string-width@^5.0.1, string-width@^5.1.2:
version "5.1.2"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794"
@@ -15140,7 +15194,7 @@ string_decoder@~1.1.1:
dependencies:
safe-buffer "~5.1.0"
-"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
+"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
@@ -15168,6 +15222,13 @@ strip-ansi@^5.1.0:
dependencies:
ansi-regex "^4.1.0"
+strip-ansi@^6.0.0, strip-ansi@^6.0.1:
+ version "6.0.1"
+ resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
+ integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
+ dependencies:
+ ansi-regex "^5.0.1"
+
strip-ansi@^7.0.1:
version "7.1.0"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45"
@@ -16620,7 +16681,7 @@ workerize-loader@^2.0.2:
dependencies:
loader-utils "^2.0.0"
-"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
+"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
@@ -16638,6 +16699,15 @@ wrap-ansi@^6.2.0:
string-width "^4.1.0"
strip-ansi "^6.0.0"
+wrap-ansi@^7.0.0:
+ version "7.0.0"
+ resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
+ integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
+ dependencies:
+ ansi-styles "^4.0.0"
+ string-width "^4.1.0"
+ strip-ansi "^6.0.0"
+
wrap-ansi@^8.1.0:
version "8.1.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"
@@ -16691,20 +16761,15 @@ ws@7.2.3:
resolved "https://registry.yarnpkg.com/ws/-/ws-7.2.3.tgz#a5411e1fb04d5ed0efee76d26d5c46d830c39b46"
integrity sha512-HTDl9G9hbkNDk98naoR/cHDws7+EyYMOdL1BmjsZXRUjf7d+MficC4B7HLUPlSiho0vg+CWKrGIt/VJBd1xunQ==
-ws@^7:
- version "7.2.1"
- resolved "https://registry.yarnpkg.com/ws/-/ws-7.2.1.tgz#03ed52423cd744084b2cf42ed197c8b65a936b8e"
- integrity sha512-sucePNSafamSKoOqoNfBd8V0StlkzJKL2ZAhGQinCfNQ+oacw+Pk7lcdAElecBF2VkLNZRiIb5Oi1Q5lVUVt2A==
-
-ws@^7.4.6:
- version "7.5.5"
- resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.5.tgz#8b4bc4af518cfabd0473ae4f99144287b33eb881"
- integrity sha512-BAkMFcAzl8as1G/hArkxOxq3G7pjUqQ3gzYbLL0/5zNkph70e+lCoxBGnm6AW1+/aiNeV4fnKqZ8m4GZewmH2w==
+ws@^7, ws@^7.4.6:
+ version "7.5.10"
+ resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.10.tgz#58b5c20dc281633f6c19113f39b349bd8bd558d9"
+ integrity sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==
ws@^8.13.0:
- version "8.17.0"
- resolved "https://registry.yarnpkg.com/ws/-/ws-8.17.0.tgz#d145d18eca2ed25aaf791a183903f7be5e295fea"
- integrity sha512-uJq6108EgZMAl20KagGkzCKfMEjxmKvZHG7Tlq0Z6nOky7YF7aq4mOx6xK8TJ/i1LeK4Qus7INktacctDgY8Ow==
+ version "8.17.1"
+ resolved "https://registry.yarnpkg.com/ws/-/ws-8.17.1.tgz#9293da530bb548febc95371d90f9c878727d919b"
+ integrity sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==
xcase@^2.0.1:
version "2.0.1"
From 3bee073c01ee7af48c4ad19d98837cabaace67dc Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?=
Date: Thu, 27 Jun 2024 18:08:56 +0200
Subject: [PATCH 30/38] Remove isPrimaryRenderer from Flight Server Config
(#30115)
This was used for Context but since ReactFlightNewContext is gone now
this is no longer used.
---
.../src/server/ReactFlightServerConfigDOM.js | 4 ----
.../react-server/src/forks/ReactFlightServerConfig.custom.js | 2 --
2 files changed, 6 deletions(-)
diff --git a/packages/react-dom-bindings/src/server/ReactFlightServerConfigDOM.js b/packages/react-dom-bindings/src/server/ReactFlightServerConfigDOM.js
index 32905aa45b1fb..9cded881352af 100644
--- a/packages/react-dom-bindings/src/server/ReactFlightServerConfigDOM.js
+++ b/packages/react-dom-bindings/src/server/ReactFlightServerConfigDOM.js
@@ -20,10 +20,6 @@ import type {
// but it does not have any exports
import './ReactDOMFlightServerHostDispatcher';
-// Used to distinguish these contexts from ones used in other renderers.
-// E.g. this can be used to distinguish legacy renderers from this modern one.
-export const isPrimaryRenderer = true;
-
// We use zero to represent the absence of an explicit precedence because it is
// small, smaller than how we encode undefined, and is unambiguous. We could use
// a different tuple structure to encode this instead but this makes the runtime
diff --git a/packages/react-server/src/forks/ReactFlightServerConfig.custom.js b/packages/react-server/src/forks/ReactFlightServerConfig.custom.js
index 5afdf8c29c888..15874c64e0858 100644
--- a/packages/react-server/src/forks/ReactFlightServerConfig.custom.js
+++ b/packages/react-server/src/forks/ReactFlightServerConfig.custom.js
@@ -19,8 +19,6 @@ export type HintCode = any;
// eslint-disable-next-line no-unused-vars
export type HintModel = any;
-export const isPrimaryRenderer = false;
-
export const supportsRequestStorage = false;
export const requestStorage: AsyncLocalStorage = (null: any);
From ffec9ec5b5c846f61d7b40e92f138e2a7b34f273 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?=
Date: Thu, 27 Jun 2024 18:09:40 +0200
Subject: [PATCH 31/38] Add new package with renderToMarkup export (#30105)
Name of the package is tbd (straw: `react-html`). It's a new package
separate from `react-dom` though and can be used as a standalone package
- e.g. also from a React Native app.
```js
import {renderToMarkup} from '...';
const html = await renderToMarkup();
```
The idea is that this is a helper for rendering HTML that is not
intended to be hydrated. It's primarily intended to support a subset of
HTML that can be used as embedding and not served as HTML documents from
HTTP. For example as e-mails or in RSS/Atom feeds or other
distributions. It's a successor to `renderToStaticMarkup`.
A few differences:
- This doesn't support "Client Components". It can only use the Server
Components subset. No useEffect, no useState etc. since it will never be
hydrated. Use of those are errors.
- You also can't pass Client References so you can't use components
marked with `"use client"`.
- Unlike `renderToStaticMarkup` this does support async so you can
suspend and use data from these components.
- Unlike `renderToReadableStream` this does not support streaming or
Suspense boundaries and any error rejects the promise. Since there's no
feasible way to "client render" or patch up the document.
- Form Actions are not supported since in an embedded environment
there's no place to post back to across versions. You can render plain
forms with fixed URLs though.
- You can't use any resource preloading like `preload()` from
`react-dom`.
## Implementation
This first version in this PR only supports Server Components since
that's the thing that doesn't have an existing API. Might add a Client
Components version later that errors.
We don't want to maintain a completely separate implementation for this
use case so this uses the `dom-legacy` build dimension to wire up a
build that encapsulates a Flight Server -> Flight Client -> Fizz stream
to render Server Components that then get SSR:ed.
There's no problem to use a Flight Client in a Server Component
environment since it's already supported for Server-to-Server. Both of
these use a bundler config that just errors for Client References though
since we don't need any bundling integration and this is just a
standalone package.
Running Fizz in a Server Component environment is a problem though
because it depends on "react" and it needs the client version.
Therefore, for this build we embed the client version of "react" shared
internals into the build. It doesn't need anything to be able to use
those APIs since you can't call the client APIs anyway.
One unfortunate thing though is that since Flight currently needs to go
to binary and back, we need TextEncoder/TextDecoder to be available but
this shouldn't really be necessary. Also since we use the legacy stream
config, large strings that use byteLengthOfChunk errors atm. This needs
to be fixed before shipping. I'm not sure what would be the best
layering though that isn't unnecessarily burdensome to maintain. Maybe
some kind of pass-through protocol that would also be useful in general
- e.g. when Fizz and Flight are in the same process.
---------
Co-authored-by: Sebastian Silbermann
---
.../forks/ReactFlightClientConfig.dom-bun.js | 1 -
.../ReactFlightClientConfig.dom-legacy.js | 92 +++++++--
packages/react-html/README.md | 32 ++++
packages/react-html/index.js | 5 +
packages/react-html/npm/index.js | 5 +
.../react-html/npm/react-html.react-server.js | 7 +
packages/react-html/package.json | 38 ++++
.../react-html/react-html.react-server.js | 10 +
.../src/ReactHTMLLegacyClientStreamConfig.js | 32 ++++
packages/react-html/src/ReactHTMLServer.js | 178 ++++++++++++++++++
.../src/__tests__/ReactHTMLServer-test.js | 112 +++++++++++
.../ReactFlightServerConfig.dom-legacy.js | 71 ++++++-
scripts/error-codes/codes.json | 7 +-
scripts/rollup/bundles.js | 13 ++
scripts/rollup/forks.js | 9 +-
scripts/shared/inlinedHostConfigs.js | 35 ++++
16 files changed, 627 insertions(+), 20 deletions(-)
create mode 100644 packages/react-html/README.md
create mode 100644 packages/react-html/index.js
create mode 100644 packages/react-html/npm/index.js
create mode 100644 packages/react-html/npm/react-html.react-server.js
create mode 100644 packages/react-html/package.json
create mode 100644 packages/react-html/react-html.react-server.js
create mode 100644 packages/react-html/src/ReactHTMLLegacyClientStreamConfig.js
create mode 100644 packages/react-html/src/ReactHTMLServer.js
create mode 100644 packages/react-html/src/__tests__/ReactHTMLServer-test.js
diff --git a/packages/react-client/src/forks/ReactFlightClientConfig.dom-bun.js b/packages/react-client/src/forks/ReactFlightClientConfig.dom-bun.js
index 461996a2e0887..0a8027e3e12aa 100644
--- a/packages/react-client/src/forks/ReactFlightClientConfig.dom-bun.js
+++ b/packages/react-client/src/forks/ReactFlightClientConfig.dom-bun.js
@@ -11,7 +11,6 @@ export * from 'react-client/src/ReactFlightClientStreamConfigWeb';
export * from 'react-client/src/ReactClientConsoleConfigPlain';
export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM';
-export type Response = any;
export opaque type ModuleLoading = mixed;
export opaque type SSRModuleMap = mixed;
export opaque type ServerManifest = mixed;
diff --git a/packages/react-client/src/forks/ReactFlightClientConfig.dom-legacy.js b/packages/react-client/src/forks/ReactFlightClientConfig.dom-legacy.js
index ddf6440a20e48..8e91e3a8062fd 100644
--- a/packages/react-client/src/forks/ReactFlightClientConfig.dom-legacy.js
+++ b/packages/react-client/src/forks/ReactFlightClientConfig.dom-legacy.js
@@ -7,20 +7,82 @@
* @flow
*/
-export * from 'react-client/src/ReactFlightClientStreamConfigWeb';
-export * from 'react-client/src/ReactClientConsoleConfigBrowser';
-export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM';
-
-export type Response = any;
-export opaque type ModuleLoading = mixed;
-export opaque type SSRModuleMap = mixed;
-export opaque type ServerManifest = mixed;
+import type {Thenable} from 'shared/ReactTypes';
+
+export * from 'react-html/src/ReactHTMLLegacyClientStreamConfig.js';
+export * from 'react-client/src/ReactClientConsoleConfigPlain';
+
+export type ModuleLoading = null;
+export type SSRModuleMap = null;
+export opaque type ServerManifest = null;
export opaque type ServerReferenceId = string;
-export opaque type ClientReferenceMetadata = mixed;
-export opaque type ClientReference = mixed; // eslint-disable-line no-unused-vars
-export const resolveClientReference: any = null;
-export const resolveServerReference: any = null;
-export const preloadModule: any = null;
-export const requireModule: any = null;
-export const prepareDestinationForModule: any = null;
+export opaque type ClientReferenceMetadata = null;
+export opaque type ClientReference = null; // eslint-disable-line no-unused-vars
+
+export function prepareDestinationForModule(
+ moduleLoading: ModuleLoading,
+ nonce: ?string,
+ metadata: ClientReferenceMetadata,
+) {
+ throw new Error(
+ 'renderToMarkup should not have emitted Client References. This is a bug in React.',
+ );
+}
+
+export function resolveClientReference(
+ bundlerConfig: SSRModuleMap,
+ metadata: ClientReferenceMetadata,
+): ClientReference {
+ throw new Error(
+ 'renderToMarkup should not have emitted Client References. This is a bug in React.',
+ );
+}
+
+export function resolveServerReference(
+ config: ServerManifest,
+ id: ServerReferenceId,
+): ClientReference {
+ throw new Error(
+ 'renderToMarkup should not have emitted Server References. This is a bug in React.',
+ );
+}
+
+export function preloadModule(
+ metadata: ClientReference,
+): null | Thenable {
+ return null;
+}
+
+export function requireModule(metadata: ClientReference): T {
+ throw new Error(
+ 'renderToMarkup should not have emitted Client References. This is a bug in React.',
+ );
+}
+
export const usedWithSSR = true;
+
+type HintCode = string;
+type HintModel = null; // eslint-disable-line no-unused-vars
+
+export function dispatchHint(
+ code: Code,
+ model: HintModel,
+): void {
+ // Should never happen.
+}
+
+export function preinitModuleForSSR(
+ href: string,
+ nonce: ?string,
+ crossOrigin: ?string,
+) {
+ // Should never happen.
+}
+
+export function preinitScriptForSSR(
+ href: string,
+ nonce: ?string,
+ crossOrigin: ?string,
+) {
+ // Should never happen.
+}
diff --git a/packages/react-html/README.md b/packages/react-html/README.md
new file mode 100644
index 0000000000000..c0794aac104c5
--- /dev/null
+++ b/packages/react-html/README.md
@@ -0,0 +1,32 @@
+# `react-html`
+
+This package provides the ability to render standalone HTML from Server Components for use in embedded contexts such as e-mails and RSS/Atom feeds. It cannot use Client Components and does not hydrate. It is intended to be paired with the generic React package, which is shipped as `react` to npm.
+
+## Installation
+
+```sh
+npm install react react-html
+```
+
+## Usage
+
+```js
+import { renderToMarkup } from 'react-html';
+import EmailTemplate from './my-email-template-component.js'
+
+async function action(email, name) {
+ "use server";
+ // ... in your server, e.g. a Server Action...
+ const htmlString = await renderToMarkup();
+ // ... send e-mail using some e-mail provider
+ await sendEmail({ to: email, contentType: 'text/html', body: htmlString });
+}
+```
+
+Note that this is an async function that needs to be awaited - unlike the legacy `renderToString` in `react-dom`.
+
+## API
+
+### `react-html`
+
+See https://react.dev/reference/react-html
diff --git a/packages/react-html/index.js b/packages/react-html/index.js
new file mode 100644
index 0000000000000..a1818e8c3bba6
--- /dev/null
+++ b/packages/react-html/index.js
@@ -0,0 +1,5 @@
+'use strict';
+
+throw new Error(
+ 'react-html is not supported outside a React Server Components environment.',
+);
diff --git a/packages/react-html/npm/index.js b/packages/react-html/npm/index.js
new file mode 100644
index 0000000000000..e567bb2c0aa21
--- /dev/null
+++ b/packages/react-html/npm/index.js
@@ -0,0 +1,5 @@
+'use strict';
+
+throw new Error(
+ 'react-html is not supported outside a React Server Components environment.'
+);
diff --git a/packages/react-html/npm/react-html.react-server.js b/packages/react-html/npm/react-html.react-server.js
new file mode 100644
index 0000000000000..be2af1e0c5838
--- /dev/null
+++ b/packages/react-html/npm/react-html.react-server.js
@@ -0,0 +1,7 @@
+'use strict';
+
+if (process.env.NODE_ENV === 'production') {
+ module.exports = require('./cjs/react-html.react-server.production.js');
+} else {
+ module.exports = require('./cjs/react-html.react-server.development.js');
+}
diff --git a/packages/react-html/package.json b/packages/react-html/package.json
new file mode 100644
index 0000000000000..92dfc38512338
--- /dev/null
+++ b/packages/react-html/package.json
@@ -0,0 +1,38 @@
+{
+ "name": "react-html",
+ "version": "19.0.0",
+ "private": true,
+ "description": "React package generating embedded HTML markup such as e-mails using Server Components.",
+ "main": "index.js",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/facebook/react.git",
+ "directory": "packages/react-html"
+ },
+ "keywords": [
+ "react"
+ ],
+ "license": "MIT",
+ "bugs": {
+ "url": "https://github.com/facebook/react/issues"
+ },
+ "homepage": "https://react.dev/",
+ "peerDependencies": {
+ "react": "^19.0.0"
+ },
+ "files": [
+ "LICENSE",
+ "README.md",
+ "index.js",
+ "react-html.react-server.js",
+ "cjs/"
+ ],
+ "exports": {
+ ".": {
+ "react-server": "./react-html.react-server.js",
+ "default": "./index.js"
+ },
+ "./src/*": "./src/*",
+ "./package.json": "./package.json"
+ }
+}
diff --git a/packages/react-html/react-html.react-server.js b/packages/react-html/react-html.react-server.js
new file mode 100644
index 0000000000000..fb30cd4200836
--- /dev/null
+++ b/packages/react-html/react-html.react-server.js
@@ -0,0 +1,10 @@
+/**
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @flow
+ */
+
+export * from './src/ReactHTMLServer';
diff --git a/packages/react-html/src/ReactHTMLLegacyClientStreamConfig.js b/packages/react-html/src/ReactHTMLLegacyClientStreamConfig.js
new file mode 100644
index 0000000000000..74b0503590462
--- /dev/null
+++ b/packages/react-html/src/ReactHTMLLegacyClientStreamConfig.js
@@ -0,0 +1,32 @@
+/**
+ * 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
+ */
+
+// TODO: The legacy one should not use binary.
+
+export type StringDecoder = TextDecoder;
+
+export function createStringDecoder(): StringDecoder {
+ return new TextDecoder();
+}
+
+const decoderOptions = {stream: true};
+
+export function readPartialStringChunk(
+ decoder: StringDecoder,
+ buffer: Uint8Array,
+): string {
+ return decoder.decode(buffer, decoderOptions);
+}
+
+export function readFinalStringChunk(
+ decoder: StringDecoder,
+ buffer: Uint8Array,
+): string {
+ return decoder.decode(buffer);
+}
diff --git a/packages/react-html/src/ReactHTMLServer.js b/packages/react-html/src/ReactHTMLServer.js
new file mode 100644
index 0000000000000..c4eebe3f51054
--- /dev/null
+++ b/packages/react-html/src/ReactHTMLServer.js
@@ -0,0 +1,178 @@
+/**
+ * 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 {ReactNodeList} from 'shared/ReactTypes';
+import type {LazyComponent} from 'react/src/ReactLazy';
+
+import ReactVersion from 'shared/ReactVersion';
+
+import {
+ createRequest as createFlightRequest,
+ startWork as startFlightWork,
+ startFlowing as startFlightFlowing,
+ abort as abortFlight,
+} from 'react-server/src/ReactFlightServer';
+
+import {
+ createResponse as createFlightResponse,
+ getRoot as getFlightRoot,
+ processBinaryChunk as processFlightBinaryChunk,
+ close as closeFlight,
+} from 'react-client/src/ReactFlightClient';
+
+import {
+ createRequest as createFizzRequest,
+ startWork as startFizzWork,
+ startFlowing as startFizzFlowing,
+ abort as abortFizz,
+} from 'react-server/src/ReactFizzServer';
+
+import {
+ createResumableState,
+ createRenderState,
+ createRootFormatContext,
+} from 'react-dom-bindings/src/server/ReactFizzConfigDOMLegacy';
+
+type ReactMarkupNodeList =
+ // This is the intersection of ReactNodeList and ReactClientValue minus
+ // Client/ServerReferences.
+ | React$Element>
+ | LazyComponent
+ | React$Element
+ | string
+ | boolean
+ | number
+ | symbol
+ | null
+ | void
+ | bigint
+ | $AsyncIterable
+ | $AsyncIterator
+ | Iterable
+ | Iterator
+ | Array
+ | Promise; // Thenable
+
+type MarkupOptions = {
+ identifierPrefix?: string,
+ signal?: AbortSignal,
+};
+
+function noServerCallOrFormAction() {
+ throw new Error(
+ 'renderToMarkup should not have emitted Server References. This is a bug in React.',
+ );
+}
+
+export function renderToMarkup(
+ children: ReactMarkupNodeList,
+ options?: MarkupOptions,
+): Promise {
+ return new Promise((resolve, reject) => {
+ const textEncoder = new TextEncoder();
+ const flightDestination = {
+ push(chunk: string | null): boolean {
+ if (chunk !== null) {
+ // TODO: Legacy should not use binary streams.
+ processFlightBinaryChunk(flightResponse, textEncoder.encode(chunk));
+ } else {
+ closeFlight(flightResponse);
+ }
+ return true;
+ },
+ destroy(error: mixed): void {
+ abortFizz(fizzRequest, error);
+ reject(error);
+ },
+ };
+ let buffer = '';
+ const fizzDestination = {
+ // $FlowFixMe[missing-local-annot]
+ push(chunk) {
+ if (chunk !== null) {
+ buffer += chunk;
+ } else {
+ // null indicates that we finished
+ resolve(buffer);
+ }
+ return true;
+ },
+ // $FlowFixMe[missing-local-annot]
+ destroy(error) {
+ abortFlight(flightRequest, error);
+ reject(error);
+ },
+ };
+ function onError(error: mixed) {
+ // Any error rejects the promise, regardless of where it happened.
+ // Unlike other React SSR we don't want to put Suspense boundaries into
+ // client rendering mode because there's no client rendering here.
+ reject(error);
+ }
+ const flightRequest = createFlightRequest(
+ // $FlowFixMe: This should be a subtype but not everything is typed covariant.
+ children,
+ null,
+ onError,
+ options ? options.identifierPrefix : undefined,
+ undefined,
+ 'Markup',
+ undefined,
+ );
+ const flightResponse = createFlightResponse(
+ null,
+ null,
+ noServerCallOrFormAction,
+ noServerCallOrFormAction,
+ undefined,
+ undefined,
+ undefined,
+ );
+ const resumableState = createResumableState(
+ options ? options.identifierPrefix : undefined,
+ undefined,
+ );
+ const root = getFlightRoot(flightResponse);
+ const fizzRequest = createFizzRequest(
+ // $FlowFixMe: Thenables as children are supported.
+ root,
+ resumableState,
+ createRenderState(resumableState, true),
+ createRootFormatContext(),
+ Infinity,
+ onError,
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ );
+ if (options && options.signal) {
+ const signal = options.signal;
+ if (signal.aborted) {
+ abortFlight(flightRequest, (signal: any).reason);
+ abortFizz(fizzRequest, (signal: any).reason);
+ } else {
+ const listener = () => {
+ abortFlight(flightRequest, (signal: any).reason);
+ abortFizz(fizzRequest, (signal: any).reason);
+ signal.removeEventListener('abort', listener);
+ };
+ signal.addEventListener('abort', listener);
+ }
+ }
+ startFlightWork(flightRequest);
+ startFlightFlowing(flightRequest, flightDestination);
+ startFizzWork(fizzRequest);
+ startFizzFlowing(fizzRequest, fizzDestination);
+ });
+}
+
+export {ReactVersion as version};
diff --git a/packages/react-html/src/__tests__/ReactHTMLServer-test.js b/packages/react-html/src/__tests__/ReactHTMLServer-test.js
new file mode 100644
index 0000000000000..503e233301c9a
--- /dev/null
+++ b/packages/react-html/src/__tests__/ReactHTMLServer-test.js
@@ -0,0 +1,112 @@
+/**
+ * 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.
+ *
+ * @emails react-core
+ */
+
+'use strict';
+
+global.TextDecoder = require('util').TextDecoder;
+global.TextEncoder = require('util').TextEncoder;
+
+let React;
+let ReactHTML;
+
+describe('ReactHTML', () => {
+ beforeEach(() => {
+ jest.resetModules();
+ // We run in the react-server condition.
+ jest.mock('react', () => require('react/react.react-server'));
+ jest.mock('react-html', () =>
+ require('react-html/react-html.react-server'),
+ );
+
+ React = require('react');
+ ReactHTML = require('react-html');
+ });
+
+ it('should be able to render a simple component', async () => {
+ function Component() {
+ // We can't use JSX because that's client-JSX in our tests.
+ return React.createElement('div', null, 'hello world');
+ }
+
+ const html = await ReactHTML.renderToMarkup(React.createElement(Component));
+ expect(html).toBe('
');
+ });
+});
diff --git a/packages/react-server/src/forks/ReactFlightServerConfig.dom-legacy.js b/packages/react-server/src/forks/ReactFlightServerConfig.dom-legacy.js
index bfe794c9a2c55..99591bb954ea9 100644
--- a/packages/react-server/src/forks/ReactFlightServerConfig.dom-legacy.js
+++ b/packages/react-server/src/forks/ReactFlightServerConfig.dom-legacy.js
@@ -9,9 +9,15 @@
import type {Request} from 'react-server/src/ReactFlightServer';
import type {ReactComponentInfo} from 'shared/ReactTypes';
+import type {ReactClientValue} from 'react-server/src/ReactFlightServer';
-export * from '../ReactFlightServerConfigBundlerCustom';
-export * from 'react-dom-bindings/src/server/ReactFlightServerConfigDOM';
+export type HintCode = string;
+export type HintModel = null; // eslint-disable-line no-unused-vars
+export type Hints = null;
+
+export function createHints(): Hints {
+ return null;
+}
export const supportsRequestStorage = false;
export const requestStorage: AsyncLocalStorage = (null: any);
@@ -21,3 +27,64 @@ export const componentStorage: AsyncLocalStorage =
(null: any);
export * from '../ReactFlightServerConfigDebugNoop';
+
+export type ClientManifest = null;
+export opaque type ClientReference = null; // eslint-disable-line no-unused-vars
+export opaque type ServerReference = null; // eslint-disable-line no-unused-vars
+export opaque type ClientReferenceMetadata: any = null;
+export opaque type ServerReferenceId: string = string;
+export opaque type ClientReferenceKey: any = string;
+
+const CLIENT_REFERENCE_TAG = Symbol.for('react.client.reference');
+const SERVER_REFERENCE_TAG = Symbol.for('react.server.reference');
+
+export function isClientReference(reference: Object): boolean {
+ return reference.$$typeof === CLIENT_REFERENCE_TAG;
+}
+
+export function isServerReference(reference: Object): boolean {
+ return reference.$$typeof === SERVER_REFERENCE_TAG;
+}
+
+export function getClientReferenceKey(
+ reference: ClientReference,
+): ClientReferenceKey {
+ throw new Error(
+ 'Attempted to render a Client Component from renderToMarkup. ' +
+ 'This is not supported since it will never hydrate. ' +
+ 'Only render Server Components with renderToMarkup.',
+ );
+}
+
+export function resolveClientReferenceMetadata(
+ config: ClientManifest,
+ clientReference: ClientReference,
+): ClientReferenceMetadata {
+ throw new Error(
+ 'Attempted to render a Client Component from renderToMarkup. ' +
+ 'This is not supported since it will never hydrate. ' +
+ 'Only render Server Components with renderToMarkup.',
+ );
+}
+
+export function getServerReferenceId(
+ config: ClientManifest,
+ serverReference: ServerReference,
+): ServerReferenceId {
+ throw new Error(
+ 'Attempted to render a Server Action from renderToMarkup. ' +
+ 'This is not supported since it varies by version of the app. ' +
+ 'Use a fixed URL for any forms instead.',
+ );
+}
+
+export function getServerReferenceBoundArguments(
+ config: ClientManifest,
+ serverReference: ServerReference,
+): null | Array {
+ throw new Error(
+ 'Attempted to render a Server Action from renderToMarkup. ' +
+ 'This is not supported since it varies by version of the app. ' +
+ 'Use a fixed URL for any forms instead.',
+ );
+}
diff --git a/scripts/error-codes/codes.json b/scripts/error-codes/codes.json
index ef4ae75a6d634..9d8fb4ed3739f 100644
--- a/scripts/error-codes/codes.json
+++ b/scripts/error-codes/codes.json
@@ -515,5 +515,10 @@
"527": "Incompatible React versions: The \"react\" and \"react-dom\" packages must have the exact same version. Instead got:\n - react: %s\n - react-dom: %s\nLearn more: https://react.dev/warnings/version-mismatch",
"528": "Expected not to update to be updated to a stylesheet with precedence. Check the `rel`, `href`, and `precedence` props of this component. Alternatively, check whether two different components render in the same slot or share the same key.%s",
"529": "Expected stylesheet with precedence to not be updated to a different kind of . Check the `rel`, `href`, and `precedence` props of this component. Alternatively, check whether two different components render in the same slot or share the same key.%s",
- "530": "The render was aborted by the server with a promise."
+ "530": "The render was aborted by the server with a promise.",
+ "531": "react-html is not supported outside a React Server Components environment.",
+ "532": "Attempted to render a Client Component from renderToMarkup. This is not supported since it will never hydrate. Only render Server Components with renderToMarkup.",
+ "533": "Attempted to render a Server Action from renderToMarkup. This is not supported since it varies by version of the app. Use a fixed URL for any forms instead.",
+ "534": "renderToMarkup should not have emitted Client References. This is a bug in React.",
+ "535": "renderToMarkup should not have emitted Server References. This is a bug in React."
}
diff --git a/scripts/rollup/bundles.js b/scripts/rollup/bundles.js
index 66e59124ca2d5..67f1c237b209f 100644
--- a/scripts/rollup/bundles.js
+++ b/scripts/rollup/bundles.js
@@ -363,6 +363,19 @@ const bundles = [
externals: [],
},
+ /******* React HTML *******/
+ {
+ bundleTypes: [NODE_DEV, NODE_PROD],
+ moduleType: RENDERER,
+ entry: 'react-html/src/ReactHTMLServer.js',
+ name: 'react-html.react-server',
+ condition: 'react-server',
+ global: 'ReactHTML',
+ minifyWithProdErrorCodes: false,
+ wrapWithModuleBoundaries: false,
+ externals: ['react'],
+ },
+
/******* React Server DOM Webpack Server *******/
{
bundleTypes: [NODE_DEV, NODE_PROD],
diff --git a/scripts/rollup/forks.js b/scripts/rollup/forks.js
index a83b8b1b1a969..0ba6827bba7a2 100644
--- a/scripts/rollup/forks.js
+++ b/scripts/rollup/forks.js
@@ -65,6 +65,12 @@ const forks = Object.freeze({
if (entry === 'react/src/ReactServer.js') {
return './packages/react/src/ReactSharedInternalsServer.js';
}
+ if (entry === 'react-html/src/ReactHTMLServer.js') {
+ // Inside the ReactHTMLServer render we don't refer to any shared internals
+ // but instead use our own internal copy of the state because you cannot use
+ // any of this state from a component anyway. E.g. you can't use a client hook.
+ return './packages/react/src/ReactSharedInternalsClient.js';
+ }
if (bundle.condition === 'react-server') {
return './packages/react-server/src/ReactSharedInternalsServer.js';
}
@@ -93,7 +99,8 @@ const forks = Object.freeze({
entry === 'react-dom' ||
entry === 'react-dom/src/ReactDOMFB.js' ||
entry === 'react-dom/src/ReactDOMTestingFB.js' ||
- entry === 'react-dom/src/ReactDOMServer.js'
+ entry === 'react-dom/src/ReactDOMServer.js' ||
+ entry === 'react-html/src/ReactHTMLServer.js'
) {
if (
bundleType === FB_WWW_DEV ||
diff --git a/scripts/shared/inlinedHostConfigs.js b/scripts/shared/inlinedHostConfigs.js
index a63da44c36f29..a21b624150aac 100644
--- a/scripts/shared/inlinedHostConfigs.js
+++ b/scripts/shared/inlinedHostConfigs.js
@@ -36,6 +36,9 @@ module.exports = [
'react-dom/src/server/react-dom-server.node',
'react-dom/src/server/ReactDOMFizzServerNode.js', // react-dom/server.node
'react-dom/src/server/ReactDOMFizzStaticNode.js',
+ 'react-dom-bindings/src/server/ReactDOMFlightServerHostDispatcher.js',
+ 'react-dom-bindings/src/server/ReactFlightServerConfigDOM.js',
+ 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM.js',
'react-server-dom-webpack',
'react-server-dom-webpack/client.node.unbundled',
'react-server-dom-webpack/server',
@@ -71,6 +74,9 @@ module.exports = [
'react-dom/src/server/react-dom-server.node',
'react-dom/src/server/ReactDOMFizzServerNode.js', // react-dom/server.node
'react-dom/src/server/ReactDOMFizzStaticNode.js',
+ 'react-dom-bindings/src/server/ReactDOMFlightServerHostDispatcher.js',
+ 'react-dom-bindings/src/server/ReactFlightServerConfigDOM.js',
+ 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM.js',
'react-server-dom-webpack',
'react-server-dom-webpack/client.node',
'react-server-dom-webpack/server',
@@ -108,6 +114,9 @@ module.exports = [
'react-dom/src/server/react-dom-server.node',
'react-dom/src/server/ReactDOMFizzServerNode.js', // react-dom/server.node
'react-dom/src/server/ReactDOMFizzStaticNode.js',
+ 'react-dom-bindings/src/server/ReactDOMFlightServerHostDispatcher.js',
+ 'react-dom-bindings/src/server/ReactFlightServerConfigDOM.js',
+ 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM.js',
'react-server-dom-turbopack',
'react-server-dom-turbopack/client.node.unbundled',
'react-server-dom-turbopack/server',
@@ -145,6 +154,9 @@ module.exports = [
'react-dom/src/server/react-dom-server.node',
'react-dom/src/server/ReactDOMFizzServerNode.js', // react-dom/server.node
'react-dom/src/server/ReactDOMFizzStaticNode.js',
+ 'react-dom-bindings/src/server/ReactDOMFlightServerHostDispatcher.js',
+ 'react-dom-bindings/src/server/ReactFlightServerConfigDOM.js',
+ 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM.js',
'react-server-dom-turbopack',
'react-server-dom-turbopack/client.node',
'react-server-dom-turbopack/server',
@@ -182,6 +194,9 @@ module.exports = [
'react-dom/src/server/react-dom-server.bun',
'react-dom/src/server/ReactDOMFizzServerBun.js',
'react-dom-bindings',
+ 'react-dom-bindings/src/server/ReactDOMFlightServerHostDispatcher.js',
+ 'react-dom-bindings/src/server/ReactFlightServerConfigDOM.js',
+ 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM.js',
'shared/ReactDOMSharedInternals',
],
isFlowTyped: true,
@@ -212,6 +227,9 @@ module.exports = [
'react-dom/src/server/react-dom-server.browser',
'react-dom/src/server/ReactDOMFizzServerBrowser.js', // react-dom/server.browser
'react-dom/src/server/ReactDOMFizzStaticBrowser.js',
+ 'react-dom-bindings/src/server/ReactDOMFlightServerHostDispatcher.js',
+ 'react-dom-bindings/src/server/ReactFlightServerConfigDOM.js',
+ 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM.js',
'react-server-dom-webpack',
'react-server-dom-webpack/client',
'react-server-dom-webpack/client.browser',
@@ -240,6 +258,9 @@ module.exports = [
'react-dom/server',
'react-dom/server.node',
'react-dom-bindings',
+ 'react-dom-bindings/src/server/ReactDOMFlightServerHostDispatcher.js',
+ 'react-dom-bindings/src/server/ReactFlightServerConfigDOM.js',
+ 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM.js',
'react-server-dom-esm',
'react-server-dom-esm/client',
'react-server-dom-esm/client.browser',
@@ -274,6 +295,9 @@ module.exports = [
'react-server-dom-turbopack/src/ReactFlightDOMServerBrowser.js', // react-server-dom-turbopack/server.browser
'react-server-dom-turbopack/src/ReactFlightClientConfigBundlerTurbopack.js',
'react-server-dom-turbopack/src/ReactFlightClientConfigBundlerTurbopackBrowser.js',
+ 'react-dom-bindings/src/server/ReactDOMFlightServerHostDispatcher.js',
+ 'react-dom-bindings/src/server/ReactFlightServerConfigDOM.js',
+ 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM.js',
'react-devtools',
'react-devtools-core',
'react-devtools-shell',
@@ -303,6 +327,9 @@ module.exports = [
'react-dom/src/server/react-dom-server.edge',
'react-dom/src/server/ReactDOMFizzServerEdge.js', // react-dom/server.edge
'react-dom/src/server/ReactDOMFizzStaticEdge.js',
+ 'react-dom-bindings/src/server/ReactDOMFlightServerHostDispatcher.js',
+ 'react-dom-bindings/src/server/ReactFlightServerConfigDOM.js',
+ 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM.js',
'react-server-dom-webpack',
'react-server-dom-webpack/client.edge',
'react-server-dom-webpack/server.edge',
@@ -338,6 +365,9 @@ module.exports = [
'react-dom/src/server/react-dom-server.edge',
'react-dom/src/server/ReactDOMFizzServerEdge.js', // react-dom/server.edge
'react-dom/src/server/ReactDOMFizzStaticEdge.js',
+ 'react-dom-bindings/src/server/ReactDOMFlightServerHostDispatcher.js',
+ 'react-dom-bindings/src/server/ReactFlightServerConfigDOM.js',
+ 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM.js',
'react-server-dom-turbopack',
'react-server-dom-turbopack/client.edge',
'react-server-dom-turbopack/server.edge',
@@ -374,6 +404,9 @@ module.exports = [
'react-dom/src/server/react-dom-server.node',
'react-dom/src/server/ReactDOMFizzServerNode.js', // react-dom/server.node
'react-dom/src/server/ReactDOMFizzStaticNode.js',
+ 'react-dom-bindings/src/server/ReactDOMFlightServerHostDispatcher.js',
+ 'react-dom-bindings/src/server/ReactFlightServerConfigDOM.js',
+ 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM.js',
'react-server-dom-esm',
'react-server-dom-esm/client.node',
'react-server-dom-esm/server',
@@ -395,6 +428,7 @@ module.exports = [
entryPoints: [
'react-dom/src/server/ReactDOMLegacyServerBrowser.js', // react-dom/server.browser
'react-dom/src/server/ReactDOMLegacyServerNode.js', // react-dom/server.node
+ 'react-html/src/ReactHTMLServer.js',
],
paths: [
'react-dom',
@@ -404,6 +438,7 @@ module.exports = [
'react-dom/src/server/ReactDOMLegacyServerImpl.js', // not an entrypoint, but only usable in *Browser and *Node files
'react-dom/src/server/ReactDOMLegacyServerBrowser.js', // react-dom/server.browser
'react-dom/src/server/ReactDOMLegacyServerNode.js', // react-dom/server.node
+ 'react-html',
'shared/ReactDOMSharedInternals',
],
isFlowTyped: true,
From e02baf6c92833a0d45a77fb2e741676f393c24f7 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?=
Date: Thu, 27 Jun 2024 18:10:09 +0200
Subject: [PATCH 32/38] Warn for invalid type in renderer with the correct RSC
stack (#30102)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
This is all behind the `enableOwnerStacks` flag.
This is a follow up to #29088. In that I moved type validation into the
renderer since that's the one that knows what types are allowed.
However, I only removed it from `React.createElement` and not the JSX
which was an oversight.
However, I also noticed that for invalid types we don't have the right
stack trace for throws because we're not yet inside the JSX element that
itself is invalid. We should use its stack for the stack trace. That's
the reason it's enough to just use the throw now because we can get a
good stack trace from the owner stack. This is fixed by creating a fake
Throw Fiber that gets assigned the right stack.
Additionally, I noticed that for certain invalid types like the most
common one `undefined` we error in Flight so a missing import in RSC
leads to a generic error. Instead of erroring on the Flight side we
should just let anything that's not a Server Component through to the
client and then let the Client renderer determine whether it's a valid
type or not. Since we now have owner stacks through the server too, this
will still be able to provide a good stack trace on the client that
points to the server in that case.
To get the best stack you have to expand the little icon and the regular
stack is noisy [due to this Chrome
bug](https://issues.chromium.org/issues/345248263) which makes it a
little harder to find but once that's fixed it might be easier.
---
.../src/__tests__/ReactFlight-test.js | 22 +++--
.../src/__tests__/ReactComponent-test.js | 84 +++++++++++------
.../ReactDOMServerIntegrationElements-test.js | 32 ++++---
...eactLegacyErrorBoundaries-test.internal.js | 57 +++++++-----
.../react-reconciler/src/ReactChildFiber.js | 6 ++
packages/react-reconciler/src/ReactFiber.js | 10 +-
...rorBoundaryReconciliation-test.internal.js | 23 +++--
...tIncrementalErrorHandling-test.internal.js | 42 ++++++---
.../react-server/src/ReactFlightServer.js | 93 ++++++++-----------
.../ReactElementValidator-test.internal.js | 14 ++-
.../ReactJSXElementValidator-test.js | 29 ------
packages/react/src/jsx/ReactJSXElement.js | 21 ++++-
12 files changed, 250 insertions(+), 183 deletions(-)
diff --git a/packages/react-client/src/__tests__/ReactFlight-test.js b/packages/react-client/src/__tests__/ReactFlight-test.js
index 8a652dab4aaa8..2e3857a90deca 100644
--- a/packages/react-client/src/__tests__/ReactFlight-test.js
+++ b/packages/react-client/src/__tests__/ReactFlight-test.js
@@ -692,14 +692,22 @@ describe('ReactFlight', () => {
const transport = ReactNoopFlightServer.render();
- await act(async () => {
- const rootModel = await ReactNoopFlightClient.read(transport);
- ReactNoop.render(rootModel);
- });
- expect(ReactNoop).toMatchRenderedOutput('Loading...');
- spyOnDevAndProd(console, 'error').mockImplementation(() => {});
await load();
- expect(console.error).toHaveBeenCalledTimes(1);
+
+ await expect(async () => {
+ await act(async () => {
+ const rootModel = await ReactNoopFlightClient.read(transport);
+ ReactNoop.render(rootModel);
+ });
+ }).rejects.toThrow(
+ __DEV__
+ ? 'Element type is invalid: expected a string (for built-in components) or a class/function ' +
+ '(for composite components) but got: . ' +
+ 'Did you accidentally export a JSX literal instead of a component?'
+ : 'Element type is invalid: expected a string (for built-in components) or a class/function ' +
+ '(for composite components) but got: object.',
+ );
+ expect(ReactNoop).toMatchRenderedOutput(null);
});
it('can render a lazy element', async () => {
diff --git a/packages/react-dom/src/__tests__/ReactComponent-test.js b/packages/react-dom/src/__tests__/ReactComponent-test.js
index f0d58280cd8c1..9b2f443a7c34d 100644
--- a/packages/react-dom/src/__tests__/ReactComponent-test.js
+++ b/packages/react-dom/src/__tests__/ReactComponent-test.js
@@ -14,6 +14,7 @@ let ReactDOM;
let ReactDOMClient;
let ReactDOMServer;
let act;
+let assertConsoleErrorDev;
describe('ReactComponent', () => {
beforeEach(() => {
@@ -24,6 +25,8 @@ describe('ReactComponent', () => {
ReactDOMClient = require('react-dom/client');
ReactDOMServer = require('react-dom/server');
act = require('internal-test-utils').act;
+ assertConsoleErrorDev =
+ require('internal-test-utils').assertConsoleErrorDev;
});
// @gate !disableLegacyMode
@@ -131,8 +134,6 @@ describe('ReactComponent', () => {
// @gate !disableStringRefs
it('string refs do not detach and reattach on every render', async () => {
- spyOnDev(console, 'error').mockImplementation(() => {});
-
let refVal;
class Child extends React.Component {
componentDidUpdate() {
@@ -171,6 +172,8 @@ describe('ReactComponent', () => {
root.render();
});
+ assertConsoleErrorDev(['contains the string ref']);
+
expect(refVal).toBe(undefined);
await act(() => {
root.render();
@@ -511,19 +514,25 @@ describe('ReactComponent', () => {
});
it('throws usefully when rendering badly-typed elements', async () => {
+ const container = document.createElement('div');
+ const root = ReactDOMClient.createRoot(container);
+
const X = undefined;
- let container = document.createElement('div');
- let root = ReactDOMClient.createRoot(container);
- await expect(
- expect(async () => {
- await act(() => {
- root.render();
- });
- }).toErrorDev(
- 'React.jsx: type is invalid -- expected a string (for built-in components) ' +
- 'or a class/function (for composite components) but got: undefined.',
- ),
- ).rejects.toThrowError(
+ const XElement = ;
+ if (gate(flags => !flags.enableOwnerStacks)) {
+ assertConsoleErrorDev(
+ [
+ 'React.jsx: type is invalid -- expected a string (for built-in components) ' +
+ 'or a class/function (for composite components) but got: undefined.',
+ ],
+ {withoutStack: true},
+ );
+ }
+ await expect(async () => {
+ await act(() => {
+ root.render(XElement);
+ });
+ }).rejects.toThrowError(
'Element type is invalid: expected a string (for built-in components) ' +
'or a class/function (for composite components) but got: undefined.' +
(__DEV__
@@ -533,21 +542,44 @@ describe('ReactComponent', () => {
);
const Y = null;
- container = document.createElement('div');
- root = ReactDOMClient.createRoot(container);
- await expect(
- expect(async () => {
- await act(() => {
- root.render();
- });
- }).toErrorDev(
- 'React.jsx: type is invalid -- expected a string (for built-in components) ' +
- 'or a class/function (for composite components) but got: null.',
- ),
- ).rejects.toThrowError(
+ const YElement = ;
+ if (gate(flags => !flags.enableOwnerStacks)) {
+ assertConsoleErrorDev(
+ [
+ 'React.jsx: type is invalid -- expected a string (for built-in components) ' +
+ 'or a class/function (for composite components) but got: null.',
+ ],
+ {withoutStack: true},
+ );
+ }
+ await expect(async () => {
+ await act(() => {
+ root.render(YElement);
+ });
+ }).rejects.toThrowError(
'Element type is invalid: expected a string (for built-in components) ' +
'or a class/function (for composite components) but got: null.',
);
+
+ const Z = true;
+ const ZElement = ;
+ if (gate(flags => !flags.enableOwnerStacks)) {
+ assertConsoleErrorDev(
+ [
+ 'React.jsx: type is invalid -- expected a string (for built-in components) ' +
+ 'or a class/function (for composite components) but got: boolean.',
+ ],
+ {withoutStack: true},
+ );
+ }
+ await expect(async () => {
+ await act(() => {
+ root.render(ZElement);
+ });
+ }).rejects.toThrowError(
+ 'Element type is invalid: expected a string (for built-in components) ' +
+ 'or a class/function (for composite components) but got: boolean.',
+ );
});
it('includes owner name in the error about badly-typed elements', async () => {
diff --git a/packages/react-dom/src/__tests__/ReactDOMServerIntegrationElements-test.js b/packages/react-dom/src/__tests__/ReactDOMServerIntegrationElements-test.js
index 2912a4f401dec..0fcc314d39a05 100644
--- a/packages/react-dom/src/__tests__/ReactDOMServerIntegrationElements-test.js
+++ b/packages/react-dom/src/__tests__/ReactDOMServerIntegrationElements-test.js
@@ -987,11 +987,13 @@ describe('ReactDOMServerIntegration', () => {
expect(() => {
EmptyComponent = ;
}).toErrorDev(
- 'React.jsx: type is invalid -- expected a string ' +
- '(for built-in components) or a class/function (for composite ' +
- 'components) but got: object. You likely forgot to export your ' +
- "component from the file it's defined in, or you might have mixed up " +
- 'default and named imports.',
+ gate(flags => flags.enableOwnerStacks)
+ ? []
+ : 'React.jsx: type is invalid -- expected a string ' +
+ '(for built-in components) or a class/function (for composite ' +
+ 'components) but got: object. You likely forgot to export your ' +
+ "component from the file it's defined in, or you might have mixed up " +
+ 'default and named imports.',
{withoutStack: true},
);
await render(EmptyComponent);
@@ -1011,9 +1013,11 @@ describe('ReactDOMServerIntegration', () => {
expect(() => {
NullComponent = ;
}).toErrorDev(
- 'React.jsx: type is invalid -- expected a string ' +
- '(for built-in components) or a class/function (for composite ' +
- 'components) but got: null.',
+ gate(flags => flags.enableOwnerStacks)
+ ? []
+ : 'React.jsx: type is invalid -- expected a string ' +
+ '(for built-in components) or a class/function (for composite ' +
+ 'components) but got: null.',
{withoutStack: true},
);
await render(NullComponent);
@@ -1029,11 +1033,13 @@ describe('ReactDOMServerIntegration', () => {
expect(() => {
UndefinedComponent = ;
}).toErrorDev(
- 'React.jsx: type is invalid -- expected a string ' +
- '(for built-in components) or a class/function (for composite ' +
- 'components) but got: undefined. You likely forgot to export your ' +
- "component from the file it's defined in, or you might have mixed up " +
- 'default and named imports.',
+ gate(flags => flags.enableOwnerStacks)
+ ? []
+ : 'React.jsx: type is invalid -- expected a string ' +
+ '(for built-in components) or a class/function (for composite ' +
+ 'components) but got: undefined. You likely forgot to export your ' +
+ "component from the file it's defined in, or you might have mixed up " +
+ 'default and named imports.',
{withoutStack: true},
);
diff --git a/packages/react-dom/src/__tests__/ReactLegacyErrorBoundaries-test.internal.js b/packages/react-dom/src/__tests__/ReactLegacyErrorBoundaries-test.internal.js
index 2809460c5b06b..f45fae7bbd7e1 100644
--- a/packages/react-dom/src/__tests__/ReactLegacyErrorBoundaries-test.internal.js
+++ b/packages/react-dom/src/__tests__/ReactLegacyErrorBoundaries-test.internal.js
@@ -13,6 +13,7 @@ let PropTypes;
let React;
let ReactDOM;
let act;
+let assertConsoleErrorDev;
// TODO: Refactor this test once componentDidCatch setState is deprecated.
describe('ReactLegacyErrorBoundaries', () => {
@@ -42,6 +43,8 @@ describe('ReactLegacyErrorBoundaries', () => {
ReactDOM = require('react-dom');
React = require('react');
act = require('internal-test-utils').act;
+ assertConsoleErrorDev =
+ require('internal-test-utils').assertConsoleErrorDev;
log = [];
@@ -2099,32 +2102,38 @@ describe('ReactLegacyErrorBoundaries', () => {
const Y = undefined;
await expect(async () => {
- await expect(async () => {
- const container = document.createElement('div');
- await act(() => {
- ReactDOM.render(, container);
- });
- }).rejects.toThrow('got: null');
- }).toErrorDev(
- 'React.jsx: type is invalid -- expected a string ' +
- '(for built-in components) or a class/function ' +
- '(for composite components) but got: null.',
- {withoutStack: 1},
- );
+ const container = document.createElement('div');
+ await act(() => {
+ ReactDOM.render(, container);
+ });
+ }).rejects.toThrow('got: null');
+ if (gate(flags => !flags.enableOwnerStacks)) {
+ assertConsoleErrorDev(
+ [
+ 'React.jsx: type is invalid -- expected a string ' +
+ '(for built-in components) or a class/function ' +
+ '(for composite components) but got: null.',
+ ],
+ {withoutStack: true},
+ );
+ }
await expect(async () => {
- await expect(async () => {
- const container = document.createElement('div');
- await act(() => {
- ReactDOM.render(, container);
- });
- }).rejects.toThrow('got: undefined');
- }).toErrorDev(
- 'React.jsx: type is invalid -- expected a string ' +
- '(for built-in components) or a class/function ' +
- '(for composite components) but got: undefined.',
- {withoutStack: 1},
- );
+ const container = document.createElement('div');
+ await act(() => {
+ ReactDOM.render(, container);
+ });
+ }).rejects.toThrow('got: undefined');
+ if (gate(flags => !flags.enableOwnerStacks)) {
+ assertConsoleErrorDev(
+ [
+ 'React.jsx: type is invalid -- expected a string ' +
+ '(for built-in components) or a class/function ' +
+ '(for composite components) but got: undefined.',
+ ],
+ {withoutStack: true},
+ );
+ }
});
// @gate !disableLegacyMode
diff --git a/packages/react-reconciler/src/ReactChildFiber.js b/packages/react-reconciler/src/ReactChildFiber.js
index db8f0e6233644..10edf8537a53f 100644
--- a/packages/react-reconciler/src/ReactChildFiber.js
+++ b/packages/react-reconciler/src/ReactChildFiber.js
@@ -220,6 +220,9 @@ function validateFragmentProps(
// For unkeyed root fragments there's no Fiber. We create a fake one just for
// error stack handling.
fiber = createFiberFromElement(element, returnFiber.mode, 0);
+ if (__DEV__) {
+ fiber._debugInfo = currentDebugInfo;
+ }
fiber.return = returnFiber;
}
runWithFiberInDEV(
@@ -242,6 +245,9 @@ function validateFragmentProps(
// For unkeyed root fragments there's no Fiber. We create a fake one just for
// error stack handling.
fiber = createFiberFromElement(element, returnFiber.mode, 0);
+ if (__DEV__) {
+ fiber._debugInfo = currentDebugInfo;
+ }
fiber.return = returnFiber;
}
runWithFiberInDEV(fiber, () => {
diff --git a/packages/react-reconciler/src/ReactFiber.js b/packages/react-reconciler/src/ReactFiber.js
index da67714d0906f..2911b1bf5c549 100644
--- a/packages/react-reconciler/src/ReactFiber.js
+++ b/packages/react-reconciler/src/ReactFiber.js
@@ -485,6 +485,7 @@ export function createHostRootFiber(
return createFiber(HostRoot, null, null, mode);
}
+// TODO: Get rid of this helper. Only createFiberFromElement should exist.
export function createFiberFromTypeAndProps(
type: any, // React$ElementType
key: null | string,
@@ -650,11 +651,18 @@ export function createFiberFromTypeAndProps(
typeString = type === null ? 'null' : typeof type;
}
- throw new Error(
+ // The type is invalid but it's conceptually a child that errored and not the
+ // current component itself so we create a virtual child that throws in its
+ // begin phase. This is the same thing we do in ReactChildFiber if we throw
+ // but we do it here so that we can assign the debug owner and stack from the
+ // element itself. That way the error stack will point to the JSX callsite.
+ fiberTag = Throw;
+ pendingProps = new Error(
'Element type is invalid: expected a string (for built-in ' +
'components) or a class/function (for composite components) ' +
`but got: ${typeString}.${info}`,
);
+ resolvedType = null;
}
}
}
diff --git a/packages/react-reconciler/src/__tests__/ErrorBoundaryReconciliation-test.internal.js b/packages/react-reconciler/src/__tests__/ErrorBoundaryReconciliation-test.internal.js
index b577bd1bc074e..43f58ffc4bfd5 100644
--- a/packages/react-reconciler/src/__tests__/ErrorBoundaryReconciliation-test.internal.js
+++ b/packages/react-reconciler/src/__tests__/ErrorBoundaryReconciliation-test.internal.js
@@ -6,6 +6,7 @@ describe('ErrorBoundaryReconciliation', () => {
let ReactTestRenderer;
let span;
let act;
+ let assertConsoleErrorDev;
beforeEach(() => {
jest.resetModules();
@@ -13,6 +14,8 @@ describe('ErrorBoundaryReconciliation', () => {
ReactTestRenderer = require('react-test-renderer');
React = require('react');
act = require('internal-test-utils').act;
+ assertConsoleErrorDev =
+ require('internal-test-utils').assertConsoleErrorDev;
DidCatchErrorBoundary = class extends React.Component {
state = {error: null};
componentDidCatch(error) {
@@ -58,15 +61,17 @@ describe('ErrorBoundaryReconciliation', () => {
);
});
expect(renderer).toMatchRenderedOutput();
- await expect(async () => {
- await act(() => {
- renderer.update(
-
-
- ,
- );
- });
- }).toErrorDev(['invalid', 'invalid']);
+ await act(() => {
+ renderer.update(
+
+
+ ,
+ );
+ });
+ if (gate(flags => !flags.enableOwnerStacks)) {
+ assertConsoleErrorDev(['invalid', 'invalid']);
+ }
+
const Fallback = fallbackTagName;
expect(renderer).toMatchRenderedOutput();
}
diff --git a/packages/react-reconciler/src/__tests__/ReactIncrementalErrorHandling-test.internal.js b/packages/react-reconciler/src/__tests__/ReactIncrementalErrorHandling-test.internal.js
index e800dd74e10a3..0cee8150cb15f 100644
--- a/packages/react-reconciler/src/__tests__/ReactIncrementalErrorHandling-test.internal.js
+++ b/packages/react-reconciler/src/__tests__/ReactIncrementalErrorHandling-test.internal.js
@@ -19,6 +19,7 @@ let assertLog;
let waitForAll;
let waitFor;
let waitForThrow;
+let assertConsoleErrorDev;
describe('ReactIncrementalErrorHandling', () => {
beforeEach(() => {
@@ -28,6 +29,8 @@ describe('ReactIncrementalErrorHandling', () => {
ReactNoop = require('react-noop-renderer');
Scheduler = require('scheduler');
act = require('internal-test-utils').act;
+ assertConsoleErrorDev =
+ require('internal-test-utils').assertConsoleErrorDev;
const InternalTestUtils = require('internal-test-utils');
assertLog = InternalTestUtils.assertLog;
@@ -1237,11 +1240,15 @@ describe('ReactIncrementalErrorHandling', () => {
,
);
- await expect(async () => await waitForAll([])).toErrorDev([
- 'React.jsx: type is invalid -- expected a string',
- // React retries once on error
- 'React.jsx: type is invalid -- expected a string',
- ]);
+ await waitForAll([]);
+ if (gate(flags => !flags.enableOwnerStacks)) {
+ assertConsoleErrorDev([
+ 'React.jsx: type is invalid -- expected a string',
+ // React retries once on error
+ 'React.jsx: type is invalid -- expected a string',
+ ]);
+ }
+
expect(ReactNoop).toMatchRenderedOutput(
{
,
);
- await expect(async () => await waitForAll([])).toErrorDev([
- 'React.jsx: type is invalid -- expected a string',
- // React retries once on error
- 'React.jsx: type is invalid -- expected a string',
- ]);
+ await waitForAll([]);
+ if (gate(flags => !flags.enableOwnerStacks)) {
+ assertConsoleErrorDev([
+ 'React.jsx: type is invalid -- expected a string',
+ // React retries once on error
+ 'React.jsx: type is invalid -- expected a string',
+ ]);
+ }
expect(ReactNoop).toMatchRenderedOutput(
{
it('recovers from uncaught reconciler errors', async () => {
const InvalidType = undefined;
- expect(() => ReactNoop.render()).toErrorDev(
- 'React.jsx: type is invalid -- expected a string',
- {withoutStack: true},
- );
+ ReactNoop.render();
+ if (gate(flags => !flags.enableOwnerStacks)) {
+ assertConsoleErrorDev(
+ ['React.jsx: type is invalid -- expected a string'],
+ {withoutStack: true},
+ );
+ }
+
await waitForThrow(
'Element type is invalid: expected a string (for built-in components) or ' +
'a class/function (for composite components) but got: undefined.' +
diff --git a/packages/react-server/src/ReactFlightServer.js b/packages/react-server/src/ReactFlightServer.js
index 02847f204bc3e..9987c689f2aa8 100644
--- a/packages/react-server/src/ReactFlightServer.js
+++ b/packages/react-server/src/ReactFlightServer.js
@@ -110,7 +110,6 @@ import {
} from 'shared/ReactSymbols';
import {
- describeValueForErrorMessage,
describeObjectForErrorMessage,
isSimpleObject,
jsxPropsParents,
@@ -1501,19 +1500,11 @@ function renderElement(
jsxChildrenParents.set(props.children, type);
}
}
- if (typeof type === 'function') {
- if (isClientReference(type) || isOpaqueTemporaryReference(type)) {
- // This is a reference to a Client Component.
- return renderClientElement(
- task,
- type,
- key,
- props,
- owner,
- stack,
- validated,
- );
- }
+ if (
+ typeof type === 'function' &&
+ !isClientReference(type) &&
+ !isOpaqueTemporaryReference(type)
+ ) {
// This is a Server Component.
return renderFunctionComponent(
request,
@@ -1525,43 +1516,27 @@ function renderElement(
stack,
validated,
);
- } else if (typeof type === 'string') {
- // This is a host element. E.g. HTML.
- return renderClientElement(task, type, key, props, owner, stack, validated);
- } else if (typeof type === 'symbol') {
- if (type === REACT_FRAGMENT_TYPE && key === null) {
- // For key-less fragments, we add a small optimization to avoid serializing
- // it as a wrapper.
- const prevImplicitSlot = task.implicitSlot;
- if (task.keyPath === null) {
- task.implicitSlot = true;
- }
- const json = renderModelDestructive(
- request,
- task,
- emptyRoot,
- '',
- props.children,
- );
- task.implicitSlot = prevImplicitSlot;
- return json;
- }
- // This might be a built-in React component. We'll let the client decide.
- // Any built-in works as long as its props are serializable.
- return renderClientElement(task, type, key, props, owner, stack, validated);
- } else if (type != null && typeof type === 'object') {
- if (isClientReference(type)) {
- // This is a reference to a Client Component.
- return renderClientElement(
- task,
- type,
- key,
- props,
- owner,
- stack,
- validated,
- );
- }
+ } else if (type === REACT_FRAGMENT_TYPE && key === null) {
+ // For key-less fragments, we add a small optimization to avoid serializing
+ // it as a wrapper.
+ const prevImplicitSlot = task.implicitSlot;
+ if (task.keyPath === null) {
+ task.implicitSlot = true;
+ }
+ const json = renderModelDestructive(
+ request,
+ task,
+ emptyRoot,
+ '',
+ props.children,
+ );
+ task.implicitSlot = prevImplicitSlot;
+ return json;
+ } else if (
+ type != null &&
+ typeof type === 'object' &&
+ !isClientReference(type)
+ ) {
switch (type.$$typeof) {
case REACT_LAZY_TYPE: {
let wrappedType;
@@ -1615,11 +1590,21 @@ function renderElement(
validated,
);
}
+ case REACT_ELEMENT_TYPE: {
+ // This is invalid but we'll let the client determine that it is.
+ if (__DEV__) {
+ // Disable the key warning that would happen otherwise because this
+ // element gets serialized inside an array. We'll error later anyway.
+ type._store.validated = 1;
+ }
+ }
}
}
- throw new Error(
- `Unsupported Server Component type: ${describeValueForErrorMessage(type)}`,
- );
+ // For anything else, try it on the client instead.
+ // We don't know if the client will support it or not. This might error on the
+ // client or error during serialization but the stack will point back to the
+ // server.
+ return renderClientElement(task, type, key, props, owner, stack, validated);
}
function pingTask(request: Request, task: Task): void {
diff --git a/packages/react/src/__tests__/ReactElementValidator-test.internal.js b/packages/react/src/__tests__/ReactElementValidator-test.internal.js
index 00c92eabe703f..4383a6472d350 100644
--- a/packages/react/src/__tests__/ReactElementValidator-test.internal.js
+++ b/packages/react/src/__tests__/ReactElementValidator-test.internal.js
@@ -515,11 +515,15 @@ describe('ReactElementValidator', () => {
expect(() => {
void ({[]});
}).toErrorDev(
- 'React.jsx: type is invalid -- expected a string ' +
- '(for built-in components) or a class/function (for composite ' +
- 'components) but got: undefined. You likely forgot to export your ' +
- "component from the file it's defined in, or you might have mixed up " +
- 'default and named imports.',
+ gate(flags => flags.enableOwnerStacks)
+ ? []
+ : [
+ 'React.jsx: type is invalid -- expected a string ' +
+ '(for built-in components) or a class/function (for composite ' +
+ 'components) but got: undefined. You likely forgot to export your ' +
+ "component from the file it's defined in, or you might have mixed up " +
+ 'default and named imports.',
+ ],
{withoutStack: true},
);
});
diff --git a/packages/react/src/__tests__/ReactJSXElementValidator-test.js b/packages/react/src/__tests__/ReactJSXElementValidator-test.js
index 4fc666aa4e7fd..f827a52bc59ca 100644
--- a/packages/react/src/__tests__/ReactJSXElementValidator-test.js
+++ b/packages/react/src/__tests__/ReactJSXElementValidator-test.js
@@ -215,35 +215,6 @@ describe('ReactJSXElementValidator', () => {
);
});
- it('gives a helpful error when passing null, undefined, or boolean', () => {
- const Undefined = undefined;
- const Null = null;
- const True = true;
- const Div = 'div';
- expect(() => void ()).toErrorDev(
- 'React.jsx: type is invalid -- expected a string ' +
- '(for built-in components) or a class/function (for composite ' +
- 'components) but got: undefined. You likely forgot to export your ' +
- "component from the file it's defined in, or you might have mixed up " +
- 'default and named imports.',
- {withoutStack: true},
- );
- expect(() => void ()).toErrorDev(
- 'React.jsx: type is invalid -- expected a string ' +
- '(for built-in components) or a class/function (for composite ' +
- 'components) but got: null.',
- {withoutStack: true},
- );
- expect(() => void ()).toErrorDev(
- 'React.jsx: type is invalid -- expected a string ' +
- '(for built-in components) or a class/function (for composite ' +
- 'components) but got: boolean.',
- {withoutStack: true},
- );
- // No error expected
- void ();
- });
-
it('warns for fragments with illegal attributes', async () => {
class Foo extends React.Component {
render() {
diff --git a/packages/react/src/jsx/ReactJSXElement.js b/packages/react/src/jsx/ReactJSXElement.js
index 0f8b9f397df5b..1652b1e2edecc 100644
--- a/packages/react/src/jsx/ReactJSXElement.js
+++ b/packages/react/src/jsx/ReactJSXElement.js
@@ -559,9 +559,14 @@ function jsxDEVImpl(
debugTask,
) {
if (__DEV__) {
- if (!isValidElementType(type)) {
+ if (!enableOwnerStacks && !isValidElementType(type)) {
// This is an invalid element type.
//
+ // We warn here so that we can get better stack traces but with enableOwnerStacks
+ // enabled we don't need this because we get good stacks if we error in the
+ // renderer anyway. The renderer is the only one that knows what types are valid
+ // for this particular renderer so we let it error there instead.
+ //
// We warn in this case but don't throw. We expect the element creation to
// succeed and there will likely be errors in render.
let info = '';
@@ -604,6 +609,9 @@ function jsxDEVImpl(
// errors. We don't want exception behavior to differ between dev and
// prod. (Rendering will throw with a helpful message and as soon as the
// type is fixed, the key warnings will appear.)
+ // When enableOwnerStacks is on, we no longer need the type here so this
+ // comment is no longer true. Which is why we can run this even for invalid
+ // types.
const children = config.children;
if (children !== undefined) {
if (isStaticChildren) {
@@ -1103,6 +1111,17 @@ export function cloneElement(element, config, children) {
*/
function validateChildKeys(node, parentType) {
if (__DEV__) {
+ if (enableOwnerStacks) {
+ // When owner stacks is enabled no warnings happens. All we do is
+ // mark elements as being in a valid static child position so they
+ // don't need keys.
+ if (isValidElement(node)) {
+ if (node._store) {
+ node._store.validated = 1;
+ }
+ }
+ return;
+ }
if (typeof node !== 'object' || !node) {
return;
}
From b3aface19a17d78d190dd2883f6e4f9f719181d0 Mon Sep 17 00:00:00 2001
From: Sharath <29162020+tnfssc@users.noreply.github.com>
Date: Fri, 28 Jun 2024 13:56:28 +0530
Subject: [PATCH 33/38] [fix] replace polyfill.io with cloudflare equivalent
(#30123)
---
fixtures/dom/public/renderer.html | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/fixtures/dom/public/renderer.html b/fixtures/dom/public/renderer.html
index 3852dd952f2b9..83abba28c0311 100644
--- a/fixtures/dom/public/renderer.html
+++ b/fixtures/dom/public/renderer.html
@@ -80,7 +80,7 @@
-
+