From c70ab3f4b051348e3dd91144d9c7299a2e2311a5 Mon Sep 17 00:00:00 2001 From: lauren Date: Thu, 19 Dec 2024 13:03:11 -0500 Subject: [PATCH 0001/1160] [ci] getWorkflowRun should not throw early if workflow hasn't completed (#31861) We already have handling and retry logic for in-flight workflows in `downloadArtifactsFromGitHub`, so there's no need to exit early if we find a workflow for a given commit but it hasn't finished yet. --- .../release/shared-commands/download-build-artifacts.js | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/scripts/release/shared-commands/download-build-artifacts.js b/scripts/release/shared-commands/download-build-artifacts.js index 85cbb8b9fa176..cf7301688027a 100644 --- a/scripts/release/shared-commands/download-build-artifacts.js +++ b/scripts/release/shared-commands/download-build-artifacts.js @@ -43,12 +43,7 @@ async function getWorkflowRun(commit) { ); const json = JSON.parse(res.stdout); - const workflowRun = json.workflow_runs.find( - run => - run.head_sha === commit && - run.status === 'completed' && - run.conclusion === 'success' - ); + const workflowRun = json.workflow_runs.find(run => run.head_sha === commit); if (workflowRun == null || workflowRun.id == null) { console.log( From 36d15d58628baf5e15624a52febae873a7a56345 Mon Sep 17 00:00:00 2001 From: Ricky Date: Thu, 19 Dec 2024 13:05:23 -0500 Subject: [PATCH 0002/1160] [assert helpers] ReactChildren-test (#31844) Based off https://github.com/facebook/react/pull/31843 Commit to review: https://github.com/facebook/react/pull/31844/commits/2c653b81a73e155f1548c0362e5334629a45351e Moar tests --- .../react/src/__tests__/ReactChildren-test.js | 206 +++++++++++------- 1 file changed, 122 insertions(+), 84 deletions(-) diff --git a/packages/react/src/__tests__/ReactChildren-test.js b/packages/react/src/__tests__/ReactChildren-test.js index 2723755abfe4a..5cce6b24873f0 100644 --- a/packages/react/src/__tests__/ReactChildren-test.js +++ b/packages/react/src/__tests__/ReactChildren-test.js @@ -13,12 +13,13 @@ describe('ReactChildren', () => { let React; let ReactDOMClient; let act; + let assertConsoleErrorDev; beforeEach(() => { jest.resetModules(); React = require('react'); ReactDOMClient = require('react-dom/client'); - act = require('internal-test-utils').act; + ({act, assertConsoleErrorDev} = require('internal-test-utils')); }); it('should support identity for simple', () => { @@ -331,14 +332,16 @@ describe('ReactChildren', () => { callback.mockClear(); } - let instance; - expect(() => { - instance =
{threeDivIterable}
; - }).toErrorDev( + const instance =
{threeDivIterable}
; + assertConsoleErrorDev( // With the flag on this doesn't warn eagerly but only when rendered gate(flag => flag.enableOwnerStacks) ? [] - : ['Each child in a list should have a unique "key" prop.'], + : [ + 'Each child in a list should have a unique "key" prop.\n\n' + + 'Check the top-level render call using
. See https://react.dev/link/warning-keys for more information.\n' + + ' in div (at **)', + ], ); React.Children.forEach(instance.props.children, callback, context); @@ -359,11 +362,16 @@ describe('ReactChildren', () => { const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); - await expect(async () => { - await act(() => { - root.render(instance); - }); - }).toErrorDev('Each child in a list should have a unique "key" prop.'); + await act(() => { + root.render(instance); + }); + assertConsoleErrorDev([ + 'Each child in a list should have a unique "key" prop.\n\n' + + 'Check the top-level render call using
. It was passed a child from div.' + + ' See https://react.dev/link/warning-keys for more information.\n' + + ' in div (at **)' + + (gate(flag => flag.enableOwnerStacks) ? '' : '\n in div (at **)'), + ]); }); it('should be called for each child in an iterable with keys', () => { @@ -879,15 +887,29 @@ describe('ReactChildren', () => { const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); - await expect(async () => { - await act(() => { - root.render( - - {[
]} - , - ); - }); - }).toErrorDev(['Each child in a list should have a unique "key" prop.']); + await act(() => { + root.render( + + {[
]} + , + ); + }); + assertConsoleErrorDev( + gate(flags => flags.enableOwnerStacks) + ? [ + 'Each child in a list should have a unique "key" prop.\n\n' + + 'Check the render method of `ComponentRenderingMappedChildren`.' + + ' See https://react.dev/link/warning-keys for more information.\n' + + ' in div (at **)\n' + + ' in **/ReactChildren-test.js:**:** (at **)', + ] + : [ + 'Each child in a list should have a unique "key" prop.\n\n' + + 'Check the top-level render call using .' + + ' See https://react.dev/link/warning-keys for more information.\n' + + ' in div (at **)', + ], + ); }); it('does not warn for mapped static children without keys', async () => { @@ -903,16 +925,14 @@ describe('ReactChildren', () => { const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); - await expect(async () => { - await act(() => { - root.render( - -
-
- , - ); - }); - }).toErrorDev([]); + await act(() => { + root.render( + +
+
+ , + ); + }); }); it('warns for cloned list children without keys', async () => { @@ -926,15 +946,28 @@ describe('ReactChildren', () => { const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); - await expect(async () => { - await act(() => { - root.render( - - {[
]} - , - ); - }); - }).toErrorDev(['Each child in a list should have a unique "key" prop.']); + await act(() => { + root.render( + + {[
]} + , + ); + }); + assertConsoleErrorDev( + gate(flags => flags.enableOwnerStacks) + ? [ + 'Each child in a list should have a unique "key" prop.\n\n' + + 'Check the render method of `ComponentRenderingClonedChildren`.' + + ' See https://react.dev/link/warning-keys for more information.\n' + + ' in div (at **)', + ] + : [ + 'Each child in a list should have a unique "key" prop.\n\n' + + 'Check the top-level render call using .' + + ' See https://react.dev/link/warning-keys for more information.\n' + + ' in div (at **)', + ], + ); }); it('does not warn for cloned static children without keys', async () => { @@ -948,16 +981,14 @@ describe('ReactChildren', () => { const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); - await expect(async () => { - await act(() => { - root.render( - -
-
- , - ); - }); - }).toErrorDev([]); + await act(() => { + root.render( + +
+
+ , + ); + }); }); it('warns for flattened list children without keys', async () => { @@ -967,15 +998,28 @@ describe('ReactChildren', () => { const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); - await expect(async () => { - await act(() => { - root.render( - - {[
]} - , - ); - }); - }).toErrorDev(['Each child in a list should have a unique "key" prop.']); + await act(() => { + root.render( + + {[
]} + , + ); + }); + assertConsoleErrorDev( + gate(flags => flags.enableOwnerStacks) + ? [ + 'Each child in a list should have a unique "key" prop.\n\n' + + 'Check the render method of `ComponentRenderingFlattenedChildren`.' + + ' See https://react.dev/link/warning-keys for more information.\n' + + ' in div (at **)', + ] + : [ + 'Each child in a list should have a unique "key" prop.\n\n' + + 'Check the top-level render call using .' + + ' See https://react.dev/link/warning-keys for more information.\n' + + ' in div (at **)', + ], + ); }); it('does not warn for flattened static children without keys', async () => { @@ -985,16 +1029,14 @@ describe('ReactChildren', () => { const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); - await expect(async () => { - await act(() => { - root.render( - -
-
- , - ); - }); - }).toErrorDev([]); + await act(() => { + root.render( + +
+
+ , + ); + }); }); it('should escape keys', () => { @@ -1153,18 +1195,16 @@ describe('ReactChildren', () => { const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); - await expect(async () => { - await act(() => { - root.render(); - }); - }).toErrorDev( - '' + - 'Each child in a list should have a unique "key" prop.' + + await act(() => { + root.render(); + }); + assertConsoleErrorDev([ + 'Each child in a list should have a unique "key" prop.' + '\n\nCheck the top-level render call using . It was passed a child from ComponentReturningArray. ' + 'See https://react.dev/link/warning-keys for more information.' + '\n in div (at **)' + '\n in ComponentReturningArray (at **)', - ); + ]); }); it('does not warn when there are keys on elements in a fragment', async () => { @@ -1184,17 +1224,15 @@ describe('ReactChildren', () => { it('warns for keys for arrays at the top level', async () => { const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); - await expect(async () => { - await act(() => { - root.render([
,
]); - }); - }).toErrorDev( - '' + - 'Each child in a list should have a unique "key" prop.' + + await act(() => { + root.render([
,
]); + }); + assertConsoleErrorDev([ + 'Each child in a list should have a unique "key" prop.' + '\n\nCheck the top-level render call using . ' + 'See https://react.dev/link/warning-keys for more information.' + '\n in div (at **)', - ); + ]); }); }); }); From 518d06d26a97df6d4f5b04e529e5018ad35ea936 Mon Sep 17 00:00:00 2001 From: "Sebastian \"Sebbie\" Silbermann" Date: Thu, 19 Dec 2024 20:43:01 +0100 Subject: [PATCH 0003/1160] Turn off `enableYieldingBeforePassive` (#31857) --- .../ReactSuspenseyCommitPhase-test.js | 42 +++++++++++++++++++ packages/shared/ReactFeatureFlags.js | 3 +- 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/packages/react-reconciler/src/__tests__/ReactSuspenseyCommitPhase-test.js b/packages/react-reconciler/src/__tests__/ReactSuspenseyCommitPhase-test.js index 4dbba1bca21f0..2b5707b8c554d 100644 --- a/packages/react-reconciler/src/__tests__/ReactSuspenseyCommitPhase-test.js +++ b/packages/react-reconciler/src/__tests__/ReactSuspenseyCommitPhase-test.js @@ -491,4 +491,46 @@ describe('ReactSuspenseyCommitPhase', () => { , ); }); + + // FIXME: Should pass with `enableYieldingBeforePassive` + // @gate !enableYieldingBeforePassive + it('runs passive effects after suspended commit resolves', async () => { + function Effect() { + React.useEffect(() => { + Scheduler.log('flush effect'); + }); + return ; + } + + const root = ReactNoop.createRoot(); + + await act(() => { + root.render( + }> + + + , + ); + }); + + assertLog([ + 'render effect', + 'Image requested [A]', + 'Loading...', + 'render effect', + ]); + expect(root).toMatchRenderedOutput('Loading...'); + + await act(() => { + resolveSuspenseyThing('A'); + }); + + assertLog(['flush effect']); + expect(root).toMatchRenderedOutput( + <> + {'render effect'} + + , + ); + }); }); diff --git a/packages/shared/ReactFeatureFlags.js b/packages/shared/ReactFeatureFlags.js index 3046f8f4cb45e..0720ab2a88b78 100644 --- a/packages/shared/ReactFeatureFlags.js +++ b/packages/shared/ReactFeatureFlags.js @@ -78,7 +78,8 @@ export const enableLegacyFBSupport = false; // ----------------------------------------------------------------------------- // Yield to the browser event loop and not just the scheduler event loop before passive effects. -export const enableYieldingBeforePassive = __EXPERIMENTAL__; +// Fix gated tests that fail with this flag enabled before turning it back on. +export const enableYieldingBeforePassive = false; export const enableLegacyCache = __EXPERIMENTAL__; From de82912e620518d501680bbd93fbb5cc8d134223 Mon Sep 17 00:00:00 2001 From: Jack Pope Date: Fri, 20 Dec 2024 09:48:50 -0500 Subject: [PATCH 0004/1160] Turn off enableYieldingBeforePassive in internal test renderers (#31863) https://github.com/facebook/react/pull/31785 turned on `enableYieldingBeforePassive` for the internal test renderer builds. We have some failing tests on the RN side blocking the sync so lets turn these off for now. --- .../shared/forks/ReactFeatureFlags.test-renderer.native-fb.js | 2 +- packages/shared/forks/ReactFeatureFlags.test-renderer.www.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js index 8d38112b16879..81060cfafb1b1 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js @@ -68,7 +68,7 @@ export const enableFabricCompleteRootInCommitPhase = false; export const enableSiblingPrerendering = true; export const enableUseResourceEffectHook = true; export const enableHydrationLaneScheduling = true; -export const enableYieldingBeforePassive = true; +export const enableYieldingBeforePassive = false; // Flow magic to verify the exports of this file match the original version. ((((null: any): ExportsType): FeatureFlagsType): ExportsType); diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js index 28a303a034cac..e0e9906d52b8f 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js @@ -82,7 +82,7 @@ export const enableUseResourceEffectHook = false; export const enableHydrationLaneScheduling = true; -export const enableYieldingBeforePassive = true; +export const enableYieldingBeforePassive = false; // Flow magic to verify the exports of this file match the original version. ((((null: any): ExportsType): FeatureFlagsType): ExportsType); From 6a3d6a4382cdafc1260483a6fc5f76593fc038e4 Mon Sep 17 00:00:00 2001 From: Joseph Savona <6425824+josephsavona@users.noreply.github.com> Date: Fri, 20 Dec 2024 08:56:48 -0800 Subject: [PATCH 0005/1160] [compiler] Allow type cast expressions with refs (#31871) We report a false positive for the combination of a ref-accessing function placed inside an array which is they type-cast. Here we teach ref validation about type casts. I also tried other variants like `return ref as const` but those already worked. Closes #31864 --- .../Validation/ValidateNoRefAccesInRender.ts | 8 +++ .../allow-ref-type-cast-in-render.expect.md | 60 +++++++++++++++++++ .../compiler/allow-ref-type-cast-in-render.js | 17 ++++++ 3 files changed, 85 insertions(+) create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-type-cast-in-render.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-type-cast-in-render.js diff --git a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoRefAccesInRender.ts b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoRefAccesInRender.ts index b361b2016a1dd..4db8c700f387f 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoRefAccesInRender.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoRefAccesInRender.ts @@ -305,6 +305,14 @@ function validateNoRefAccessInRenderImpl( ); break; } + case 'TypeCastExpression': { + env.set( + instr.lvalue.identifier.id, + env.get(instr.value.value.identifier.id) ?? + refTypeOfType(instr.lvalue), + ); + break; + } case 'LoadContext': case 'LoadLocal': { env.set( diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-type-cast-in-render.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-type-cast-in-render.expect.md new file mode 100644 index 0000000000000..56e3039f63cec --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-type-cast-in-render.expect.md @@ -0,0 +1,60 @@ + +## Input + +```javascript +import {useRef} from 'react'; + +function useArrayOfRef() { + const ref = useRef(null); + const callback = value => { + ref.current = value; + }; + return [callback] as const; +} + +export const FIXTURE_ENTRYPOINT = { + fn: () => { + useArrayOfRef(); + return 'ok'; + }, + params: [{}], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; +import { useRef } from "react"; + +function useArrayOfRef() { + const $ = _c(1); + const ref = useRef(null); + let t0; + if ($[0] === Symbol.for("react.memo_cache_sentinel")) { + const callback = (value) => { + ref.current = value; + }; + + t0 = [callback]; + $[0] = t0; + } else { + t0 = $[0]; + } + return t0 as const; +} + +export const FIXTURE_ENTRYPOINT = { + fn: () => { + useArrayOfRef(); + return "ok"; + }, + + params: [{}], +}; + +``` + +### Eval output +(kind: ok) "ok" \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-type-cast-in-render.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-type-cast-in-render.js new file mode 100644 index 0000000000000..2d0aafeffda4c --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-type-cast-in-render.js @@ -0,0 +1,17 @@ +import {useRef} from 'react'; + +function useArrayOfRef() { + const ref = useRef(null); + const callback = value => { + ref.current = value; + }; + return [callback] as const; +} + +export const FIXTURE_ENTRYPOINT = { + fn: () => { + useArrayOfRef(); + return 'ok'; + }, + params: [{}], +}; From 26297f5383f7e7150d9aa2cf12e8326c96991cab Mon Sep 17 00:00:00 2001 From: Ricky Date: Fri, 20 Dec 2024 12:41:13 -0500 Subject: [PATCH 0006/1160] [assert helpers] not dom or reconciler (#31862) converts everything left outside react-dom and react-reconciler --- .../__tests__/ReactCacheOld-test.internal.js | 35 +++++--- .../ReactHooksInspectionIntegration-test.js | 13 +-- .../__tests__/trustedTypes-test.internal.js | 14 +-- .../__tests__/ReactFabric-test.internal.js | 86 ++++++++++--------- .../ReactNativeEvents-test.internal.js | 50 ++++++----- .../ReactNativeMount-test.internal.js | 33 +++---- .../__tests__/ReactFlightDOMBrowser-test.js | 52 +++++++---- .../src/__tests__/ReactFlightDOMEdge-test.js | 21 +++-- .../src/__tests__/ReactFlightDOMForm-test.js | 13 ++- .../src/__tests__/ReactTestRenderer-test.js | 23 +++-- 10 files changed, 195 insertions(+), 145 deletions(-) diff --git a/packages/react-cache/src/__tests__/ReactCacheOld-test.internal.js b/packages/react-cache/src/__tests__/ReactCacheOld-test.internal.js index 0e9cb549f653a..554e6e4bfb29a 100644 --- a/packages/react-cache/src/__tests__/ReactCacheOld-test.internal.js +++ b/packages/react-cache/src/__tests__/ReactCacheOld-test.internal.js @@ -22,6 +22,7 @@ let waitForPaint; let assertLog; let waitForThrow; let act; +let assertConsoleErrorDev; describe('ReactCache', () => { beforeEach(() => { @@ -39,6 +40,7 @@ describe('ReactCache', () => { assertLog = InternalTestUtils.assertLog; waitForThrow = InternalTestUtils.waitForThrow; waitForPaint = InternalTestUtils.waitForPaint; + assertConsoleErrorDev = InternalTestUtils.assertConsoleErrorDev; act = InternalTestUtils.act; TextResource = createResource( @@ -190,20 +192,31 @@ describe('ReactCache', () => { ); if (__DEV__) { - await expect(async () => { - await waitForAll([ - 'App', - 'Loading...', - - ...(gate('enableSiblingPrerendering') ? ['App'] : []), - ]); - }).toErrorDev([ + await waitForAll([ + 'App', + 'Loading...', + + ...(gate('enableSiblingPrerendering') ? ['App'] : []), + ]); + assertConsoleErrorDev([ 'Invalid key type. Expected a string, number, symbol, or ' + "boolean, but instead received: [ 'Hi', 100 ]\n\n" + 'To use non-primitive values as keys, you must pass a hash ' + - 'function as the second argument to createResource().', - - ...(gate('enableSiblingPrerendering') ? ['Invalid key type'] : []), + 'function as the second argument to createResource().\n' + + ' in App (at **)' + + (gate(flags => flags.enableOwnerStacks) + ? '' + : '\n in Suspense (at **)'), + + ...(gate('enableSiblingPrerendering') + ? [ + 'Invalid key type. Expected a string, number, symbol, or ' + + "boolean, but instead received: [ 'Hi', 100 ]\n\n" + + 'To use non-primitive values as keys, you must pass a hash ' + + 'function as the second argument to createResource().\n' + + ' in App (at **)', + ] + : []), ]); } else { await waitForAll([ diff --git a/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js b/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js index ff8e7e1ac8683..87f98b99f2534 100644 --- a/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js +++ b/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js @@ -14,6 +14,7 @@ let React; let ReactTestRenderer; let ReactDebugTools; let act; +let assertConsoleErrorDev; let useMemoCache; function normalizeSourceLoc(tree) { @@ -33,7 +34,7 @@ describe('ReactHooksInspectionIntegration', () => { jest.resetModules(); React = require('react'); ReactTestRenderer = require('react-test-renderer'); - act = require('internal-test-utils').act; + ({act, assertConsoleErrorDev} = require('internal-test-utils')); ReactDebugTools = require('react-debug-tools'); useMemoCache = require('react/compiler-runtime').c; }); @@ -2344,10 +2345,12 @@ describe('ReactHooksInspectionIntegration', () => { , ); - await expect(async () => { - await act(async () => await LazyFoo); - }).toErrorDev([ - 'Foo: Support for defaultProps will be removed from function components in a future major release. Use JavaScript default parameters instead.', + await act(async () => await LazyFoo); + assertConsoleErrorDev([ + 'Foo: Support for defaultProps will be removed from function components in a future major release. Use JavaScript default parameters instead.' + + (gate(flags => flags.enableOwnerStacks) + ? '' + : '\n in Foo (at **)\n' + ' in Suspense (at **)'), ]); const childFiber = renderer.root._currentFiber(); diff --git a/packages/react-dom/src/client/__tests__/trustedTypes-test.internal.js b/packages/react-dom/src/client/__tests__/trustedTypes-test.internal.js index 923ee1f5d81a6..5a43a9ec2f06c 100644 --- a/packages/react-dom/src/client/__tests__/trustedTypes-test.internal.js +++ b/packages/react-dom/src/client/__tests__/trustedTypes-test.internal.js @@ -14,6 +14,7 @@ describe('when Trusted Types are available in global object', () => { let ReactDOMClient; let ReactFeatureFlags; let act; + let assertConsoleErrorDev; let container; let ttObject1; let ttObject2; @@ -36,7 +37,7 @@ describe('when Trusted Types are available in global object', () => { ReactFeatureFlags.enableTrustedTypesIntegration = true; React = require('react'); ReactDOMClient = require('react-dom/client'); - act = require('internal-test-utils').act; + ({act, assertConsoleErrorDev} = require('internal-test-utils')); ttObject1 = { toString() { return 'Hi'; @@ -208,17 +209,16 @@ describe('when Trusted Types are available in global object', () => { it('should warn once when rendering script tag in jsx on client', async () => { const root = ReactDOMClient.createRoot(container); - await expect(async () => { - await act(() => { - root.render(); - }); - }).toErrorDev( + await act(() => { + root.render(); + }); + assertConsoleErrorDev([ 'Encountered a script tag while rendering React component. ' + 'Scripts inside React components are never executed when rendering ' + 'on the client. Consider using template tag instead ' + '(https://developer.mozilla.org/en-US/docs/Web/HTML/Element/template).\n' + ' in script (at **)', - ); + ]); // check that the warning is printed only once await act(() => { 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 03f0cd0a6c0f3..05116be30110f 100644 --- a/packages/react-native-renderer/src/__tests__/ReactFabric-test.internal.js +++ b/packages/react-native-renderer/src/__tests__/ReactFabric-test.internal.js @@ -16,6 +16,7 @@ let ReactNativePrivateInterface; let createReactNativeComponentClass; let StrictMode; let act; +let assertConsoleErrorDev; const DISPATCH_COMMAND_REQUIRES_HOST_COMPONENT = "dispatchCommand was called with a ref that isn't a " + @@ -38,7 +39,7 @@ describe('ReactFabric', () => { createReactNativeComponentClass = require('react-native/Libraries/ReactPrivate/ReactNativePrivateInterface') .ReactNativeViewConfigRegistry.register; - act = require('internal-test-utils').act; + ({act, assertConsoleErrorDev} = require('internal-test-utils')); }); it('should be able to create and render a native component', async () => { @@ -459,9 +460,8 @@ describe('ReactFabric', () => { }); expect(nativeFabricUIManager.dispatchCommand).not.toBeCalled(); - expect(() => { - ReactFabric.dispatchCommand(viewRef, 'updateCommand', [10, 20]); - }).toErrorDev([DISPATCH_COMMAND_REQUIRES_HOST_COMPONENT], { + ReactFabric.dispatchCommand(viewRef, 'updateCommand', [10, 20]); + assertConsoleErrorDev([DISPATCH_COMMAND_REQUIRES_HOST_COMPONENT], { withoutStack: true, }); @@ -525,9 +525,8 @@ describe('ReactFabric', () => { }); expect(nativeFabricUIManager.sendAccessibilityEvent).not.toBeCalled(); - expect(() => { - ReactFabric.sendAccessibilityEvent(viewRef, 'eventTypeName'); - }).toErrorDev([SEND_ACCESSIBILITY_EVENT_REQUIRES_HOST_COMPONENT], { + ReactFabric.sendAccessibilityEvent(viewRef, 'eventTypeName'); + assertConsoleErrorDev([SEND_ACCESSIBILITY_EVENT_REQUIRES_HOST_COMPONENT], { withoutStack: true, }); @@ -856,24 +855,31 @@ describe('ReactFabric', () => { uiViewClassName: 'RCTView', })); - await expect(async () => { - await act(() => { - ReactFabric.render(this should warn, 11, null, true); - }); - }).toErrorDev(['Text strings must be rendered within a component.']); + await act(() => { + ReactFabric.render(this should warn, 11, null, true); + }); + assertConsoleErrorDev([ + 'Text strings must be rendered within a component.\n' + + ' in RCTView (at **)', + ]); - await expect(async () => { - await act(() => { - ReactFabric.render( - - hi hello hi - , - 11, - null, - true, - ); - }); - }).toErrorDev(['Text strings must be rendered within a component.']); + await act(() => { + ReactFabric.render( + + hi hello hi + , + 11, + null, + true, + ); + }); + assertConsoleErrorDev([ + 'Text strings must be rendered within a component.\n' + + ' in RCTScrollView (at **)' + + (gate(flags => !flags.enableOwnerStacks) + ? '\n in RCTText (at **)' + : ''), + ]); }); it('should not throw for text inside of an indirect ancestor', async () => { @@ -1166,10 +1172,8 @@ describe('ReactFabric', () => { ); }); - let match; - expect( - () => (match = ReactFabric.findHostInstance_DEPRECATED(parent)), - ).toErrorDev([ + const match = ReactFabric.findHostInstance_DEPRECATED(parent); + assertConsoleErrorDev([ 'findHostInstance_DEPRECATED is deprecated in StrictMode. ' + 'findHostInstance_DEPRECATED was passed an instance of ContainsStrictModeChild which renders StrictMode children. ' + 'Instead, add a ref directly to the element you want to reference. ' + @@ -1207,10 +1211,8 @@ describe('ReactFabric', () => { ); }); - let match; - expect( - () => (match = ReactFabric.findHostInstance_DEPRECATED(parent)), - ).toErrorDev([ + const match = ReactFabric.findHostInstance_DEPRECATED(parent); + assertConsoleErrorDev([ 'findHostInstance_DEPRECATED is deprecated in StrictMode. ' + 'findHostInstance_DEPRECATED was passed an instance of IsInStrictMode which is inside StrictMode. ' + 'Instead, add a ref directly to the element you want to reference. ' + @@ -1250,8 +1252,8 @@ describe('ReactFabric', () => { ); }); - let match; - expect(() => (match = ReactFabric.findNodeHandle(parent))).toErrorDev([ + const match = ReactFabric.findNodeHandle(parent); + assertConsoleErrorDev([ 'findNodeHandle is deprecated in StrictMode. ' + 'findNodeHandle was passed an instance of ContainsStrictModeChild which renders StrictMode children. ' + 'Instead, add a ref directly to the element you want to reference. ' + @@ -1291,8 +1293,8 @@ describe('ReactFabric', () => { ); }); - let match; - expect(() => (match = ReactFabric.findNodeHandle(parent))).toErrorDev([ + const match = ReactFabric.findNodeHandle(parent); + assertConsoleErrorDev([ 'findNodeHandle is deprecated in StrictMode. ' + 'findNodeHandle was passed an instance of IsInStrictMode which is inside StrictMode. ' + 'Instead, add a ref directly to the element you want to reference. ' + @@ -1313,16 +1315,16 @@ describe('ReactFabric', () => { return null; } } - await expect(async () => { - await act(() => { - ReactFabric.render(, 11, null, true); - }); - }).toErrorDev([ + await act(() => { + ReactFabric.render(, 11, null, true); + }); + assertConsoleErrorDev([ 'TestComponent is accessing findNodeHandle inside its render(). ' + 'render() should be a pure function of props and state. It should ' + 'never access something that requires stale data from the previous ' + 'render, such as refs. Move this logic to componentDidMount and ' + - 'componentDidUpdate instead.', + 'componentDidUpdate instead.\n' + + ' in TestComponent (at **)', ]); }); diff --git a/packages/react-native-renderer/src/__tests__/ReactNativeEvents-test.internal.js b/packages/react-native-renderer/src/__tests__/ReactNativeEvents-test.internal.js index 46b2ad9cf1fc7..f4ab24d71d429 100644 --- a/packages/react-native-renderer/src/__tests__/ReactNativeEvents-test.internal.js +++ b/packages/react-native-renderer/src/__tests__/ReactNativeEvents-test.internal.js @@ -18,6 +18,7 @@ let ReactNative; let ResponderEventPlugin; let UIManager; let createReactNativeComponentClass; +let assertConsoleErrorDev; // Parallels requireNativeComponent() in that it lazily constructs a view config, // And registers view manager event types with ReactNativeViewConfigRegistry. @@ -69,6 +70,7 @@ beforeEach(() => { require('react-native/Libraries/ReactPrivate/ReactNativePrivateInterface').RCTEventEmitter; React = require('react'); act = require('internal-test-utils').act; + assertConsoleErrorDev = require('internal-test-utils').assertConsoleErrorDev; ReactNative = require('react-native-renderer'); ResponderEventPlugin = require('react-native-renderer/src/legacy-events/ResponderEventPlugin').default; @@ -227,30 +229,32 @@ test('handles events on text nodes', () => { } const log = []; - expect(() => { - ReactNative.render( - - - log.push('string touchend')} - onTouchEndCapture={() => log.push('string touchend capture')} - onTouchStart={() => log.push('string touchstart')} - onTouchStartCapture={() => log.push('string touchstart capture')}> - Text Content - - log.push('number touchend')} - onTouchEndCapture={() => log.push('number touchend capture')} - onTouchStart={() => log.push('number touchstart')} - onTouchStartCapture={() => log.push('number touchstart capture')}> - {123} - + ReactNative.render( + + + log.push('string touchend')} + onTouchEndCapture={() => log.push('string touchend capture')} + onTouchStart={() => log.push('string touchstart')} + onTouchStartCapture={() => log.push('string touchstart capture')}> + Text Content - , - 1, - ); - }).toErrorDev([ - 'ContextHack uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.', + log.push('number touchend')} + onTouchEndCapture={() => log.push('number touchend capture')} + onTouchStart={() => log.push('number touchstart')} + onTouchStartCapture={() => log.push('number touchstart capture')}> + {123} + + + , + 1, + ); + assertConsoleErrorDev([ + 'ContextHack uses the legacy childContextTypes API which will soon be removed. ' + + 'Use React.createContext() instead. ' + + '(https://react.dev/link/legacy-context)' + + '\n in ContextHack (at **)', ]); expect(UIManager.createView).toHaveBeenCalledTimes(5); diff --git a/packages/react-native-renderer/src/__tests__/ReactNativeMount-test.internal.js b/packages/react-native-renderer/src/__tests__/ReactNativeMount-test.internal.js index 790078b224b83..0aa1c3f0ba0af 100644 --- a/packages/react-native-renderer/src/__tests__/ReactNativeMount-test.internal.js +++ b/packages/react-native-renderer/src/__tests__/ReactNativeMount-test.internal.js @@ -18,6 +18,7 @@ let UIManager; let TextInputState; let ReactNativePrivateInterface; let act; +let assertConsoleErrorDev; const DISPATCH_COMMAND_REQUIRES_HOST_COMPONENT = "dispatchCommand was called with a ref that isn't a " + @@ -32,7 +33,7 @@ describe('ReactNative', () => { jest.resetModules(); React = require('react'); - act = require('internal-test-utils').act; + ({act, assertConsoleErrorDev} = require('internal-test-utils')); StrictMode = React.StrictMode; ReactNative = require('react-native-renderer'); ReactNativePrivateInterface = require('react-native/Libraries/ReactPrivate/ReactNativePrivateInterface'); @@ -158,9 +159,8 @@ describe('ReactNative', () => { ); expect(UIManager.dispatchViewManagerCommand).not.toBeCalled(); - expect(() => { - ReactNative.dispatchCommand(viewRef, 'updateCommand', [10, 20]); - }).toErrorDev([DISPATCH_COMMAND_REQUIRES_HOST_COMPONENT], { + ReactNative.dispatchCommand(viewRef, 'updateCommand', [10, 20]); + assertConsoleErrorDev([DISPATCH_COMMAND_REQUIRES_HOST_COMPONENT], { withoutStack: true, }); @@ -219,9 +219,8 @@ describe('ReactNative', () => { ); expect(UIManager.sendAccessibilityEvent).not.toBeCalled(); - expect(() => { - ReactNative.sendAccessibilityEvent(viewRef, 'updateCommand', [10, 20]); - }).toErrorDev([SEND_ACCESSIBILITY_EVENT_REQUIRES_HOST_COMPONENT], { + ReactNative.sendAccessibilityEvent(viewRef, 'updateCommand', [10, 20]); + assertConsoleErrorDev([SEND_ACCESSIBILITY_EVENT_REQUIRES_HOST_COMPONENT], { withoutStack: true, }); @@ -614,10 +613,8 @@ describe('ReactNative', () => { ReactNative.render( (parent = n)} />, 11); - let match; - expect( - () => (match = ReactNative.findHostInstance_DEPRECATED(parent)), - ).toErrorDev([ + const match = ReactNative.findHostInstance_DEPRECATED(parent); + assertConsoleErrorDev([ 'findHostInstance_DEPRECATED is deprecated in StrictMode. ' + 'findHostInstance_DEPRECATED was passed an instance of ContainsStrictModeChild which renders StrictMode children. ' + 'Instead, add a ref directly to the element you want to reference. ' + @@ -652,10 +649,8 @@ describe('ReactNative', () => { 11, ); - let match; - expect( - () => (match = ReactNative.findHostInstance_DEPRECATED(parent)), - ).toErrorDev([ + const match = ReactNative.findHostInstance_DEPRECATED(parent); + assertConsoleErrorDev([ 'findHostInstance_DEPRECATED is deprecated in StrictMode. ' + 'findHostInstance_DEPRECATED was passed an instance of IsInStrictMode which is inside StrictMode. ' + 'Instead, add a ref directly to the element you want to reference. ' + @@ -689,8 +684,8 @@ describe('ReactNative', () => { ReactNative.render( (parent = n)} />, 11); - let match; - expect(() => (match = ReactNative.findNodeHandle(parent))).toErrorDev([ + const match = ReactNative.findNodeHandle(parent); + assertConsoleErrorDev([ 'findNodeHandle is deprecated in StrictMode. ' + 'findNodeHandle was passed an instance of ContainsStrictModeChild which renders StrictMode children. ' + 'Instead, add a ref directly to the element you want to reference. ' + @@ -725,8 +720,8 @@ describe('ReactNative', () => { 11, ); - let match; - expect(() => (match = ReactNative.findNodeHandle(parent))).toErrorDev([ + const match = ReactNative.findNodeHandle(parent); + assertConsoleErrorDev([ 'findNodeHandle is deprecated in StrictMode. ' + 'findNodeHandle was passed an instance of IsInStrictMode which is inside StrictMode. ' + 'Instead, add a ref directly to the element you want to reference. ' + diff --git a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js index 3eccca1a9ba70..040bb046b5518 100644 --- a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js +++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js @@ -38,6 +38,7 @@ let ReactServerDOM; let Scheduler; let ReactServerScheduler; let reactServerAct; +let assertConsoleErrorDev; describe('ReactFlightDOMBrowser', () => { beforeEach(() => { @@ -75,7 +76,7 @@ describe('ReactFlightDOMBrowser', () => { Scheduler = require('scheduler'); patchMessageChannel(Scheduler); - act = require('internal-test-utils').act; + ({act, assertConsoleErrorDev} = require('internal-test-utils')); React = require('react'); ReactDOM = require('react-dom'); ReactDOMClient = require('react-dom/client'); @@ -1156,25 +1157,38 @@ describe('ReactFlightDOMBrowser', () => { const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); - await expect(async () => { - const stream = await serverAct(() => - ReactServerDOMServer.renderToReadableStream( - <> - {Array(6).fill(
no key
)}
- - {Array(6).fill(
no key
)} -
- , - webpackMap, - ), - ); - const result = - await ReactServerDOMClient.createFromReadableStream(stream); + const stream = await serverAct(() => + ReactServerDOMServer.renderToReadableStream( + <> + {Array(6).fill(
no key
)}
+ + {Array(6).fill(
no key
)} +
+ , + webpackMap, + ), + ); + const result = await ReactServerDOMClient.createFromReadableStream(stream); - await act(() => { - root.render(result); - }); - }).toErrorDev('Each child in a list should have a unique "key" prop.'); + if (!gate(flags => flags.enableOwnerStacks)) { + assertConsoleErrorDev([ + 'Each child in a list should have a unique "key" prop. ' + + 'See https://react.dev/link/warning-keys for more information.\n' + + ' in div (at **)', + ]); + } + + await act(() => { + root.render(result); + }); + if (gate(flags => flags.enableOwnerStacks)) { + assertConsoleErrorDev([ + 'Each child in a list should have a unique "key" prop.\n\n' + + 'Check the top-level render call using . ' + + 'See https://react.dev/link/warning-keys for more information.\n' + + ' in div (at **)', + ]); + } }); it('basic use(promise)', async () => { diff --git a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMEdge-test.js b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMEdge-test.js index f2814f250a2df..603dbbf09e5ca 100644 --- a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMEdge-test.js +++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMEdge-test.js @@ -37,6 +37,7 @@ let ReactServerDOMStaticServer; let ReactServerDOMClient; let use; let reactServerAct; +let assertConsoleErrorDev; function normalizeCodeLocInfo(str) { return ( @@ -66,6 +67,8 @@ describe('ReactFlightDOMEdge', () => { jest.resetModules(); reactServerAct = require('internal-test-utils').serverAct; + assertConsoleErrorDev = + require('internal-test-utils').assertConsoleErrorDev; // Simulate the condition resolution jest.mock('react', () => require('react/react.react-server')); @@ -802,17 +805,19 @@ describe('ReactFlightDOMEdge', () => { ), }; - expect(() => { - ServerModule.greet.bind({}, 'hi'); - }).toErrorDev( - 'Cannot bind "this" of a Server Action. Pass null or undefined as the first argument to .bind().', + ServerModule.greet.bind({}, 'hi'); + assertConsoleErrorDev( + [ + 'Cannot bind "this" of a Server Action. Pass null or undefined as the first argument to .bind().', + ], {withoutStack: true}, ); - expect(() => { - ServerModuleImportedOnClient.greet.bind({}, 'hi'); - }).toErrorDev( - 'Cannot bind "this" of a Server Action. Pass null or undefined as the first argument to .bind().', + ServerModuleImportedOnClient.greet.bind({}, 'hi'); + assertConsoleErrorDev( + [ + 'Cannot bind "this" of a Server Action. Pass null or undefined as the first argument to .bind().', + ], {withoutStack: true}, ); }); diff --git a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMForm-test.js b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMForm-test.js index 0b4549d5bac47..bb7c2c955bcb4 100644 --- a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMForm-test.js +++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMForm-test.js @@ -50,6 +50,7 @@ let ReactServerDOMClient; let ReactDOMClient; let useActionState; let act; +let assertConsoleErrorDev; describe('ReactFlightDOMForm', () => { beforeEach(() => { @@ -72,6 +73,8 @@ describe('ReactFlightDOMForm', () => { ReactDOMServer = require('react-dom/server.edge'); ReactDOMClient = require('react-dom/client'); act = React.act; + assertConsoleErrorDev = + require('internal-test-utils').assertConsoleErrorDev; // TODO: Test the old api but it warns so needs warnings to be asserted. // if (__VARIANT__) { @@ -959,12 +962,13 @@ describe('ReactFlightDOMForm', () => { await readIntoContainer(postbackSsrStream); } - await expect(submitTheForm).toErrorDev( + await submitTheForm(); + assertConsoleErrorDev([ 'Failed to serialize an action for progressive enhancement:\n' + 'Error: React Element cannot be passed to Server Functions from the Client without a temporary reference set. Pass a TemporaryReferenceSet to the options.\n' + ' [
]\n' + ' ^^^^^^', - ); + ]); // The error message was returned as JSX. const form2 = container.getElementsByTagName('form')[0]; @@ -1035,10 +1039,11 @@ describe('ReactFlightDOMForm', () => { await readIntoContainer(postbackSsrStream); } - await expect(submitTheForm).toErrorDev( + await submitTheForm(); + assertConsoleErrorDev([ 'Failed to serialize an action for progressive enhancement:\n' + 'Error: File/Blob fields are not yet supported in progressive forms. Will fallback to client hydration.', - ); + ]); expect(blob instanceof Blob).toBe(true); expect(blob.size).toBe(2); diff --git a/packages/react-test-renderer/src/__tests__/ReactTestRenderer-test.js b/packages/react-test-renderer/src/__tests__/ReactTestRenderer-test.js index e3400b173a21d..0b08cde378c45 100644 --- a/packages/react-test-renderer/src/__tests__/ReactTestRenderer-test.js +++ b/packages/react-test-renderer/src/__tests__/ReactTestRenderer-test.js @@ -14,6 +14,7 @@ let React; let ReactCache; let ReactTestRenderer; let act; +let assertConsoleErrorDev; describe('ReactTestRenderer', () => { beforeEach(() => { @@ -27,19 +28,27 @@ describe('ReactTestRenderer', () => { ReactTestRenderer = require('react-test-renderer'); const InternalTestUtils = require('internal-test-utils'); act = InternalTestUtils.act; + assertConsoleErrorDev = InternalTestUtils.assertConsoleErrorDev; }); it('should warn if used to render a ReactDOM portal', async () => { const container = document.createElement('div'); let error; - await expect(async () => { - await act(() => { - ReactTestRenderer.create(ReactDOM.createPortal('foo', container)); - }).catch(e => (error = e)); - }).toErrorDev('An invalid container has been provided.', { - withoutStack: true, - }); + await act(() => { + ReactTestRenderer.create(ReactDOM.createPortal('foo', container)); + }).catch(e => (error = e)); + assertConsoleErrorDev( + [ + 'An invalid container has been provided. ' + + 'This may indicate that another renderer is being used in addition to the test renderer. ' + + '(For example, ReactDOM.createPortal inside of a ReactTestRenderer tree.) ' + + 'This is not supported.', + ], + { + withoutStack: true, + }, + ); // After the update throws, a subsequent render is scheduled to // unmount the whole tree. This update also causes an error, so React From 99471c02dd6631df1892bf76d932afd22fffa5e3 Mon Sep 17 00:00:00 2001 From: Ricky Date: Fri, 20 Dec 2024 12:41:30 -0500 Subject: [PATCH 0007/1160] [assert helpers] ReactFlight (#31860) --- .../src/__tests__/ReactFlight-test.js | 640 +++++++++++++----- 1 file changed, 474 insertions(+), 166 deletions(-) diff --git a/packages/react-client/src/__tests__/ReactFlight-test.js b/packages/react-client/src/__tests__/ReactFlight-test.js index b2489705394a1..980dbbf0e1247 100644 --- a/packages/react-client/src/__tests__/ReactFlight-test.js +++ b/packages/react-client/src/__tests__/ReactFlight-test.js @@ -1467,13 +1467,12 @@ describe('ReactFlight', () => { const transport = ReactNoopFlightServer.render(); - await expect(async () => { - await act(() => { - startTransition(() => { - ReactNoop.render(ReactNoopFlightClient.read(transport)); - }); + await act(() => { + startTransition(() => { + ReactNoop.render(ReactNoopFlightClient.read(transport)); }); - }).toErrorDev( + }); + assertConsoleErrorDev([ 'Each child in a list should have a unique "key" prop.\n' + '\n' + 'Check the render method of `Component`. See https://react.dev/link/warning-keys for more information.\n' + @@ -1483,7 +1482,7 @@ describe('ReactFlight', () => { ? '' : ' in Indirection (at **)\n') + ' in App (at **)', - ); + ]); }); it('should trigger the inner most error boundary inside a Client Component', async () => { @@ -1541,17 +1540,47 @@ describe('ReactFlight', () => { return 123; }, }; - expect(() => { - const transport = ReactNoopFlightServer.render(); - ReactNoopFlightClient.read(transport); - }).toErrorDev( - 'Only plain objects can be passed to Client Components from Server Components. ' + - 'Objects with toJSON methods are not supported. ' + - 'Convert it manually to a simple value before passing it to props.\n' + - ' \n' + - ' ^^^^^^^^^^^^^^^', - {withoutStack: true}, - ); + const transport = ReactNoopFlightServer.render(); + if (gate(flags => flags.enableOwnerStacks)) { + assertConsoleErrorDev( + [ + 'Only plain objects can be passed to Client Components from Server Components. ' + + 'Objects with toJSON methods are not supported. ' + + 'Convert it manually to a simple value before passing it to props.\n' + + ' \n' + + ' ^^^^^^^^^^^^^^^', + ], + {withoutStack: true}, + ); + } + + ReactNoopFlightClient.read(transport); + if (gate(flags => flags.enableOwnerStacks)) { + assertConsoleErrorDev([ + 'Only plain objects can be passed to Client Components from Server Components. ' + + 'Objects with toJSON methods are not supported. ' + + 'Convert it manually to a simple value before passing it to props.\n' + + ' \n' + + ' ^^^^^^^^^^^^^^^\n' + + ' at ()', + ]); + } else { + assertConsoleErrorDev( + [ + 'Only plain objects can be passed to Client Components from Server Components. ' + + 'Objects with toJSON methods are not supported. ' + + 'Convert it manually to a simple value before passing it to props.\n' + + ' \n' + + ' ^^^^^^^^^^^^^^^', + 'Only plain objects can be passed to Client Components from Server Components. ' + + 'Objects with toJSON methods are not supported. ' + + 'Convert it manually to a simple value before passing it to props.\n' + + ' \n' + + ' ^^^^^^^^^^^^^^^', + ], + {withoutStack: true}, + ); + } }); it('should warn in DEV if a toJSON instance is passed to a host component child', () => { @@ -1560,43 +1589,123 @@ describe('ReactFlight', () => { return 123; } } - expect(() => { - const transport = ReactNoopFlightServer.render( -
Womp womp: {new MyError('spaghetti')}
, + const transport = ReactNoopFlightServer.render( +
Womp womp: {new MyError('spaghetti')}
, + ); + if (gate(flags => flags.enableOwnerStacks)) { + assertConsoleErrorDev( + [ + 'Error objects cannot be rendered as text children. Try formatting it using toString().\n' + + '
Womp womp: {Error}
\n' + + ' ^^^^^^^', + ], + {withoutStack: true}, ); - ReactNoopFlightClient.read(transport); - }).toErrorDev( - 'Error objects cannot be rendered as text children. Try formatting it using toString().\n' + - '
Womp womp: {Error}
\n' + - ' ^^^^^^^', - {withoutStack: true}, - ); + } + + ReactNoopFlightClient.read(transport); + if (gate(flags => flags.enableOwnerStacks)) { + assertConsoleErrorDev([ + 'Error objects cannot be rendered as text children. Try formatting it using toString().\n' + + '
Womp womp: {Error}
\n' + + ' ^^^^^^^\n' + + ' at ()', + ]); + } else { + assertConsoleErrorDev( + [ + 'Error objects cannot be rendered as text children. Try formatting it using toString().\n' + + '
Womp womp: {Error}
\n' + + ' ^^^^^^^', + 'Error objects cannot be rendered as text children. Try formatting it using toString().\n' + + '
Womp womp: {Error}
\n' + + ' ^^^^^^^', + ], + {withoutStack: true}, + ); + } }); it('should warn in DEV if a special object is passed to a host component', () => { - expect(() => { - const transport = ReactNoopFlightServer.render(); - ReactNoopFlightClient.read(transport); - }).toErrorDev( - 'Only plain objects can be passed to Client Components from Server Components. ' + - 'Math objects are not supported.\n' + - ' \n' + - ' ^^^^^^', - {withoutStack: true}, - ); + const transport = ReactNoopFlightServer.render(); + if (gate(flags => flags.enableOwnerStacks)) { + assertConsoleErrorDev( + [ + 'Only plain objects can be passed to Client Components from Server Components. ' + + 'Math objects are not supported.\n' + + ' \n' + + ' ^^^^^^', + ], + {withoutStack: true}, + ); + } + + ReactNoopFlightClient.read(transport); + if (gate(flags => flags.enableOwnerStacks)) { + assertConsoleErrorDev([ + 'Only plain objects can be passed to Client Components from Server Components. ' + + 'Math objects are not supported.\n' + + ' \n' + + ' ^^^^^^\n' + + ' at ()', + ]); + } else { + assertConsoleErrorDev( + [ + 'Only plain objects can be passed to Client Components from Server Components. ' + + 'Math objects are not supported.\n' + + ' \n' + + ' ^^^^^^', + 'Only plain objects can be passed to Client Components from Server Components. ' + + 'Math objects are not supported.\n' + + ' \n' + + ' ^^^^^^', + ], + {withoutStack: true}, + ); + } }); it('should warn in DEV if an object with symbols is passed to a host component', () => { - expect(() => { - const transport = ReactNoopFlightServer.render( - , + const transport = ReactNoopFlightServer.render( + , + ); + if (gate(flags => flags.enableOwnerStacks)) { + assertConsoleErrorDev( + [ + 'Only plain objects can be passed to Client Components from Server Components. ' + + 'Objects with symbol properties like Symbol.iterator are not supported.\n' + + ' \n' + + ' ^^^^', + ], + {withoutStack: true}, ); - ReactNoopFlightClient.read(transport); - }).toErrorDev( - 'Only plain objects can be passed to Client Components from Server Components. ' + - 'Objects with symbol properties like Symbol.iterator are not supported.', - {withoutStack: true}, - ); + } + + ReactNoopFlightClient.read(transport); + if (gate(flags => flags.enableOwnerStacks)) { + assertConsoleErrorDev([ + 'Only plain objects can be passed to Client Components from Server Components. ' + + 'Objects with symbol properties like Symbol.iterator are not supported.\n' + + ' \n' + + ' ^^^^\n' + + ' at ()', + ]); + } else { + assertConsoleErrorDev( + [ + 'Only plain objects can be passed to Client Components from Server Components. ' + + 'Objects with symbol properties like Symbol.iterator are not supported.\n' + + ' \n' + + ' ^^^^', + 'Only plain objects can be passed to Client Components from Server Components. ' + + 'Objects with symbol properties like Symbol.iterator are not supported.\n' + + ' \n' + + ' ^^^^', + ], + {withoutStack: true}, + ); + } }); it('should warn in DEV if a toJSON instance is passed to a Client Component', () => { @@ -1609,14 +1718,47 @@ describe('ReactFlight', () => { return
{value}
; } const Client = clientReference(ClientImpl); - expect(() => { - const transport = ReactNoopFlightServer.render(); - ReactNoopFlightClient.read(transport); - }).toErrorDev( - 'Only plain objects can be passed to Client Components from Server Components. ' + - 'Objects with toJSON methods are not supported.', - {withoutStack: true}, - ); + const transport = ReactNoopFlightServer.render(); + if (gate(flags => flags.enableOwnerStacks)) { + assertConsoleErrorDev( + [ + 'Only plain objects can be passed to Client Components from Server Components. ' + + 'Objects with toJSON methods are not supported. ' + + 'Convert it manually to a simple value before passing it to props.\n' + + ' <... value={{toJSON: ...}}>\n' + + ' ^^^^^^^^^^^^^^^', + ], + {withoutStack: true}, + ); + } + + ReactNoopFlightClient.read(transport); + if (gate(flags => flags.enableOwnerStacks)) { + assertConsoleErrorDev([ + 'Only plain objects can be passed to Client Components from Server Components. ' + + 'Objects with toJSON methods are not supported. ' + + 'Convert it manually to a simple value before passing it to props.\n' + + ' <... value={{toJSON: ...}}>\n' + + ' ^^^^^^^^^^^^^^^\n' + + ' at ()', + ]); + } else { + assertConsoleErrorDev( + [ + 'Only plain objects can be passed to Client Components from Server Components. ' + + 'Objects with toJSON methods are not supported. ' + + 'Convert it manually to a simple value before passing it to props.\n' + + ' <... value={{toJSON: ...}}>\n' + + ' ^^^^^^^^^^^^^^^', + 'Only plain objects can be passed to Client Components from Server Components. ' + + 'Objects with toJSON methods are not supported. ' + + 'Convert it manually to a simple value before passing it to props.\n' + + ' <... value={{toJSON: ...}}>\n' + + ' ^^^^^^^^^^^^^^^', + ], + {withoutStack: true}, + ); + } }); it('should warn in DEV if a toJSON instance is passed to a Client Component child', () => { @@ -1629,19 +1771,49 @@ describe('ReactFlight', () => { return
{children}
; } const Client = clientReference(ClientImpl); - expect(() => { - const transport = ReactNoopFlightServer.render( - Current date: {obj}, + const transport = ReactNoopFlightServer.render( + Current date: {obj}, + ); + if (gate(flags => flags.enableOwnerStacks)) { + assertConsoleErrorDev( + [ + 'Only plain objects can be passed to Client Components from Server Components. ' + + 'Objects with toJSON methods are not supported. ' + + 'Convert it manually to a simple value before passing it to props.\n' + + ' <>Current date: {{toJSON: ...}}\n' + + ' ^^^^^^^^^^^^^^^', + ], + {withoutStack: true}, ); - ReactNoopFlightClient.read(transport); - }).toErrorDev( - 'Only plain objects can be passed to Client Components from Server Components. ' + - 'Objects with toJSON methods are not supported. ' + - 'Convert it manually to a simple value before passing it to props.\n' + - ' <>Current date: {{toJSON: ...}}\n' + - ' ^^^^^^^^^^^^^^^', - {withoutStack: true}, - ); + } + + ReactNoopFlightClient.read(transport); + if (gate(flags => flags.enableOwnerStacks)) { + assertConsoleErrorDev([ + 'Only plain objects can be passed to Client Components from Server Components. ' + + 'Objects with toJSON methods are not supported. ' + + 'Convert it manually to a simple value before passing it to props.\n' + + ' <>Current date: {{toJSON: ...}}\n' + + ' ^^^^^^^^^^^^^^^\n' + + ' at ()', + ]); + } else { + assertConsoleErrorDev( + [ + 'Only plain objects can be passed to Client Components from Server Components. ' + + 'Objects with toJSON methods are not supported. ' + + 'Convert it manually to a simple value before passing it to props.\n' + + ' <>Current date: {{toJSON: ...}}\n' + + ' ^^^^^^^^^^^^^^^', + 'Only plain objects can be passed to Client Components from Server Components. ' + + 'Objects with toJSON methods are not supported. ' + + 'Convert it manually to a simple value before passing it to props.\n' + + ' <>Current date: {{toJSON: ...}}\n' + + ' ^^^^^^^^^^^^^^^', + ], + {withoutStack: true}, + ); + } }); it('should warn in DEV if a special object is passed to a Client Component', () => { @@ -1649,16 +1821,44 @@ describe('ReactFlight', () => { return
{value}
; } const Client = clientReference(ClientImpl); - expect(() => { - const transport = ReactNoopFlightServer.render(); - ReactNoopFlightClient.read(transport); - }).toErrorDev( - 'Only plain objects can be passed to Client Components from Server Components. ' + - 'Math objects are not supported.\n' + - ' <... value={Math}>\n' + - ' ^^^^^^', - {withoutStack: true}, - ); + const transport = ReactNoopFlightServer.render(); + + if (gate(flags => flags.enableOwnerStacks)) { + assertConsoleErrorDev( + [ + 'Only plain objects can be passed to Client Components from Server Components. ' + + 'Math objects are not supported.\n' + + ' <... value={Math}>\n' + + ' ^^^^^^', + ], + {withoutStack: true}, + ); + } + + ReactNoopFlightClient.read(transport); + if (gate(flags => flags.enableOwnerStacks)) { + assertConsoleErrorDev([ + 'Only plain objects can be passed to Client Components from Server Components. ' + + 'Math objects are not supported.\n' + + ' <... value={Math}>\n' + + ' ^^^^^^\n' + + ' at ()', + ]); + } else { + assertConsoleErrorDev( + [ + 'Only plain objects can be passed to Client Components from Server Components. ' + + 'Math objects are not supported.\n' + + ' <... value={Math}>\n' + + ' ^^^^^^', + 'Only plain objects can be passed to Client Components from Server Components. ' + + 'Math objects are not supported.\n' + + ' <... value={Math}>\n' + + ' ^^^^^^', + ], + {withoutStack: true}, + ); + } }); it('should warn in DEV if an object with symbols is passed to a Client Component', () => { @@ -1666,16 +1866,46 @@ describe('ReactFlight', () => { return
{value}
; } const Client = clientReference(ClientImpl); - expect(() => { - const transport = ReactNoopFlightServer.render( - , + assertConsoleErrorDev([]); + const transport = ReactNoopFlightServer.render( + , + ); + if (gate(flags => flags.enableOwnerStacks)) { + assertConsoleErrorDev( + [ + 'Only plain objects can be passed to Client Components from Server Components. ' + + 'Objects with symbol properties like Symbol.iterator are not supported.\n' + + ' <... value={{}}>\n' + + ' ^^^^', + ], + {withoutStack: true}, ); - ReactNoopFlightClient.read(transport); - }).toErrorDev( - 'Only plain objects can be passed to Client Components from Server Components. ' + - 'Objects with symbol properties like Symbol.iterator are not supported.', - {withoutStack: true}, - ); + } + + ReactNoopFlightClient.read(transport); + + if (gate(flags => flags.enableOwnerStacks)) { + assertConsoleErrorDev([ + 'Only plain objects can be passed to Client Components from Server Components. ' + + 'Objects with symbol properties like Symbol.iterator are not supported.\n' + + ' <... value={{}}>\n' + + ' ^^^^\n', + ]); + } else { + assertConsoleErrorDev( + [ + 'Only plain objects can be passed to Client Components from Server Components. ' + + 'Objects with symbol properties like Symbol.iterator are not supported.\n' + + ' <... value={{}}>\n' + + ' ^^^^', + 'Only plain objects can be passed to Client Components from Server Components. ' + + 'Objects with symbol properties like Symbol.iterator are not supported.\n' + + ' <... value={{}}>\n' + + ' ^^^^', + ], + {withoutStack: true}, + ); + } }); it('should warn in DEV if a special object is passed to a nested object in Client Component', () => { @@ -1683,18 +1913,41 @@ describe('ReactFlight', () => { return
{value}
; } const Client = clientReference(ClientImpl); - expect(() => { - const transport = ReactNoopFlightServer.render( - hi}} />, - ); - ReactNoopFlightClient.read(transport); - }).toErrorDev( - 'Only plain objects can be passed to Client Components from Server Components. ' + - 'Math objects are not supported.\n' + - ' {hello: Math, title:

}\n' + - ' ^^^^', - {withoutStack: true}, + const transport = ReactNoopFlightServer.render( + , ); + ReactNoopFlightClient.read(transport); + + if (gate(flags => flags.enableOwnerStacks)) { + assertConsoleErrorDev([ + [ + 'Only plain objects can be passed to Client Components from Server Components. ' + + 'Objects with symbol properties like Symbol.iterator are not supported.\n' + + ' <... value={{}}>\n' + + ' ^^^^', + {withoutStack: true}, + ], + 'Only plain objects can be passed to Client Components from Server Components. ' + + 'Objects with symbol properties like Symbol.iterator are not supported.\n' + + ' <... value={{}}>\n' + + ' ^^^^\n' + + ' at ()', + ]); + } else { + assertConsoleErrorDev( + [ + 'Only plain objects can be passed to Client Components from Server Components. ' + + 'Objects with symbol properties like Symbol.iterator are not supported.\n' + + ' <... value={{}}>\n' + + ' ^^^^', + 'Only plain objects can be passed to Client Components from Server Components. ' + + 'Objects with symbol properties like Symbol.iterator are not supported.\n' + + ' <... value={{}}>\n' + + ' ^^^^', + ], + {withoutStack: true}, + ); + } }); it('should warn in DEV if a special object is passed to a nested array in Client Component', () => { @@ -1702,20 +1955,40 @@ describe('ReactFlight', () => { return
{value}
; } const Client = clientReference(ClientImpl); - expect(() => { - const transport = ReactNoopFlightServer.render( - hi

]} - />, - ); - ReactNoopFlightClient.read(transport); - }).toErrorDev( - 'Only plain objects can be passed to Client Components from Server Components. ' + - 'Math objects are not supported.\n' + - ' [..., Math,

]\n' + - ' ^^^^', - {withoutStack: true}, + const transport = ReactNoopFlightServer.render( + hi

]} />, ); + ReactNoopFlightClient.read(transport); + if (gate(flags => flags.enableOwnerStacks)) { + assertConsoleErrorDev([ + [ + 'Only plain objects can be passed to Client Components from Server Components. ' + + 'Math objects are not supported.\n' + + ' [..., Math,

]\n' + + ' ^^^^', + {withoutStack: true}, + ], + 'Only plain objects can be passed to Client Components from Server Components. ' + + 'Math objects are not supported.\n' + + ' [..., Math,

]\n' + + ' ^^^^\n' + + ' at ()', + ]); + } else { + assertConsoleErrorDev( + [ + 'Only plain objects can be passed to Client Components from Server Components. ' + + 'Math objects are not supported.\n' + + ' [..., Math,

]\n' + + ' ^^^^', + 'Only plain objects can be passed to Client Components from Server Components. ' + + 'Math objects are not supported.\n' + + ' [..., Math,

]\n' + + ' ^^^^', + ], + {withoutStack: true}, + ); + } }); it('should NOT warn in DEV for key getters', () => { @@ -1729,63 +2002,100 @@ describe('ReactFlight', () => { key: "this has a key but parent doesn't", }); } - expect(() => { - // While we're on the server we need to have the Server version active to track component stacks. - jest.resetModules(); - jest.mock('react', () => ReactServer); - const transport = ReactNoopFlightServer.render( - ReactServer.createElement( - 'div', - null, - Array(6).fill(ReactServer.createElement(NoKey)), - ), - ); - jest.resetModules(); - jest.mock('react', () => React); - ReactNoopFlightClient.read(transport); - }).toErrorDev('Each child in a list should have a unique "key" prop.'); + // While we're on the server we need to have the Server version active to track component stacks. + jest.resetModules(); + jest.mock('react', () => ReactServer); + const transport = ReactNoopFlightServer.render( + ReactServer.createElement( + 'div', + null, + Array(6).fill(ReactServer.createElement(NoKey)), + ), + ); + jest.resetModules(); + jest.mock('react', () => React); + ReactNoopFlightClient.read(transport); + if (gate(flags => flags.enableOwnerStacks)) { + assertConsoleErrorDev([ + 'Each child in a list should have a unique "key" prop. ' + + 'See https://react.dev/link/warning-keys for more information.\n' + + ' in NoKey (at **)', + 'Each child in a list should have a unique "key" prop. ' + + 'See https://react.dev/link/warning-keys for more information.\n' + + ' in NoKey (at **)', + ]); + } else { + assertConsoleErrorDev([ + 'Each child in a list should have a unique "key" prop.\n\n' + + 'Check the top-level render call using
. ' + + 'See https://react.dev/link/warning-keys for more information.\n' + + ' in NoKey (at **)', + ]); + } }); // @gate !__DEV__ || enableOwnerStacks it('should warn in DEV a child is missing keys on a fragment', () => { - expect(() => { - // While we're on the server we need to have the Server version active to track component stacks. - jest.resetModules(); - jest.mock('react', () => ReactServer); - const transport = ReactNoopFlightServer.render( - ReactServer.createElement( - 'div', - null, - Array(6).fill(ReactServer.createElement(ReactServer.Fragment)), - ), - ); - jest.resetModules(); - jest.mock('react', () => React); - ReactNoopFlightClient.read(transport); - }).toErrorDev('Each child in a list should have a unique "key" prop.'); + // While we're on the server we need to have the Server version active to track component stacks. + jest.resetModules(); + jest.mock('react', () => ReactServer); + const transport = ReactNoopFlightServer.render( + ReactServer.createElement( + 'div', + null, + Array(6).fill(ReactServer.createElement(ReactServer.Fragment)), + ), + ); + jest.resetModules(); + jest.mock('react', () => React); + ReactNoopFlightClient.read(transport); + if (gate(flags => flags.enableOwnerStacks)) { + assertConsoleErrorDev([ + 'Each child in a list should have a unique "key" prop. ' + + 'See https://react.dev/link/warning-keys for more information.\n' + + ' in Fragment (at **)', + 'Each child in a list should have a unique "key" prop. ' + + 'See https://react.dev/link/warning-keys for more information.\n' + + ' in Fragment (at **)', + ]); + } else { + assertConsoleErrorDev([ + 'Each child in a list should have a unique "key" prop.\n\n' + + 'Check the top-level render call using
. ' + + 'See https://react.dev/link/warning-keys for more information.\n' + + ' in Fragment (at **)', + ]); + } }); it('should warn in DEV a child is missing keys in client component', async () => { function ParentClient({children}) { return children; } - const Parent = clientReference(ParentClient); - await expect(async () => { + + await act(async () => { + const Parent = clientReference(ParentClient); const transport = ReactNoopFlightServer.render( {Array(6).fill(
no key
)}
, ); ReactNoopFlightClient.read(transport); - await act(async () => { - ReactNoop.render(await ReactNoopFlightClient.read(transport)); - }); - }).toErrorDev( - gate(flags => flags.enableOwnerStacks) - ? 'Each child in a list should have a unique "key" prop.' + - '\n\nCheck the top-level render call using . ' + - 'See https://react.dev/link/warning-keys for more information.' - : 'Each child in a list should have a unique "key" prop. ' + - 'See https://react.dev/link/warning-keys for more information.', - ); + + ReactNoop.render(await ReactNoopFlightClient.read(transport)); + }); + if (gate(flags => flags.enableOwnerStacks)) { + assertConsoleErrorDev([ + 'Each child in a list should have a unique "key" prop.\n\n' + + 'Check the top-level render call using . ' + + 'See https://react.dev/link/warning-keys for more information.\n' + + ' in div (at **)', + ]); + } else { + assertConsoleErrorDev([ + 'Each child in a list should have a unique "key" prop. ' + + 'See https://react.dev/link/warning-keys for more information.\n' + + ' in div (at **)', + ]); + } }); it('should error if a class instance is passed to a host component', () => { @@ -3135,19 +3445,17 @@ describe('ReactFlight', () => { }, ); - let transport; - expect(() => { - // Reset the modules so that we get a new overridden console on top of the - // one installed by expect. This ensures that we still emit console.error - // calls. - jest.resetModules(); - jest.mock('react', () => require('react/react.react-server')); - ReactServer = require('react'); - ReactNoopFlightServer = require('react-noop-renderer/flight-server'); - transport = ReactNoopFlightServer.render({ - root: ReactServer.createElement(App), - }); - }).toErrorDev('err'); + // Reset the modules so that we get a new overridden console on top of the + // one installed by expect. This ensures that we still emit console.error + // calls. + jest.resetModules(); + jest.mock('react', () => require('react/react.react-server')); + ReactServer = require('react'); + ReactNoopFlightServer = require('react-noop-renderer/flight-server'); + const transport = ReactNoopFlightServer.render({ + root: ReactServer.createElement(App), + }); + assertConsoleErrorDev(['Error: err']); expect(mockConsoleLog).toHaveBeenCalledTimes(1); expect(mockConsoleLog.mock.calls[0][0]).toBe('hi'); From 03297e048d08de2f7c4c0d2950e2cb1c13875f66 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Fri, 20 Dec 2024 15:09:09 -0500 Subject: [PATCH 0008/1160] [compiler] transform fire calls (#31796) This is the diff with the meaningful changes. The approach is: 1. Collect fire callees and remove fire() calls, create a new binding for the useFire result 2. Update LoadLocals for captured callees to point to the useFire result 3. Update function context to reference useFire results 4. Insert useFire calls after getting to the component scope This approach aims to minimize the amount of new bindings we introduce for the function expressions to minimize bookkeeping for dependency arrays. We keep all of the LoadLocals leading up to function calls as they are and insert new instructions to load the originally captured function, call useFire, and store the result in a new promoted temporary. The lvalues that referenced the original callee are changed to point to the new useFire result. This is the minimal diff to implement the expected behavior (up to importing the useFire call, next diff) and further stacked diffs implement error handling. The rules for fire are: 1. If you use fire for a callee in the effect once you must use it for every time you call it in that effect 2. You can only use fire in a useEffect lambda/functions defined inside the useEffect lambda There is still more work to do here, like updating the effect dependency array and handling object methods -- --- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/facebook/react/pull/31796). * #31811 * #31798 * #31797 * __->__ #31796 --- .../src/Entrypoint/Pipeline.ts | 6 + .../src/Transform/TransformFire.ts | 613 ++++++++++++++++++ .../src/Transform/index.ts | 7 + .../compiler/transform-fire/basic.expect.md | 52 ++ .../fixtures/compiler/transform-fire/basic.js | 13 + .../transform-fire/deep-scope.expect.md | 73 +++ .../compiler/transform-fire/deep-scope.js | 22 + ...r.invalid-conditional-use-effect.expect.md | 37 ++ .../error.invalid-conditional-use-effect.js | 16 + .../error.invalid-multiple-args.expect.md | 34 + .../error.invalid-multiple-args.js | 13 + .../error.invalid-nested-use-effect.expect.md | 40 ++ .../error.invalid-nested-use-effect.js | 19 + .../error.invalid-not-call.expect.md | 34 + .../transform-fire/error.invalid-not-call.js | 13 + .../error.invalid-spread.expect.md | 34 + .../transform-fire/error.invalid-spread.js | 13 + .../error.todo-method.expect.md | 34 + .../transform-fire/error.todo-method.js | 13 + .../transform-fire/multiple-scope.expect.md | 65 ++ .../compiler/transform-fire/multiple-scope.js | 21 + .../transform-fire/repeated-calls.expect.md | 61 ++ .../compiler/transform-fire/repeated-calls.js | 14 + .../shared-hook-calls.expect.md | 80 +++ .../transform-fire/shared-hook-calls.js | 18 + .../use-effect-no-args-no-op.expect.md | 30 + .../use-effect-no-args-no-op.js | 8 + 27 files changed, 1383 insertions(+) create mode 100644 compiler/packages/babel-plugin-react-compiler/src/Transform/TransformFire.ts create mode 100644 compiler/packages/babel-plugin-react-compiler/src/Transform/index.ts create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/basic.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/basic.js create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/deep-scope.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/deep-scope.js create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-conditional-use-effect.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-conditional-use-effect.js create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-multiple-args.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-multiple-args.js create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-nested-use-effect.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-nested-use-effect.js create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-not-call.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-not-call.js create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-spread.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-spread.js create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.todo-method.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.todo-method.js create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/multiple-scope.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/multiple-scope.js create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/repeated-calls.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/repeated-calls.js create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/shared-hook-calls.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/shared-hook-calls.js create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/use-effect-no-args-no-op.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/use-effect-no-args-no-op.js 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 c48cba32b2642..ca6abc0748eb9 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Pipeline.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Pipeline.ts @@ -98,6 +98,7 @@ import {validateNoJSXInTryStatement} from '../Validation/ValidateNoJSXInTryState import {propagateScopeDependenciesHIR} from '../HIR/PropagateScopeDependenciesHIR'; import {outlineJSX} from '../Optimization/OutlineJsx'; import {optimizePropsMethodCalls} from '../Optimization/OptimizePropsMethodCalls'; +import {transformFire} from '../Transform'; export type CompilerPipelineValue = | {kind: 'ast'; name: string; value: CodegenFunction} @@ -197,6 +198,11 @@ function runWithEnvironment( validateHooksUsage(hir); } + if (env.config.enableFire) { + transformFire(hir); + log({kind: 'hir', name: 'TransformFire', value: hir}); + } + if (env.config.validateNoCapitalizedCalls) { validateNoCapitalizedCalls(hir); } diff --git a/compiler/packages/babel-plugin-react-compiler/src/Transform/TransformFire.ts b/compiler/packages/babel-plugin-react-compiler/src/Transform/TransformFire.ts new file mode 100644 index 0000000000000..3fbd141212db2 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/Transform/TransformFire.ts @@ -0,0 +1,613 @@ +/** + * 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, CompilerErrorDetailOptions, ErrorSeverity} from '..'; +import { + CallExpression, + Effect, + Environment, + FunctionExpression, + GeneratedSource, + HIRFunction, + Identifier, + IdentifierId, + Instruction, + InstructionId, + InstructionKind, + InstructionValue, + isUseEffectHookType, + LoadLocal, + makeInstructionId, + Place, + promoteTemporary, +} from '../HIR'; +import {createTemporaryPlace, markInstructionIds} from '../HIR/HIRBuilder'; +import {getOrInsertWith} from '../Utils/utils'; +import {BuiltInFireId, DefaultNonmutatingHook} from '../HIR/ObjectShape'; + +/* + * TODO(jmbrown): + * In this stack: + * - Insert useFire import + * - Assert no lingering fire calls + * - Ensure a fired function is not called regularly elsewhere in the same effect + * + * Future: + * - rewrite dep arrays + * - traverse object methods + * - method calls + * - React.useEffect calls + */ + +const CANNOT_COMPILE_FIRE = 'Cannot compile `fire`'; + +export function transformFire(fn: HIRFunction): void { + const context = new Context(fn.env); + replaceFireFunctions(fn, context); + context.throwIfErrorsFound(); +} + +function replaceFireFunctions(fn: HIRFunction, context: Context): void { + let hasRewrite = false; + for (const [, block] of fn.body.blocks) { + const rewriteInstrs = new Map>(); + const deleteInstrs = new Set(); + for (const instr of block.instructions) { + const {value, lvalue} = instr; + if ( + value.kind === 'CallExpression' && + isUseEffectHookType(value.callee.identifier) && + value.args.length > 0 && + value.args[0].kind === 'Identifier' + ) { + const lambda = context.getFunctionExpression( + value.args[0].identifier.id, + ); + if (lambda != null) { + const capturedCallees = + visitFunctionExpressionAndPropagateFireDependencies( + lambda, + context, + true, + ); + + // Add useFire calls for all fire calls in found in the lambda + const newInstrs = []; + for (const [ + fireCalleePlace, + fireCalleeInfo, + ] of capturedCallees.entries()) { + if (!context.hasCalleeWithInsertedFire(fireCalleePlace)) { + context.addCalleeWithInsertedFire(fireCalleePlace); + const loadUseFireInstr = makeLoadUseFireInstruction(fn.env); + const loadFireCalleeInstr = makeLoadFireCalleeInstruction( + fn.env, + fireCalleeInfo.capturedCalleeIdentifier, + ); + const callUseFireInstr = makeCallUseFireInstruction( + fn.env, + loadUseFireInstr.lvalue, + loadFireCalleeInstr.lvalue, + ); + const storeUseFireInstr = makeStoreUseFireInstruction( + fn.env, + callUseFireInstr.lvalue, + fireCalleeInfo.fireFunctionBinding, + ); + newInstrs.push( + loadUseFireInstr, + loadFireCalleeInstr, + callUseFireInstr, + storeUseFireInstr, + ); + + // We insert all of these instructions before the useEffect is loaded + const loadUseEffectInstrId = context.getLoadGlobalInstrId( + value.callee.identifier.id, + ); + if (loadUseEffectInstrId == null) { + context.pushError({ + loc: value.loc, + description: null, + severity: ErrorSeverity.Invariant, + reason: '[InsertFire] No LoadGlobal found for useEffect call', + suggestions: null, + }); + continue; + } + rewriteInstrs.set(loadUseEffectInstrId, newInstrs); + } + } + } + } else if ( + value.kind === 'CallExpression' && + value.callee.identifier.type.kind === 'Function' && + value.callee.identifier.type.shapeId === BuiltInFireId && + context.inUseEffectLambda() + ) { + /* + * We found a fire(callExpr()) call. We remove the `fire()` call and replace the callExpr() + * with a freshly generated fire function binding. We'll insert the useFire call before the + * useEffect call, which happens in the CallExpression (useEffect) case above. + */ + + /* + * We only allow fire to be called with a CallExpression: `fire(f())` + * TODO: add support for method calls: `fire(this.method())` + */ + if (value.args.length === 1 && value.args[0].kind === 'Identifier') { + const callExpr = context.getCallExpression( + value.args[0].identifier.id, + ); + + if (callExpr != null) { + const calleeId = callExpr.callee.identifier.id; + const loadLocal = context.getLoadLocalInstr(calleeId); + if (loadLocal == null) { + context.pushError({ + loc: value.loc, + description: null, + severity: ErrorSeverity.Invariant, + reason: + '[InsertFire] No loadLocal found for fire call argument', + suggestions: null, + }); + continue; + } + + const fireFunctionBinding = + context.getOrGenerateFireFunctionBinding(loadLocal.place); + + loadLocal.place = {...fireFunctionBinding}; + + // Delete the fire call expression + deleteInstrs.add(instr.id); + } else { + context.pushError({ + loc: value.loc, + description: + '`fire()` can only receive a function call such as `fire(fn(a,b)). Method calls and other expressions are not allowed', + severity: ErrorSeverity.InvalidReact, + reason: CANNOT_COMPILE_FIRE, + suggestions: null, + }); + } + } else { + let description: string = + 'fire() can only take in a single call expression as an argument'; + if (value.args.length === 0) { + description += ' but received none'; + } else if (value.args.length > 1) { + description += ' but received multiple arguments'; + } else if (value.args[0].kind === 'Spread') { + description += ' but received a spread argument'; + } + context.pushError({ + loc: value.loc, + description, + severity: ErrorSeverity.InvalidReact, + reason: CANNOT_COMPILE_FIRE, + suggestions: null, + }); + } + } else if (value.kind === 'CallExpression') { + context.addCallExpression(lvalue.identifier.id, value); + } else if ( + value.kind === 'FunctionExpression' && + context.inUseEffectLambda() + ) { + visitFunctionExpressionAndPropagateFireDependencies( + value, + context, + false, + ); + } else if (value.kind === 'FunctionExpression') { + context.addFunctionExpression(lvalue.identifier.id, value); + } else if (value.kind === 'LoadLocal') { + context.addLoadLocalInstr(lvalue.identifier.id, value); + } else if ( + value.kind === 'LoadGlobal' && + value.binding.kind === 'ImportSpecifier' && + value.binding.module === 'react' && + value.binding.imported === 'fire' && + context.inUseEffectLambda() + ) { + deleteInstrs.add(instr.id); + } else if (value.kind === 'LoadGlobal') { + context.addLoadGlobalInstrId(lvalue.identifier.id, instr.id); + } + } + block.instructions = rewriteInstructions(rewriteInstrs, block.instructions); + block.instructions = deleteInstructions(deleteInstrs, block.instructions); + + if (rewriteInstrs.size > 0 || deleteInstrs.size > 0) { + hasRewrite = true; + } + } + + if (hasRewrite) { + markInstructionIds(fn.body); + } +} + +/** + * Traverses a function expression to find fire calls fire(foo()) and replaces them with + * fireFoo(). + * + * When a function captures a fire call we need to update its context to reflect the newly created + * fire function bindings and update the LoadLocals referenced by the function's dependencies. + * + * @param isUseEffect is necessary so we can keep track of when we should additionally insert + * useFire hooks calls. + */ +function visitFunctionExpressionAndPropagateFireDependencies( + fnExpr: FunctionExpression, + context: Context, + enteringUseEffect: boolean, +): FireCalleesToFireFunctionBinding { + let withScope = enteringUseEffect + ? context.withUseEffectLambdaScope.bind(context) + : context.withFunctionScope.bind(context); + + const calleesCapturedByFnExpression = withScope(() => + replaceFireFunctions(fnExpr.loweredFunc.func, context), + ); + + /* + * Make a mapping from each dependency to the corresponding LoadLocal for it so that + * we can replace the loaded place with the generated fire function binding + */ + const loadLocalsToDepLoads = new Map(); + for (const dep of fnExpr.loweredFunc.dependencies) { + const loadLocal = context.getLoadLocalInstr(dep.identifier.id); + if (loadLocal != null) { + loadLocalsToDepLoads.set(loadLocal.place.identifier.id, loadLocal); + } + } + + const replacedCallees = new Map(); + for (const [ + calleeIdentifierId, + loadedFireFunctionBindingPlace, + ] of calleesCapturedByFnExpression.entries()) { + /* + * Given the ids of captured fire callees, look at the deps for loads of those identifiers + * and replace them with the new fire function binding + */ + const loadLocal = loadLocalsToDepLoads.get(calleeIdentifierId); + if (loadLocal == null) { + context.pushError({ + loc: fnExpr.loc, + description: null, + severity: ErrorSeverity.Invariant, + reason: + '[InsertFire] No loadLocal found for fire call argument for lambda', + suggestions: null, + }); + continue; + } + + const oldPlaceId = loadLocal.place.identifier.id; + loadLocal.place = { + ...loadedFireFunctionBindingPlace.fireFunctionBinding, + }; + + replacedCallees.set( + oldPlaceId, + loadedFireFunctionBindingPlace.fireFunctionBinding, + ); + } + + // For each replaced callee, update the context of the function expression to track it + for ( + let contextIdx = 0; + contextIdx < fnExpr.loweredFunc.func.context.length; + contextIdx++ + ) { + const contextItem = fnExpr.loweredFunc.func.context[contextIdx]; + const replacedCallee = replacedCallees.get(contextItem.identifier.id); + if (replacedCallee != null) { + fnExpr.loweredFunc.func.context[contextIdx] = replacedCallee; + } + } + + context.mergeCalleesFromInnerScope(calleesCapturedByFnExpression); + + return calleesCapturedByFnExpression; +} + +function makeLoadUseFireInstruction(env: Environment): Instruction { + const useFirePlace = createTemporaryPlace(env, GeneratedSource); + useFirePlace.effect = Effect.Read; + useFirePlace.identifier.type = DefaultNonmutatingHook; + const instrValue: InstructionValue = { + kind: 'LoadGlobal', + binding: { + kind: 'ImportSpecifier', + name: 'useFire', + module: 'react', + imported: 'useFire', + }, + loc: GeneratedSource, + }; + return { + id: makeInstructionId(0), + value: instrValue, + lvalue: {...useFirePlace}, + loc: GeneratedSource, + }; +} + +function makeLoadFireCalleeInstruction( + env: Environment, + fireCalleeIdentifier: Identifier, +): Instruction { + const loadedFireCallee = createTemporaryPlace(env, GeneratedSource); + const fireCallee: Place = { + kind: 'Identifier', + identifier: fireCalleeIdentifier, + reactive: false, + effect: Effect.Unknown, + loc: fireCalleeIdentifier.loc, + }; + return { + id: makeInstructionId(0), + value: { + kind: 'LoadLocal', + loc: GeneratedSource, + place: {...fireCallee}, + }, + lvalue: {...loadedFireCallee}, + loc: GeneratedSource, + }; +} + +function makeCallUseFireInstruction( + env: Environment, + useFirePlace: Place, + argPlace: Place, +): Instruction { + const useFireCallResultPlace = createTemporaryPlace(env, GeneratedSource); + useFireCallResultPlace.effect = Effect.Read; + + const useFireCall: CallExpression = { + kind: 'CallExpression', + callee: {...useFirePlace}, + args: [argPlace], + loc: GeneratedSource, + }; + + return { + id: makeInstructionId(0), + value: useFireCall, + lvalue: {...useFireCallResultPlace}, + loc: GeneratedSource, + }; +} + +function makeStoreUseFireInstruction( + env: Environment, + useFireCallResultPlace: Place, + fireFunctionBindingPlace: Place, +): Instruction { + promoteTemporary(fireFunctionBindingPlace.identifier); + + const fireFunctionBindingLValuePlace = createTemporaryPlace( + env, + GeneratedSource, + ); + return { + id: makeInstructionId(0), + value: { + kind: 'StoreLocal', + lvalue: { + kind: InstructionKind.Const, + place: {...fireFunctionBindingPlace}, + }, + value: {...useFireCallResultPlace}, + type: null, + loc: GeneratedSource, + }, + lvalue: fireFunctionBindingLValuePlace, + loc: GeneratedSource, + }; +} + +type FireCalleesToFireFunctionBinding = Map< + IdentifierId, + { + fireFunctionBinding: Place; + capturedCalleeIdentifier: Identifier; + } +>; + +class Context { + #env: Environment; + + #errors: CompilerError = new CompilerError(); + + /* + * Used to look up the call expression passed to a `fire(callExpr())`. Gives back + * the `callExpr()`. + */ + #callExpressions = new Map(); + + /* + * We keep track of function expressions so that we can traverse them when + * we encounter a lambda passed to a useEffect call + */ + #functionExpressions = new Map(); + + /* + * Mapping from lvalue ids to the LoadLocal for it. Allows us to replace dependency LoadLocals. + */ + #loadLocals = new Map(); + + /* + * Maps all of the fire callees found in a component/hook to the generated fire function places + * we create for them. Allows us to reuse already-inserted useFire results + */ + #fireCalleesToFireFunctions: Map = new Map(); + + /* + * The callees for which we have already created fire bindings. Used to skip inserting a new + * useFire call for a fire callee if one has already been created. + */ + #calleesWithInsertedFire = new Set(); + + /* + * A mapping from fire callees to the created fire function bindings that are reachable from this + * scope. + * + * We additionally keep track of the captured callee identifier so that we can properly reference + * it in the place where we LoadLocal the callee as an argument to useFire. + */ + #capturedCalleeIdentifierIds: FireCalleesToFireFunctionBinding = new Map(); + + /* + * We only transform fire calls if we're syntactically within a useEffect lambda (for now) + */ + #inUseEffectLambda = false; + + /* + * Mapping from useEffect callee identifier ids to the instruction id of the + * load global instruction for the useEffect call. We use this to insert the + * useFire calls before the useEffect call + */ + #loadGlobalInstructionIds = new Map(); + + constructor(env: Environment) { + this.#env = env; + } + + pushError(error: CompilerErrorDetailOptions): void { + this.#errors.push(error); + } + + withFunctionScope(fn: () => void): FireCalleesToFireFunctionBinding { + fn(); + return this.#capturedCalleeIdentifierIds; + } + + withUseEffectLambdaScope(fn: () => void): FireCalleesToFireFunctionBinding { + const capturedCalleeIdentifierIds = this.#capturedCalleeIdentifierIds; + const inUseEffectLambda = this.#inUseEffectLambda; + + this.#capturedCalleeIdentifierIds = new Map(); + this.#inUseEffectLambda = true; + + const resultCapturedCalleeIdentifierIds = this.withFunctionScope(fn); + + this.#capturedCalleeIdentifierIds = capturedCalleeIdentifierIds; + this.#inUseEffectLambda = inUseEffectLambda; + + return resultCapturedCalleeIdentifierIds; + } + + addCallExpression(id: IdentifierId, callExpr: CallExpression): void { + this.#callExpressions.set(id, callExpr); + } + + getCallExpression(id: IdentifierId): CallExpression | undefined { + return this.#callExpressions.get(id); + } + + addLoadLocalInstr(id: IdentifierId, loadLocal: LoadLocal): void { + this.#loadLocals.set(id, loadLocal); + } + + getLoadLocalInstr(id: IdentifierId): LoadLocal | undefined { + return this.#loadLocals.get(id); + } + + getOrGenerateFireFunctionBinding(callee: Place): Place { + const fireFunctionBinding = getOrInsertWith( + this.#fireCalleesToFireFunctions, + callee.identifier.id, + () => createTemporaryPlace(this.#env, GeneratedSource), + ); + + this.#capturedCalleeIdentifierIds.set(callee.identifier.id, { + fireFunctionBinding, + capturedCalleeIdentifier: callee.identifier, + }); + + return fireFunctionBinding; + } + + mergeCalleesFromInnerScope( + innerCallees: FireCalleesToFireFunctionBinding, + ): void { + for (const [id, calleeInfo] of innerCallees.entries()) { + this.#capturedCalleeIdentifierIds.set(id, calleeInfo); + } + } + + addCalleeWithInsertedFire(id: IdentifierId): void { + this.#calleesWithInsertedFire.add(id); + } + + hasCalleeWithInsertedFire(id: IdentifierId): boolean { + return this.#calleesWithInsertedFire.has(id); + } + + inUseEffectLambda(): boolean { + return this.#inUseEffectLambda; + } + + addFunctionExpression(id: IdentifierId, fn: FunctionExpression): void { + this.#functionExpressions.set(id, fn); + } + + getFunctionExpression(id: IdentifierId): FunctionExpression | undefined { + return this.#functionExpressions.get(id); + } + + addLoadGlobalInstrId(id: IdentifierId, instrId: InstructionId): void { + this.#loadGlobalInstructionIds.set(id, instrId); + } + + getLoadGlobalInstrId(id: IdentifierId): InstructionId | undefined { + return this.#loadGlobalInstructionIds.get(id); + } + + throwIfErrorsFound(): void { + if (this.#errors.hasErrors()) throw this.#errors; + } +} + +function deleteInstructions( + deleteInstrs: Set, + instructions: Array, +): Array { + if (deleteInstrs.size > 0) { + const newInstrs = instructions.filter(instr => !deleteInstrs.has(instr.id)); + return newInstrs; + } + return instructions; +} + +function rewriteInstructions( + rewriteInstrs: Map>, + instructions: Array, +): Array { + if (rewriteInstrs.size > 0) { + const newInstrs = []; + for (const instr of instructions) { + const newInstrsAtId = rewriteInstrs.get(instr.id); + if (newInstrsAtId != null) { + newInstrs.push(...newInstrsAtId, instr); + } else { + newInstrs.push(instr); + } + } + + return newInstrs; + } + + return instructions; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/Transform/index.ts b/compiler/packages/babel-plugin-react-compiler/src/Transform/index.ts new file mode 100644 index 0000000000000..8665ead0b1af0 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/Transform/index.ts @@ -0,0 +1,7 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ +export {transformFire} from './TransformFire'; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/basic.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/basic.expect.md new file mode 100644 index 0000000000000..f3b67da3ecfab --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/basic.expect.md @@ -0,0 +1,52 @@ + +## Input + +```javascript +// @enableFire +import {fire} from 'react'; + +function Component(props) { + const foo = props => { + console.log(props); + }; + useEffect(() => { + fire(foo(props)); + }); + + return null; +} + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; // @enableFire +import { fire } from "react"; + +function Component(props) { + const $ = _c(3); + const foo = _temp; + const t0 = useFire(foo); + let t1; + if ($[0] !== props || $[1] !== t0) { + t1 = () => { + t0(props); + }; + $[0] = props; + $[1] = t0; + $[2] = t1; + } else { + t1 = $[2]; + } + useEffect(t1); + return null; +} +function _temp(props_0) { + console.log(props_0); +} + +``` + +### Eval output +(kind: exception) Fixture not implemented \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/basic.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/basic.js new file mode 100644 index 0000000000000..2f7a72e4eed51 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/basic.js @@ -0,0 +1,13 @@ +// @enableFire +import {fire} from 'react'; + +function Component(props) { + const foo = props => { + console.log(props); + }; + useEffect(() => { + fire(foo(props)); + }); + + return null; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/deep-scope.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/deep-scope.expect.md new file mode 100644 index 0000000000000..ee9bf268d0e6a --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/deep-scope.expect.md @@ -0,0 +1,73 @@ + +## Input + +```javascript +// @enableFire +import {fire} from 'react'; + +function Component(props) { + const foo = props => { + console.log(props); + }; + useEffect(() => { + function nested() { + function nestedAgain() { + function nestedThrice() { + fire(foo(props)); + } + nestedThrice(); + } + nestedAgain(); + } + nested(); + }); + + return null; +} + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; // @enableFire +import { fire } from "react"; + +function Component(props) { + const $ = _c(3); + const foo = _temp; + const t0 = useFire(foo); + let t1; + if ($[0] !== props || $[1] !== t0) { + t1 = () => { + const nested = function nested() { + const nestedAgain = function nestedAgain() { + const nestedThrice = function nestedThrice() { + t0(props); + }; + + nestedThrice(); + }; + + nestedAgain(); + }; + + nested(); + }; + $[0] = props; + $[1] = t0; + $[2] = t1; + } else { + t1 = $[2]; + } + useEffect(t1); + return null; +} +function _temp(props_0) { + console.log(props_0); +} + +``` + +### Eval output +(kind: exception) Fixture not implemented \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/deep-scope.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/deep-scope.js new file mode 100644 index 0000000000000..b056c3f53a85a --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/deep-scope.js @@ -0,0 +1,22 @@ +// @enableFire +import {fire} from 'react'; + +function Component(props) { + const foo = props => { + console.log(props); + }; + useEffect(() => { + function nested() { + function nestedAgain() { + function nestedThrice() { + fire(foo(props)); + } + nestedThrice(); + } + nestedAgain(); + } + nested(); + }); + + return null; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-conditional-use-effect.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-conditional-use-effect.expect.md new file mode 100644 index 0000000000000..a24f27a695f54 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-conditional-use-effect.expect.md @@ -0,0 +1,37 @@ + +## Input + +```javascript +// @enableFire +import {fire, useEffect} from 'react'; + +function Component(props) { + const foo = props => { + console.log(props); + }; + + if (props.cond) { + useEffect(() => { + fire(foo(props)); + }); + } + + return null; +} + +``` + + +## Error + +``` + 8 | + 9 | if (props.cond) { +> 10 | useEffect(() => { + | ^^^^^^^^^ InvalidReact: Hooks must always be called in a consistent order, and may not be called conditionally. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning) (10:10) + 11 | fire(foo(props)); + 12 | }); + 13 | } +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-conditional-use-effect.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-conditional-use-effect.js new file mode 100644 index 0000000000000..30ae8e59b986e --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-conditional-use-effect.js @@ -0,0 +1,16 @@ +// @enableFire +import {fire, useEffect} from 'react'; + +function Component(props) { + const foo = props => { + console.log(props); + }; + + if (props.cond) { + useEffect(() => { + fire(foo(props)); + }); + } + + return null; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-multiple-args.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-multiple-args.expect.md new file mode 100644 index 0000000000000..8329717cb3939 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-multiple-args.expect.md @@ -0,0 +1,34 @@ + +## Input + +```javascript +// @enableFire +import {fire} from 'react'; + +function Component({bar, baz}) { + const foo = () => { + console.log(bar, baz); + }; + useEffect(() => { + fire(foo(bar), baz); + }); + + return null; +} + +``` + + +## Error + +``` + 7 | }; + 8 | useEffect(() => { +> 9 | fire(foo(bar), baz); + | ^^^^^^^^^^^^^^^^^^^ InvalidReact: Cannot compile `fire`. fire() can only take in a single call expression as an argument but received multiple arguments (9:9) + 10 | }); + 11 | + 12 | return null; +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-multiple-args.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-multiple-args.js new file mode 100644 index 0000000000000..980b0dfcb5e78 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-multiple-args.js @@ -0,0 +1,13 @@ +// @enableFire +import {fire} from 'react'; + +function Component({bar, baz}) { + const foo = () => { + console.log(bar, baz); + }; + useEffect(() => { + fire(foo(bar), baz); + }); + + return null; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-nested-use-effect.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-nested-use-effect.expect.md new file mode 100644 index 0000000000000..580fd6a2a68b8 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-nested-use-effect.expect.md @@ -0,0 +1,40 @@ + +## Input + +```javascript +// @enable +import {fire} from 'react'; + +function Component(props) { + const foo = props => { + console.log(props); + }; + useEffect(() => { + useEffect(() => { + function nested() { + fire(foo(props)); + } + + nested(); + }); + }); + + return null; +} + +``` + + +## Error + +``` + 7 | }; + 8 | useEffect(() => { +> 9 | useEffect(() => { + | ^^^^^^^^^ InvalidReact: Hooks must be called at the top level in the body of a function component or custom hook, and may not be called within function expressions. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning). Cannot call useEffect within a function component (9:9) + 10 | function nested() { + 11 | fire(foo(props)); + 12 | } +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-nested-use-effect.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-nested-use-effect.js new file mode 100644 index 0000000000000..16f242572445c --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-nested-use-effect.js @@ -0,0 +1,19 @@ +// @enable +import {fire} from 'react'; + +function Component(props) { + const foo = props => { + console.log(props); + }; + useEffect(() => { + useEffect(() => { + function nested() { + fire(foo(props)); + } + + nested(); + }); + }); + + return null; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-not-call.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-not-call.expect.md new file mode 100644 index 0000000000000..855c7b7d706cb --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-not-call.expect.md @@ -0,0 +1,34 @@ + +## Input + +```javascript +// @enableFire +import {fire} from 'react'; + +function Component(props) { + const foo = () => { + console.log(props); + }; + useEffect(() => { + fire(props); + }); + + return null; +} + +``` + + +## Error + +``` + 7 | }; + 8 | useEffect(() => { +> 9 | fire(props); + | ^^^^^^^^^^^ InvalidReact: Cannot compile `fire`. `fire()` can only receive a function call such as `fire(fn(a,b)). Method calls and other expressions are not allowed (9:9) + 10 | }); + 11 | + 12 | return null; +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-not-call.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-not-call.js new file mode 100644 index 0000000000000..3d1ae3658fd20 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-not-call.js @@ -0,0 +1,13 @@ +// @enableFire +import {fire} from 'react'; + +function Component(props) { + const foo = () => { + console.log(props); + }; + useEffect(() => { + fire(props); + }); + + return null; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-spread.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-spread.expect.md new file mode 100644 index 0000000000000..c0b797fc14471 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-spread.expect.md @@ -0,0 +1,34 @@ + +## Input + +```javascript +// @enableFire +import {fire} from 'react'; + +function Component(props) { + const foo = () => { + console.log(props); + }; + useEffect(() => { + fire(...foo); + }); + + return null; +} + +``` + + +## Error + +``` + 7 | }; + 8 | useEffect(() => { +> 9 | fire(...foo); + | ^^^^^^^^^^^^ InvalidReact: Cannot compile `fire`. fire() can only take in a single call expression as an argument but received a spread argument (9:9) + 10 | }); + 11 | + 12 | return null; +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-spread.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-spread.js new file mode 100644 index 0000000000000..68e317588bd51 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-spread.js @@ -0,0 +1,13 @@ +// @enableFire +import {fire} from 'react'; + +function Component(props) { + const foo = () => { + console.log(props); + }; + useEffect(() => { + fire(...foo); + }); + + return null; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.todo-method.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.todo-method.expect.md new file mode 100644 index 0000000000000..3f237cfc6f364 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.todo-method.expect.md @@ -0,0 +1,34 @@ + +## Input + +```javascript +// @enableFire +import {fire} from 'react'; + +function Component(props) { + const foo = () => { + console.log(props); + }; + useEffect(() => { + fire(props.foo()); + }); + + return null; +} + +``` + + +## Error + +``` + 7 | }; + 8 | useEffect(() => { +> 9 | fire(props.foo()); + | ^^^^^^^^^^^^^^^^^ InvalidReact: Cannot compile `fire`. `fire()` can only receive a function call such as `fire(fn(a,b)). Method calls and other expressions are not allowed (9:9) + 10 | }); + 11 | + 12 | return null; +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.todo-method.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.todo-method.js new file mode 100644 index 0000000000000..c75622ca5e7a3 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.todo-method.js @@ -0,0 +1,13 @@ +// @enableFire +import {fire} from 'react'; + +function Component(props) { + const foo = () => { + console.log(props); + }; + useEffect(() => { + fire(props.foo()); + }); + + return null; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/multiple-scope.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/multiple-scope.expect.md new file mode 100644 index 0000000000000..08c45ea279d8d --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/multiple-scope.expect.md @@ -0,0 +1,65 @@ + +## Input + +```javascript +// @enableFire +import {fire} from 'react'; + +function Component(props) { + const foo = props => { + console.log(props); + }; + useEffect(() => { + fire(foo(props)); + function nested() { + fire(foo(props)); + function innerNested() { + fire(foo(props)); + } + } + + nested(); + }); + + return null; +} + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; // @enableFire +import { fire } from "react"; + +function Component(props) { + const $ = _c(3); + const foo = _temp; + const t0 = useFire(foo); + let t1; + if ($[0] !== props || $[1] !== t0) { + t1 = () => { + t0(props); + const nested = function nested() { + t0(props); + }; + + nested(); + }; + $[0] = props; + $[1] = t0; + $[2] = t1; + } else { + t1 = $[2]; + } + useEffect(t1); + return null; +} +function _temp(props_0) { + console.log(props_0); +} + +``` + +### Eval output +(kind: exception) Fixture not implemented \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/multiple-scope.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/multiple-scope.js new file mode 100644 index 0000000000000..54410680e63c6 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/multiple-scope.js @@ -0,0 +1,21 @@ +// @enableFire +import {fire} from 'react'; + +function Component(props) { + const foo = props => { + console.log(props); + }; + useEffect(() => { + fire(foo(props)); + function nested() { + fire(foo(props)); + function innerNested() { + fire(foo(props)); + } + } + + nested(); + }); + + return null; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/repeated-calls.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/repeated-calls.expect.md new file mode 100644 index 0000000000000..693a8d380aa38 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/repeated-calls.expect.md @@ -0,0 +1,61 @@ + +## Input + +```javascript +// @enableFire +import {fire} from 'react'; + +function Component(props) { + const foo = () => { + console.log(props); + }; + useEffect(() => { + fire(foo(props)); + fire(foo(props)); + }); + + return null; +} + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; // @enableFire +import { fire } from "react"; + +function Component(props) { + const $ = _c(5); + let t0; + if ($[0] !== props) { + t0 = () => { + console.log(props); + }; + $[0] = props; + $[1] = t0; + } else { + t0 = $[1]; + } + const foo = t0; + const t1 = useFire(foo); + let t2; + if ($[2] !== props || $[3] !== t1) { + t2 = () => { + t1(props); + t1(props); + }; + $[2] = props; + $[3] = t1; + $[4] = t2; + } else { + t2 = $[4]; + } + useEffect(t2); + return null; +} + +``` + +### Eval output +(kind: exception) Fixture not implemented \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/repeated-calls.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/repeated-calls.js new file mode 100644 index 0000000000000..14e1cb06b1bbe --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/repeated-calls.js @@ -0,0 +1,14 @@ +// @enableFire +import {fire} from 'react'; + +function Component(props) { + const foo = () => { + console.log(props); + }; + useEffect(() => { + fire(foo(props)); + fire(foo(props)); + }); + + return null; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/shared-hook-calls.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/shared-hook-calls.expect.md new file mode 100644 index 0000000000000..959338b5d8d5d --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/shared-hook-calls.expect.md @@ -0,0 +1,80 @@ + +## Input + +```javascript +// @enableFire +import {fire} from 'react'; + +function Component({bar, baz}) { + const foo = () => { + console.log(bar); + }; + useEffect(() => { + fire(foo(bar)); + fire(baz(bar)); + }); + + useEffect(() => { + fire(foo(bar)); + }); + + return null; +} + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; // @enableFire +import { fire } from "react"; + +function Component(t0) { + const $ = _c(9); + const { bar, baz } = t0; + let t1; + if ($[0] !== bar) { + t1 = () => { + console.log(bar); + }; + $[0] = bar; + $[1] = t1; + } else { + t1 = $[1]; + } + const foo = t1; + const t2 = useFire(foo); + const t3 = useFire(baz); + let t4; + if ($[2] !== bar || $[3] !== t2 || $[4] !== t3) { + t4 = () => { + t2(bar); + t3(bar); + }; + $[2] = bar; + $[3] = t2; + $[4] = t3; + $[5] = t4; + } else { + t4 = $[5]; + } + useEffect(t4); + let t5; + if ($[6] !== bar || $[7] !== t2) { + t5 = () => { + t2(bar); + }; + $[6] = bar; + $[7] = t2; + $[8] = t5; + } else { + t5 = $[8]; + } + useEffect(t5); + return null; +} + +``` + +### Eval output +(kind: exception) Fixture not implemented \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/shared-hook-calls.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/shared-hook-calls.js new file mode 100644 index 0000000000000..5cb51e9bd3c78 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/shared-hook-calls.js @@ -0,0 +1,18 @@ +// @enableFire +import {fire} from 'react'; + +function Component({bar, baz}) { + const foo = () => { + console.log(bar); + }; + useEffect(() => { + fire(foo(bar)); + fire(baz(bar)); + }); + + useEffect(() => { + fire(foo(bar)); + }); + + return null; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/use-effect-no-args-no-op.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/use-effect-no-args-no-op.expect.md new file mode 100644 index 0000000000000..f482ac44ddda7 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/use-effect-no-args-no-op.expect.md @@ -0,0 +1,30 @@ + +## Input + +```javascript +// @enableFire +import {fire} from 'react'; + +function Component(props) { + useEffect(); + + return null; +} + +``` + +## Code + +```javascript +// @enableFire +import { fire } from "react"; + +function Component(props) { + useEffect(); + return null; +} + +``` + +### Eval output +(kind: exception) Fixture not implemented \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/use-effect-no-args-no-op.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/use-effect-no-args-no-op.js new file mode 100644 index 0000000000000..731c45df677e9 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/use-effect-no-args-no-op.js @@ -0,0 +1,8 @@ +// @enableFire +import {fire} from 'react'; + +function Component(props) { + useEffect(); + + return null; +} From ab27231dc51aa2535df37555797e630d31047fa4 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Fri, 20 Dec 2024 15:25:30 -0500 Subject: [PATCH 0009/1160] [compiler] add fire imports (#31797) Summary: Adds import {useFire} from 'react' when fire syntax is used. This is experimentation and may not become a stable feature in the compiler. -- --- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/facebook/react/pull/31797). * #31811 * #31798 * __->__ #31797 --- .../babel-plugin-react-compiler/src/Entrypoint/Program.ts | 5 +++++ .../babel-plugin-react-compiler/src/HIR/Environment.ts | 2 ++ .../src/ReactiveScopes/CodegenReactiveFunction.ts | 6 ++++++ .../src/Transform/TransformFire.ts | 2 +- .../fixtures/compiler/transform-fire/basic.expect.md | 1 + .../fixtures/compiler/transform-fire/deep-scope.expect.md | 1 + .../compiler/transform-fire/multiple-scope.expect.md | 1 + .../compiler/transform-fire/repeated-calls.expect.md | 1 + .../compiler/transform-fire/shared-hook-calls.expect.md | 1 + 9 files changed, 19 insertions(+), 1 deletion(-) diff --git a/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Program.ts b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Program.ts index a6e09a1d061da..ca10e9c0d471f 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Program.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Program.ts @@ -564,6 +564,11 @@ export function compileProgram( if (environment.enableChangeDetectionForDebugging != null) { externalFunctions.push(environment.enableChangeDetectionForDebugging); } + + const hasFireRewrite = compiledFns.some(c => c.compiledFn.hasFireRewrite); + if (environment.enableFire && hasFireRewrite) { + externalFunctions.push({source: 'react', importSpecifierName: 'useFire'}); + } } catch (err) { handleError(err, pass, null); return; diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts index e2932296ca739..f3f426df56e44 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts @@ -787,6 +787,7 @@ export class Environment { fnType: ReactFunctionType; useMemoCacheIdentifier: string; hasLoweredContextAccess: boolean; + hasFireRewrite: boolean; #contextIdentifiers: Set; #hoistedIdentifiers: Set; @@ -811,6 +812,7 @@ export class Environment { this.#shapes = new Map(DEFAULT_SHAPES); this.#globals = new Map(DEFAULT_GLOBALS); this.hasLoweredContextAccess = false; + this.hasFireRewrite = false; if ( config.disableMemoizationForDebugging && diff --git a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/CodegenReactiveFunction.ts b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/CodegenReactiveFunction.ts index b2f1b9e6d4edc..b9ec688d877ed 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/CodegenReactiveFunction.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/CodegenReactiveFunction.ts @@ -103,6 +103,11 @@ export type CodegenFunction = { * This is true if the compiler has the lowered useContext calls. */ hasLoweredContextAccess: boolean; + + /** + * This is true if the compiler has compiled a fire to a useFire call + */ + hasFireRewrite: boolean; }; export function codegenFunction( @@ -355,6 +360,7 @@ function codegenReactiveFunction( prunedMemoValues: countMemoBlockVisitor.prunedMemoValues, outlined: [], hasLoweredContextAccess: fn.env.hasLoweredContextAccess, + hasFireRewrite: fn.env.hasFireRewrite, }); } diff --git a/compiler/packages/babel-plugin-react-compiler/src/Transform/TransformFire.ts b/compiler/packages/babel-plugin-react-compiler/src/Transform/TransformFire.ts index 3fbd141212db2..5c7f906be881b 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Transform/TransformFire.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Transform/TransformFire.ts @@ -32,7 +32,6 @@ import {BuiltInFireId, DefaultNonmutatingHook} from '../HIR/ObjectShape'; /* * TODO(jmbrown): * In this stack: - * - Insert useFire import * - Assert no lingering fire calls * - Ensure a fired function is not called regularly elsewhere in the same effect * @@ -226,6 +225,7 @@ function replaceFireFunctions(fn: HIRFunction, context: Context): void { if (rewriteInstrs.size > 0 || deleteInstrs.size > 0) { hasRewrite = true; + fn.env.hasFireRewrite = true; } } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/basic.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/basic.expect.md index f3b67da3ecfab..a5bf42de7b008 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/basic.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/basic.expect.md @@ -21,6 +21,7 @@ function Component(props) { ## Code ```javascript +import { useFire } from "react"; import { c as _c } from "react/compiler-runtime"; // @enableFire import { fire } from "react"; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/deep-scope.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/deep-scope.expect.md index ee9bf268d0e6a..585a820b13416 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/deep-scope.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/deep-scope.expect.md @@ -30,6 +30,7 @@ function Component(props) { ## Code ```javascript +import { useFire } from "react"; import { c as _c } from "react/compiler-runtime"; // @enableFire import { fire } from "react"; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/multiple-scope.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/multiple-scope.expect.md index 08c45ea279d8d..0d531041355be 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/multiple-scope.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/multiple-scope.expect.md @@ -29,6 +29,7 @@ function Component(props) { ## Code ```javascript +import { useFire } from "react"; import { c as _c } from "react/compiler-runtime"; // @enableFire import { fire } from "react"; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/repeated-calls.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/repeated-calls.expect.md index 693a8d380aa38..3eb9f0e71c71a 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/repeated-calls.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/repeated-calls.expect.md @@ -22,6 +22,7 @@ function Component(props) { ## Code ```javascript +import { useFire } from "react"; import { c as _c } from "react/compiler-runtime"; // @enableFire import { fire } from "react"; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/shared-hook-calls.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/shared-hook-calls.expect.md index 959338b5d8d5d..4362a3a8461e6 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/shared-hook-calls.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/shared-hook-calls.expect.md @@ -26,6 +26,7 @@ function Component({bar, baz}) { ## Code ```javascript +import { useFire } from "react"; import { c as _c } from "react/compiler-runtime"; // @enableFire import { fire } from "react"; From 45a720f7c7ff98e22fb299b50fef90fe319081a7 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Fri, 20 Dec 2024 16:55:01 -0500 Subject: [PATCH 0010/1160] [compile] Error on fire outside of effects and ensure correct compilation, correct import (#31798) Traverse the compiled functions to ensure there are no lingering fires and that all fire calls are inside an effect lambda. Also corrects the import to import from the compiler runtime instead -- --- .../src/Entrypoint/Program.ts | 5 +- .../src/HIR/PrintHIR.ts | 8 ++ .../src/Transform/TransformFire.ts | 104 ++++++++++++++++-- .../compiler/transform-fire/basic.expect.md | 2 +- .../transform-fire/deep-scope.expect.md | 2 +- ...ror.invalid-mix-fire-and-no-fire.expect.md | 39 +++++++ .../error.invalid-mix-fire-and-no-fire.js | 18 +++ .../error.invalid-outside-effect.expect.md | 38 +++++++ .../error.invalid-outside-effect.js | 15 +++ .../transform-fire/multiple-scope.expect.md | 2 +- .../transform-fire/repeated-calls.expect.md | 2 +- .../shared-hook-calls.expect.md | 2 +- 12 files changed, 221 insertions(+), 16 deletions(-) create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-mix-fire-and-no-fire.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-mix-fire-and-no-fire.js create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-outside-effect.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-outside-effect.js diff --git a/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Program.ts b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Program.ts index ca10e9c0d471f..bb0d662c4f67e 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Program.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Program.ts @@ -567,7 +567,10 @@ export function compileProgram( const hasFireRewrite = compiledFns.some(c => c.compiledFn.hasFireRewrite); if (environment.enableFire && hasFireRewrite) { - externalFunctions.push({source: 'react', importSpecifierName: 'useFire'}); + externalFunctions.push({ + source: getReactCompilerRuntimeModule(pass.opts), + importSpecifierName: 'useFire', + }); } } catch (err) { handleError(err, pass, null); diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/PrintHIR.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/PrintHIR.ts index 526ab7c7e52bb..a6f6c606e118d 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/PrintHIR.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/PrintHIR.ts @@ -897,6 +897,14 @@ export function printSourceLocation(loc: SourceLocation): string { } } +export function printSourceLocationLine(loc: SourceLocation): string { + if (typeof loc === 'symbol') { + return 'generated'; + } else { + return `${loc.start.line}:${loc.end.line}`; + } +} + export function printAliases(aliases: DisjointSet): string { const aliasSets = aliases.buildSets(); diff --git a/compiler/packages/babel-plugin-react-compiler/src/Transform/TransformFire.ts b/compiler/packages/babel-plugin-react-compiler/src/Transform/TransformFire.ts index 5c7f906be881b..a0256fd80cd66 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Transform/TransformFire.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Transform/TransformFire.ts @@ -5,7 +5,12 @@ * LICENSE file in the root directory of this source tree. */ -import {CompilerError, CompilerErrorDetailOptions, ErrorSeverity} from '..'; +import { + CompilerError, + CompilerErrorDetailOptions, + ErrorSeverity, + SourceLocation, +} from '..'; import { CallExpression, Effect, @@ -28,14 +33,11 @@ import { import {createTemporaryPlace, markInstructionIds} from '../HIR/HIRBuilder'; import {getOrInsertWith} from '../Utils/utils'; import {BuiltInFireId, DefaultNonmutatingHook} from '../HIR/ObjectShape'; +import {eachInstructionOperand} from '../HIR/visitors'; +import {printSourceLocationLine} from '../HIR/PrintHIR'; /* * TODO(jmbrown): - * In this stack: - * - Assert no lingering fire calls - * - Ensure a fired function is not called regularly elsewhere in the same effect - * - * Future: * - rewrite dep arrays * - traverse object methods * - method calls @@ -47,6 +49,9 @@ const CANNOT_COMPILE_FIRE = 'Cannot compile `fire`'; export function transformFire(fn: HIRFunction): void { const context = new Context(fn.env); replaceFireFunctions(fn, context); + if (!context.hasErrors()) { + ensureNoMoreFireUses(fn, context); + } context.throwIfErrorsFound(); } @@ -120,6 +125,11 @@ function replaceFireFunctions(fn: HIRFunction, context: Context): void { } rewriteInstrs.set(loadUseEffectInstrId, newInstrs); } + ensureNoRemainingCalleeCaptures( + lambda.loweredFunc.func, + context, + capturedCallees, + ); } } } else if ( @@ -159,7 +169,10 @@ function replaceFireFunctions(fn: HIRFunction, context: Context): void { } const fireFunctionBinding = - context.getOrGenerateFireFunctionBinding(loadLocal.place); + context.getOrGenerateFireFunctionBinding( + loadLocal.place, + value.loc, + ); loadLocal.place = {...fireFunctionBinding}; @@ -320,6 +333,69 @@ function visitFunctionExpressionAndPropagateFireDependencies( return calleesCapturedByFnExpression; } +/* + * eachInstructionOperand is not sufficient for our cases because: + * 1. fire is a global, which will not appear + * 2. The HIR may be malformed, so can't rely on function deps and must + * traverse the whole function. + */ +function* eachReachablePlace(fn: HIRFunction): Iterable { + for (const [, block] of fn.body.blocks) { + for (const instr of block.instructions) { + if ( + instr.value.kind === 'FunctionExpression' || + instr.value.kind === 'ObjectMethod' + ) { + yield* eachReachablePlace(instr.value.loweredFunc.func); + } else { + yield* eachInstructionOperand(instr); + } + } + } +} + +function ensureNoRemainingCalleeCaptures( + fn: HIRFunction, + context: Context, + capturedCallees: FireCalleesToFireFunctionBinding, +): void { + for (const place of eachReachablePlace(fn)) { + const calleeInfo = capturedCallees.get(place.identifier.id); + if (calleeInfo != null) { + const calleeName = + calleeInfo.capturedCalleeIdentifier.name?.kind === 'named' + ? calleeInfo.capturedCalleeIdentifier.name.value + : ''; + context.pushError({ + loc: place.loc, + description: `All uses of ${calleeName} must be either used with a fire() call in \ +this effect or not used with a fire() call at all. ${calleeName} was used with fire() on line \ +${printSourceLocationLine(calleeInfo.fireLoc)} in this effect`, + severity: ErrorSeverity.InvalidReact, + reason: CANNOT_COMPILE_FIRE, + suggestions: null, + }); + } + } +} + +function ensureNoMoreFireUses(fn: HIRFunction, context: Context): void { + for (const place of eachReachablePlace(fn)) { + if ( + place.identifier.type.kind === 'Function' && + place.identifier.type.shapeId === BuiltInFireId + ) { + context.pushError({ + loc: place.identifier.loc, + description: 'Cannot use `fire` outside of a useEffect function', + severity: ErrorSeverity.Invariant, + reason: CANNOT_COMPILE_FIRE, + suggestions: null, + }); + } + } +} + function makeLoadUseFireInstruction(env: Environment): Instruction { const useFirePlace = createTemporaryPlace(env, GeneratedSource); useFirePlace.effect = Effect.Read; @@ -422,6 +498,7 @@ type FireCalleesToFireFunctionBinding = Map< { fireFunctionBinding: Place; capturedCalleeIdentifier: Identifier; + fireLoc: SourceLocation; } >; @@ -523,8 +600,10 @@ class Context { getLoadLocalInstr(id: IdentifierId): LoadLocal | undefined { return this.#loadLocals.get(id); } - - getOrGenerateFireFunctionBinding(callee: Place): Place { + getOrGenerateFireFunctionBinding( + callee: Place, + fireLoc: SourceLocation, + ): Place { const fireFunctionBinding = getOrInsertWith( this.#fireCalleesToFireFunctions, callee.identifier.id, @@ -534,6 +613,7 @@ class Context { this.#capturedCalleeIdentifierIds.set(callee.identifier.id, { fireFunctionBinding, capturedCalleeIdentifier: callee.identifier, + fireLoc, }); return fireFunctionBinding; @@ -575,8 +655,12 @@ class Context { return this.#loadGlobalInstructionIds.get(id); } + hasErrors(): boolean { + return this.#errors.hasErrors(); + } + throwIfErrorsFound(): void { - if (this.#errors.hasErrors()) throw this.#errors; + if (this.hasErrors()) throw this.#errors; } } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/basic.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/basic.expect.md index a5bf42de7b008..8d8bc179a245a 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/basic.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/basic.expect.md @@ -21,7 +21,7 @@ function Component(props) { ## Code ```javascript -import { useFire } from "react"; +import { useFire } from "react/compiler-runtime"; import { c as _c } from "react/compiler-runtime"; // @enableFire import { fire } from "react"; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/deep-scope.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/deep-scope.expect.md index 585a820b13416..a335fea8867b9 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/deep-scope.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/deep-scope.expect.md @@ -30,7 +30,7 @@ function Component(props) { ## Code ```javascript -import { useFire } from "react"; +import { useFire } from "react/compiler-runtime"; import { c as _c } from "react/compiler-runtime"; // @enableFire import { fire } from "react"; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-mix-fire-and-no-fire.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-mix-fire-and-no-fire.expect.md new file mode 100644 index 0000000000000..e73451a896ee4 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-mix-fire-and-no-fire.expect.md @@ -0,0 +1,39 @@ + +## Input + +```javascript +// @enableFire +import {fire} from 'react'; + +function Component(props) { + const foo = props => { + console.log(props); + }; + useEffect(() => { + function nested() { + fire(foo(props)); + foo(props); + } + + nested(); + }); + + return null; +} + +``` + + +## Error + +``` + 9 | function nested() { + 10 | fire(foo(props)); +> 11 | foo(props); + | ^^^ InvalidReact: Cannot compile `fire`. All uses of foo must be either used with a fire() call in this effect or not used with a fire() call at all. foo was used with fire() on line 10:10 in this effect (11:11) + 12 | } + 13 | + 14 | nested(); +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-mix-fire-and-no-fire.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-mix-fire-and-no-fire.js new file mode 100644 index 0000000000000..ee2f915a34ed5 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-mix-fire-and-no-fire.js @@ -0,0 +1,18 @@ +// @enableFire +import {fire} from 'react'; + +function Component(props) { + const foo = props => { + console.log(props); + }; + useEffect(() => { + function nested() { + fire(foo(props)); + foo(props); + } + + nested(); + }); + + return null; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-outside-effect.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-outside-effect.expect.md new file mode 100644 index 0000000000000..687a21f98cdb4 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-outside-effect.expect.md @@ -0,0 +1,38 @@ + +## Input + +```javascript +// @enableFire +import {fire, useCallback} from 'react'; + +function Component({props, bar}) { + const foo = () => { + console.log(props); + }; + fire(foo(props)); + + useCallback(() => { + fire(foo(props)); + }, [foo, props]); + + return null; +} + +``` + + +## Error + +``` + 6 | console.log(props); + 7 | }; +> 8 | fire(foo(props)); + | ^^^^ Invariant: Cannot compile `fire`. Cannot use `fire` outside of a useEffect function (8:8) + +Invariant: Cannot compile `fire`. Cannot use `fire` outside of a useEffect function (11:11) + 9 | + 10 | useCallback(() => { + 11 | fire(foo(props)); +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-outside-effect.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-outside-effect.js new file mode 100644 index 0000000000000..8ac9be6d7648f --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-outside-effect.js @@ -0,0 +1,15 @@ +// @enableFire +import {fire, useCallback} from 'react'; + +function Component({props, bar}) { + const foo = () => { + console.log(props); + }; + fire(foo(props)); + + useCallback(() => { + fire(foo(props)); + }, [foo, props]); + + return null; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/multiple-scope.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/multiple-scope.expect.md index 0d531041355be..02f3935171253 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/multiple-scope.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/multiple-scope.expect.md @@ -29,7 +29,7 @@ function Component(props) { ## Code ```javascript -import { useFire } from "react"; +import { useFire } from "react/compiler-runtime"; import { c as _c } from "react/compiler-runtime"; // @enableFire import { fire } from "react"; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/repeated-calls.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/repeated-calls.expect.md index 3eb9f0e71c71a..1734ca3ab4584 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/repeated-calls.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/repeated-calls.expect.md @@ -22,7 +22,7 @@ function Component(props) { ## Code ```javascript -import { useFire } from "react"; +import { useFire } from "react/compiler-runtime"; import { c as _c } from "react/compiler-runtime"; // @enableFire import { fire } from "react"; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/shared-hook-calls.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/shared-hook-calls.expect.md index 4362a3a8461e6..9b689b31c7ba0 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/shared-hook-calls.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/shared-hook-calls.expect.md @@ -26,7 +26,7 @@ function Component({bar, baz}) { ## Code ```javascript -import { useFire } from "react"; +import { useFire } from "react/compiler-runtime"; import { c as _c } from "react/compiler-runtime"; // @enableFire import { fire } from "react"; From 6907aa2a309bdc47dc3504683159cb50b590eed8 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Fri, 20 Dec 2024 17:16:59 -0500 Subject: [PATCH 0011/1160] [compiler] Rewrite effect dep arrays that use fire (#31811) If an effect uses a dep array, also rewrite the dep array to use the fire binding -- --- .../src/Transform/TransformFire.ts | 73 +++++++++++++++++-- ...id-rewrite-deps-no-array-literal.expect.md | 37 ++++++++++ ...r.invalid-rewrite-deps-no-array-literal.js | 16 ++++ ...rror.invalid-rewrite-deps-spread.expect.md | 37 ++++++++++ .../error.invalid-rewrite-deps-spread.js | 19 +++++ .../fire-and-autodeps.expect.md | 60 +++++++++++++++ .../transform-fire/fire-and-autodeps.js | 13 ++++ .../transform-fire/rewrite-deps.expect.md | 57 +++++++++++++++ .../compiler/transform-fire/rewrite-deps.js | 13 ++++ 9 files changed, 320 insertions(+), 5 deletions(-) create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-rewrite-deps-no-array-literal.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-rewrite-deps-no-array-literal.js create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-rewrite-deps-spread.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-rewrite-deps-spread.js create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/fire-and-autodeps.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/fire-and-autodeps.js create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/rewrite-deps.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/rewrite-deps.js diff --git a/compiler/packages/babel-plugin-react-compiler/src/Transform/TransformFire.ts b/compiler/packages/babel-plugin-react-compiler/src/Transform/TransformFire.ts index a0256fd80cd66..a35c4ddb0182c 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Transform/TransformFire.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Transform/TransformFire.ts @@ -12,6 +12,7 @@ import { SourceLocation, } from '..'; import { + ArrayExpression, CallExpression, Effect, Environment, @@ -38,7 +39,6 @@ import {printSourceLocationLine} from '../HIR/PrintHIR'; /* * TODO(jmbrown): - * - rewrite dep arrays * - traverse object methods * - method calls * - React.useEffect calls @@ -125,11 +125,58 @@ function replaceFireFunctions(fn: HIRFunction, context: Context): void { } rewriteInstrs.set(loadUseEffectInstrId, newInstrs); } - ensureNoRemainingCalleeCaptures( - lambda.loweredFunc.func, - context, - capturedCallees, + } + ensureNoRemainingCalleeCaptures( + lambda.loweredFunc.func, + context, + capturedCallees, + ); + + if ( + value.args.length > 1 && + value.args[1] != null && + value.args[1].kind === 'Identifier' + ) { + const depArray = value.args[1]; + const depArrayExpression = context.getArrayExpression( + depArray.identifier.id, ); + if (depArrayExpression != null) { + for (const dependency of depArrayExpression.elements) { + if (dependency.kind === 'Identifier') { + const loadOfDependency = context.getLoadLocalInstr( + dependency.identifier.id, + ); + if (loadOfDependency != null) { + const replacedDepArrayItem = capturedCallees.get( + loadOfDependency.place.identifier.id, + ); + if (replacedDepArrayItem != null) { + loadOfDependency.place = + replacedDepArrayItem.fireFunctionBinding; + } + } + } + } + } else { + context.pushError({ + loc: value.args[1].loc, + description: + 'You must use an array literal for an effect dependency array when that effect uses `fire()`', + severity: ErrorSeverity.Invariant, + reason: CANNOT_COMPILE_FIRE, + suggestions: null, + }); + } + } else if (value.args.length > 1 && value.args[1].kind === 'Spread') { + context.pushError({ + loc: value.args[1].place.loc, + description: + 'You must use an array literal for an effect dependency array when that effect uses `fire()`', + severity: ErrorSeverity.Invariant, + reason: CANNOT_COMPILE_FIRE, + suggestions: null, + }); } } } else if ( @@ -231,6 +278,8 @@ function replaceFireFunctions(fn: HIRFunction, context: Context): void { deleteInstrs.add(instr.id); } else if (value.kind === 'LoadGlobal') { context.addLoadGlobalInstrId(lvalue.identifier.id, instr.id); + } else if (value.kind === 'ArrayExpression') { + context.addArrayExpression(lvalue.identifier.id, value); } } block.instructions = rewriteInstructions(rewriteInstrs, block.instructions); @@ -561,6 +610,12 @@ class Context { this.#env = env; } + /* + * We keep track of array expressions so we can rewrite dependency arrays passed to useEffect + * to use the fire functions + */ + #arrayExpressions = new Map(); + pushError(error: CompilerErrorDetailOptions): void { this.#errors.push(error); } @@ -655,6 +710,14 @@ class Context { return this.#loadGlobalInstructionIds.get(id); } + addArrayExpression(id: IdentifierId, array: ArrayExpression): void { + this.#arrayExpressions.set(id, array); + } + + getArrayExpression(id: IdentifierId): ArrayExpression | undefined { + return this.#arrayExpressions.get(id); + } + hasErrors(): boolean { return this.#errors.hasErrors(); } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-rewrite-deps-no-array-literal.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-rewrite-deps-no-array-literal.expect.md new file mode 100644 index 0000000000000..dcd9312bb2e53 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-rewrite-deps-no-array-literal.expect.md @@ -0,0 +1,37 @@ + +## Input + +```javascript +// @enableFire +import {fire} from 'react'; + +function Component(props) { + const foo = props => { + console.log(props); + }; + + const deps = [foo, props]; + + useEffect(() => { + fire(foo(props)); + }, deps); + + return null; +} + +``` + + +## Error + +``` + 11 | useEffect(() => { + 12 | fire(foo(props)); +> 13 | }, deps); + | ^^^^ Invariant: Cannot compile `fire`. You must use an array literal for an effect dependency array when that effect uses `fire()` (13:13) + 14 | + 15 | return null; + 16 | } +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-rewrite-deps-no-array-literal.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-rewrite-deps-no-array-literal.js new file mode 100644 index 0000000000000..b82f735425efa --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-rewrite-deps-no-array-literal.js @@ -0,0 +1,16 @@ +// @enableFire +import {fire} from 'react'; + +function Component(props) { + const foo = props => { + console.log(props); + }; + + const deps = [foo, props]; + + useEffect(() => { + fire(foo(props)); + }, deps); + + return null; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-rewrite-deps-spread.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-rewrite-deps-spread.expect.md new file mode 100644 index 0000000000000..7c1b55f61d909 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-rewrite-deps-spread.expect.md @@ -0,0 +1,37 @@ + +## Input + +```javascript +// @enableFire +import {fire} from 'react'; + +function Component(props) { + const foo = props => { + console.log(props); + }; + + const deps = [foo, props]; + + useEffect(() => { + fire(foo(props)); + }, ...deps); + + return null; +} + +``` + + +## Error + +``` + 11 | useEffect(() => { + 12 | fire(foo(props)); +> 13 | }, ...deps); + | ^^^^ Invariant: Cannot compile `fire`. You must use an array literal for an effect dependency array when that effect uses `fire()` (13:13) + 14 | + 15 | return null; + 16 | } +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-rewrite-deps-spread.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-rewrite-deps-spread.js new file mode 100644 index 0000000000000..27d1de4f463d6 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-rewrite-deps-spread.js @@ -0,0 +1,19 @@ +// @enableFire +import {fire} from 'react'; + +function Component(props) { + const foo = props => { + console.log(props); + }; + + const deps = [foo, props]; + + useEffect( + () => { + fire(foo(props)); + }, + ...deps + ); + + return null; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/fire-and-autodeps.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/fire-and-autodeps.expect.md new file mode 100644 index 0000000000000..5767ff0746c1b --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/fire-and-autodeps.expect.md @@ -0,0 +1,60 @@ + +## Input + +```javascript +// @enableFire @inferEffectDependencies +import {fire, useEffect} from 'react'; + +function Component(props) { + const foo = arg => { + console.log(arg, props.bar); + }; + useEffect(() => { + fire(foo(props)); + }); + + return null; +} + +``` + +## Code + +```javascript +import { useFire } from "react/compiler-runtime"; +import { c as _c } from "react/compiler-runtime"; // @enableFire @inferEffectDependencies +import { fire, useEffect } from "react"; + +function Component(props) { + const $ = _c(5); + let t0; + if ($[0] !== props.bar) { + t0 = (arg) => { + console.log(arg, props.bar); + }; + $[0] = props.bar; + $[1] = t0; + } else { + t0 = $[1]; + } + const foo = t0; + const t1 = useFire(foo); + let t2; + if ($[2] !== props || $[3] !== t1) { + t2 = () => { + t1(props); + }; + $[2] = props; + $[3] = t1; + $[4] = t2; + } else { + t2 = $[4]; + } + useEffect(t2, [t1, props]); + return null; +} + +``` + +### Eval output +(kind: exception) Fixture not implemented \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/fire-and-autodeps.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/fire-and-autodeps.js new file mode 100644 index 0000000000000..e2a0068a19067 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/fire-and-autodeps.js @@ -0,0 +1,13 @@ +// @enableFire @inferEffectDependencies +import {fire, useEffect} from 'react'; + +function Component(props) { + const foo = arg => { + console.log(arg, props.bar); + }; + useEffect(() => { + fire(foo(props)); + }); + + return null; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/rewrite-deps.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/rewrite-deps.expect.md new file mode 100644 index 0000000000000..ae71f60393281 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/rewrite-deps.expect.md @@ -0,0 +1,57 @@ + +## Input + +```javascript +// @enableFire +import {fire} from 'react'; + +function Component(props) { + const foo = props => { + console.log(props); + }; + useEffect(() => { + fire(foo(props)); + }, [foo, props]); + + return null; +} + +``` + +## Code + +```javascript +import { useFire } from "react/compiler-runtime"; +import { c as _c } from "react/compiler-runtime"; // @enableFire +import { fire } from "react"; + +function Component(props) { + const $ = _c(4); + const foo = _temp; + const t0 = useFire(foo); + let t1; + let t2; + if ($[0] !== props || $[1] !== t0) { + t1 = () => { + t0(props); + }; + t2 = [t0, props]; + $[0] = props; + $[1] = t0; + $[2] = t1; + $[3] = t2; + } else { + t1 = $[2]; + t2 = $[3]; + } + useEffect(t1, t2); + return null; +} +function _temp(props_0) { + console.log(props_0); +} + +``` + +### Eval output +(kind: exception) Fixture not implemented \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/rewrite-deps.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/rewrite-deps.js new file mode 100644 index 0000000000000..ad1af704c1bcc --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/rewrite-deps.js @@ -0,0 +1,13 @@ +// @enableFire +import {fire} from 'react'; + +function Component(props) { + const foo = props => { + console.log(props); + }; + useEffect(() => { + fire(foo(props)); + }, [foo, props]); + + return null; +} From 94867f33be327a52bfffda89a14c85897180e43e Mon Sep 17 00:00:00 2001 From: Ricky Date: Mon, 23 Dec 2024 14:58:20 -0500 Subject: [PATCH 0012/1160] [asserts helpers] react package (#31853) Based off https://github.com/facebook/react/pull/31844 Commit to review: https://github.com/facebook/react/commit/11aa104e3e70c0accc21f785060b812beb145089 Converts the rest of the `react` package. --- .../src/__tests__/ReactLazy-test.internal.js | 52 +- .../ReactCoffeeScriptClass-test.coffee | 217 ++++---- .../__tests__/ReactContextValidator-test.js | 313 ++++++------ .../src/__tests__/ReactCreateElement-test.js | 66 ++- .../src/__tests__/ReactCreateRef-test.js | 40 +- .../react/src/__tests__/ReactES6Class-test.js | 159 ++++-- .../src/__tests__/ReactElementClone-test.js | 29 +- .../ReactElementValidator-test.internal.js | 321 +++++++----- .../ReactJSXElementValidator-test.js | 167 +++--- .../src/__tests__/ReactJSXRuntime-test.js | 103 ++-- .../ReactJSXTransformIntegration-test.js | 12 +- .../ReactProfilerComponent-test.internal.js | 15 +- .../src/__tests__/ReactPureComponent-test.js | 38 +- .../__tests__/ReactStartTransition-test.js | 29 +- .../src/__tests__/ReactStrictMode-test.js | 112 ++-- .../__tests__/ReactTypeScriptClass-test.ts | 282 ++++++----- .../createReactClassIntegration-test.js | 479 ++++++++++-------- .../spec-equivalence-reporter/setupTests.js | 10 + 18 files changed, 1431 insertions(+), 1013 deletions(-) diff --git a/packages/react-reconciler/src/__tests__/ReactLazy-test.internal.js b/packages/react-reconciler/src/__tests__/ReactLazy-test.internal.js index ddcd751957100..1b665b2b8bc7f 100644 --- a/packages/react-reconciler/src/__tests__/ReactLazy-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactLazy-test.internal.js @@ -233,8 +233,28 @@ describe('ReactLazy', () => { expect(error.message).toMatch('Element type is invalid'); assertLog(['Loading...']); assertConsoleErrorDev([ - 'Expected the result of a dynamic import() call', - 'Expected the result of a dynamic import() call', + 'lazy: Expected the result of a dynamic import() call. ' + + 'Instead received: function Text(props) {\n' + + ' Scheduler.log(props.text);\n' + + ' return props.text;\n' + + ' }\n\n' + + 'Your code should look like: \n ' + + "const MyComponent = lazy(() => import('./MyComponent'))\n" + + (gate('enableOwnerStacks') + ? '' + : ' in Lazy (at **)\n' + ' in Suspense (at **)\n') + + ' in App (at **)', + 'lazy: Expected the result of a dynamic import() call. ' + + 'Instead received: function Text(props) {\n' + + ' Scheduler.log(props.text);\n' + + ' return props.text;\n' + + ' }\n\n' + + 'Your code should look like: \n ' + + "const MyComponent = lazy(() => import('./MyComponent'))\n" + + (gate('enableOwnerStacks') + ? '' + : ' in Lazy (at **)\n' + ' in Suspense (at **)\n') + + ' in App (at **)', ]); expect(root).not.toMatchRenderedOutput('Hi'); }); @@ -852,19 +872,21 @@ describe('ReactLazy', () => { expect(root).not.toMatchRenderedOutput('22'); // Mount - await expect(async () => { - await act(() => resolveFakeImport(Add)); - }).toErrorDev( - shouldWarnAboutFunctionDefaultProps - ? [ - 'Add: Support for defaultProps will be removed from function components in a future major release. Use JavaScript default parameters instead.', - ] - : shouldWarnAboutMemoDefaultProps - ? [ - 'Add: Support for defaultProps will be removed from memo components in a future major release. Use JavaScript default parameters instead.', - ] - : [], - ); + await act(() => resolveFakeImport(Add)); + + if (shouldWarnAboutFunctionDefaultProps) { + assertConsoleErrorDev([ + 'Add: Support for defaultProps will be removed from function components in a future major release. Use JavaScript default parameters instead.\n' + + ' in Add (at **)\n' + + ' in Suspense (at **)', + ]); + } else if (shouldWarnAboutMemoDefaultProps) { + assertConsoleErrorDev([ + 'Add: Support for defaultProps will be removed from memo components in a future major release. Use JavaScript default parameters instead.\n' + + ' in Suspense (at **)', + ]); + } + expect(root).toMatchRenderedOutput('22'); // Update diff --git a/packages/react/src/__tests__/ReactCoffeeScriptClass-test.coffee b/packages/react/src/__tests__/ReactCoffeeScriptClass-test.coffee index 3ad6aa0235188..5fab9d7d0bbde 100644 --- a/packages/react/src/__tests__/ReactCoffeeScriptClass-test.coffee +++ b/packages/react/src/__tests__/ReactCoffeeScriptClass-test.coffee @@ -9,6 +9,8 @@ PropTypes = null React = null ReactDOM = null ReactDOMClient = null +assertConsoleErrorDev = null +assertConsoleWarnDev = null featureFlags = require 'shared/ReactFeatureFlags' @@ -28,6 +30,9 @@ describe 'ReactCoffeeScriptClass', -> root = ReactDOMClient.createRoot container attachedListener = null renderedName = null + TestUtils = require 'internal-test-utils' + assertConsoleErrorDev = TestUtils.assertConsoleErrorDev + assertConsoleWarnDev = TestUtils.assertConsoleWarnDev InnerComponent = class extends React.Component getName: -> this.props.name render: -> @@ -53,14 +58,15 @@ describe 'ReactCoffeeScriptClass', -> event.preventDefault() caughtErrors.push(event.error) window.addEventListener 'error', errorHandler; - expect(-> - ReactDOM.flushSync -> - root.render React.createElement(Foo) - ).toErrorDev([ - # A failed component renders twice in DEV in concurrent mode - 'No `render` method found on the Foo instance', - 'No `render` method found on the Foo instance', - ]) + ReactDOM.flushSync -> + root.render React.createElement(Foo) + assertConsoleErrorDev [ +# A failed component renders twice in DEV in concurrent mode + 'No `render` method found on the Foo instance: you may have forgotten to define `render`.\n' + + ' in Foo (at **)', + 'No `render` method found on the Foo instance: you may have forgotten to define `render`.\n' + + ' in Foo (at **)', + ] window.removeEventListener 'error', errorHandler; expect(caughtErrors).toEqual([ expect.objectContaining( @@ -136,11 +142,11 @@ describe 'ReactCoffeeScriptClass', -> React.createElement('div') getDerivedStateFromProps: -> {} - expect(-> - ReactDOM.flushSync -> - root.render React.createElement(Foo, foo: 'foo') - return - ).toErrorDev 'Foo: getDerivedStateFromProps() is defined as an instance method and will be ignored. Instead, declare it as a static method.' + ReactDOM.flushSync -> + root.render React.createElement(Foo, foo: 'foo') + assertConsoleErrorDev [ + 'Foo: getDerivedStateFromProps() is defined as an instance method and will be ignored. Instead, declare it as a static method.\n' + + ' in Foo (at **)'] it 'warns if getDerivedStateFromError is not static', -> class Foo extends React.Component @@ -148,11 +154,13 @@ describe 'ReactCoffeeScriptClass', -> React.createElement('div') getDerivedStateFromError: -> {} - expect(-> - ReactDOM.flushSync -> - root.render React.createElement(Foo, foo: 'foo') - return - ).toErrorDev 'Foo: getDerivedStateFromError() is defined as an instance method and will be ignored. Instead, declare it as a static method.' + ReactDOM.flushSync -> + root.render React.createElement(Foo, foo: 'foo') + + assertConsoleErrorDev [ + 'Foo: getDerivedStateFromError() is defined as an instance method and will be ignored. Instead, declare it as a static method.\n' + + ' in Foo (at **)' + ] it 'warns if getSnapshotBeforeUpdate is static', -> class Foo extends React.Component @@ -160,11 +168,13 @@ describe 'ReactCoffeeScriptClass', -> React.createElement('div') Foo.getSnapshotBeforeUpdate = () -> {} - expect(-> - ReactDOM.flushSync -> - root.render React.createElement(Foo, foo: 'foo') - return - ).toErrorDev 'Foo: getSnapshotBeforeUpdate() is defined as a static method and will be ignored. Instead, declare it as an instance method.' + ReactDOM.flushSync -> + root.render React.createElement(Foo, foo: 'foo') + + assertConsoleErrorDev [ + 'Foo: getSnapshotBeforeUpdate() is defined as a static method and will be ignored. Instead, declare it as an instance method.\n' + + ' in Foo (at **)' + ] it 'warns if state not initialized before static getDerivedStateFromProps', -> class Foo extends React.Component @@ -177,16 +187,16 @@ describe 'ReactCoffeeScriptClass', -> foo: nextProps.foo bar: 'bar' } - expect(-> - ReactDOM.flushSync -> - root.render React.createElement(Foo, foo: 'foo') - return - ).toErrorDev ( - '`Foo` uses `getDerivedStateFromProps` but its initial state is ' + - 'undefined. This is not recommended. Instead, define the initial state by ' + - 'assigning an object to `this.state` in the constructor of `Foo`. ' + - 'This ensures that `getDerivedStateFromProps` arguments have a consistent shape.' - ) + ReactDOM.flushSync -> + root.render React.createElement(Foo, foo: 'foo') + + assertConsoleErrorDev [ + '`Foo` uses `getDerivedStateFromProps` but its initial state is + undefined. This is not recommended. Instead, define the initial state by + assigning an object to `this.state` in the constructor of `Foo`. + This ensures that `getDerivedStateFromProps` arguments have a consistent shape.\n' + + ' in Foo (at **)' + ] it 'updates initial state with values returned by static getDerivedStateFromProps', -> class Foo extends React.Component @@ -254,12 +264,28 @@ describe 'ReactCoffeeScriptClass', -> render: -> React.createElement Foo - expect(-> - test React.createElement(Outer), 'SPAN', 'foo' - ).toErrorDev([ - 'Outer uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.', - 'Foo uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.', - ]) + test React.createElement(Outer), 'SPAN', 'foo' + + if featureFlags.enableOwnerStacks + assertConsoleErrorDev([ + 'Outer uses the legacy childContextTypes API which will soon be removed. + Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' + + ' in Outer (at **)', + 'Foo uses the legacy contextTypes API which will soon be removed. + Use React.createContext() with static contextType instead. (https://react.dev/link/legacy-context)\n' + + ' in Outer (at **)', + ]); + else + assertConsoleErrorDev([ + 'Outer uses the legacy childContextTypes API which will soon be removed. + Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' + + ' in Outer (at **)', + 'Foo uses the legacy contextTypes API which will soon be removed. + Use React.createContext() with static contextType instead. (https://react.dev/link/legacy-context)\n' + + ' in Foo (at **)\n' + + ' in Outer (at **)', + ]); + it 'renders only once when setting state in componentWillMount', -> renderCount = 0 @@ -286,9 +312,11 @@ describe 'ReactCoffeeScriptClass', -> render: -> React.createElement('span') - expect(-> - test React.createElement(Foo), 'SPAN', '' - ).toErrorDev('Foo.state: must be set to an object or null') + test React.createElement(Foo), 'SPAN', '' + assertConsoleErrorDev [ + 'Foo.state: must be set to an object or null\n' + + ' in Foo (at **)' + ] it 'should render with null in the initial state property', -> class Foo extends React.Component @@ -430,14 +458,21 @@ describe 'ReactCoffeeScriptClass', -> className: 'foo' ) - expect(-> - test React.createElement(Foo), 'SPAN', 'foo' - ).toErrorDev([ - 'getInitialState was defined on Foo, a plain JavaScript class.', - 'getDefaultProps was defined on Foo, a plain JavaScript class.', - 'contextTypes was defined as an instance property on Foo.', - 'contextType was defined as an instance property on Foo.', - ]) + test React.createElement(Foo), 'SPAN', 'foo' + assertConsoleErrorDev [ + 'getInitialState was defined on Foo, a plain JavaScript class. + This is only supported for classes created using React.createClass. + Did you mean to define a state property instead?\n' + + ' in Foo (at **)', + 'getDefaultProps was defined on Foo, a plain JavaScript class. + This is only supported for classes created using React.createClass. + Use a static property to define defaultProps instead.\n' + + ' in Foo (at **)', + 'contextType was defined as an instance property on Foo. Use a static property to define contextType instead.\n' + + ' in Foo (at **)', + 'contextTypes was defined as an instance property on Foo. Use a static property to define contextTypes instead.\n' + + ' in Foo (at **)', + ] expect(getInitialStateWasCalled).toBe false expect(getDefaultPropsWasCalled).toBe false @@ -468,13 +503,13 @@ describe 'ReactCoffeeScriptClass', -> className: 'foo' ) - expect(-> - test React.createElement(NamedComponent), 'SPAN', 'foo' - ).toErrorDev( + test React.createElement(NamedComponent), 'SPAN', 'foo' + assertConsoleErrorDev [ 'NamedComponent has a method called componentShouldUpdate(). Did you mean shouldComponentUpdate()? The name is phrased as a - question because the function is expected to return a value.' - ) + question because the function is expected to return a value.\n' + + ' in NamedComponent (at **)' + ] it 'should warn when misspelling componentWillReceiveProps', -> class NamedComponent extends React.Component @@ -486,12 +521,12 @@ describe 'ReactCoffeeScriptClass', -> className: 'foo' ) - expect(-> - test React.createElement(NamedComponent), 'SPAN', 'foo' - ).toErrorDev( + test React.createElement(NamedComponent), 'SPAN', 'foo' + assertConsoleErrorDev [ 'NamedComponent has a method called componentWillRecieveProps(). - Did you mean componentWillReceiveProps()?' - ) + Did you mean componentWillReceiveProps()?\n' + + ' in NamedComponent (at **)' + ] it 'should warn when misspelling UNSAFE_componentWillReceiveProps', -> class NamedComponent extends React.Component @@ -503,28 +538,28 @@ describe 'ReactCoffeeScriptClass', -> className: 'foo' ) - expect(-> - test React.createElement(NamedComponent), 'SPAN', 'foo' - ).toErrorDev( + test React.createElement(NamedComponent), 'SPAN', 'foo' + assertConsoleErrorDev [ 'NamedComponent has a method called UNSAFE_componentWillRecieveProps(). - Did you mean UNSAFE_componentWillReceiveProps()?' - ) + Did you mean UNSAFE_componentWillReceiveProps()?\n' + + ' in NamedComponent (at **)' + ] it 'should throw AND warn when trying to access classic APIs', -> ref = React.createRef() test React.createElement(InnerComponent, name: 'foo', ref: ref), 'DIV', 'foo' - expect(-> - expect(-> ref.current.replaceState {}).toThrow() - ).toWarnDev( - 'replaceState(...) is deprecated in plain JavaScript React classes', - {withoutStack: true} - ) - expect(-> - expect(-> ref.current.isMounted()).toThrow() - ).toWarnDev( - 'isMounted(...) is deprecated in plain JavaScript React classes', - {withoutStack: true} - ) + + expect(-> ref.current.replaceState {}).toThrow() + assertConsoleWarnDev([ + 'replaceState(...) is deprecated in plain JavaScript React classes. + Refactor your code to use setState instead (see https://github.com/facebook/react/issues/3236).' + ], {withoutStack: true}) + + expect(-> ref.current.isMounted()).toThrow() + assertConsoleWarnDev([ + 'isMounted(...) is deprecated in plain JavaScript React classes. + Instead, make sure to clean up subscriptions and pending requests in componentWillUnmount to prevent memory leaks.', + ], {withoutStack: true}) if !featureFlags.disableLegacyContext it 'supports this.context passed via getChildContext', -> @@ -542,13 +577,25 @@ describe 'ReactCoffeeScriptClass', -> render: -> React.createElement Bar - expect(-> - test React.createElement(Foo), 'DIV', 'bar-through-context' - ).toErrorDev( - [ - 'Foo uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.', - 'Bar uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.', - ], - ) + test React.createElement(Foo), 'DIV', 'bar-through-context' + if featureFlags.enableOwnerStacks + assertConsoleErrorDev [ + 'Foo uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead. + (https://react.dev/link/legacy-context)\n' + + ' in Foo (at **)', + 'Bar uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead. + (https://react.dev/link/legacy-context)\n' + + ' in Foo (at **)' + ] + else + assertConsoleErrorDev [ + 'Foo uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead. + (https://react.dev/link/legacy-context)\n' + + ' in Foo (at **)', + 'Bar uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead. + (https://react.dev/link/legacy-context)\n' + + ' in Bar (at **)\n' + + ' in Foo (at **)' + ] undefined diff --git a/packages/react/src/__tests__/ReactContextValidator-test.js b/packages/react/src/__tests__/ReactContextValidator-test.js index 359857af15c61..66597f0dd52d0 100644 --- a/packages/react/src/__tests__/ReactContextValidator-test.js +++ b/packages/react/src/__tests__/ReactContextValidator-test.js @@ -19,6 +19,7 @@ let PropTypes; let React; let ReactDOMClient; let act; +let assertConsoleErrorDev; describe('ReactContextValidator', () => { beforeEach(() => { @@ -27,7 +28,7 @@ describe('ReactContextValidator', () => { PropTypes = require('prop-types'); React = require('react'); ReactDOMClient = require('react-dom/client'); - act = require('internal-test-utils').act; + ({act, assertConsoleErrorDev} = require('internal-test-utils')); }); // TODO: This behavior creates a runtime dependency on propTypes. We should @@ -66,15 +67,20 @@ describe('ReactContextValidator', () => { let instance; const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); - await expect(async () => { - await act(() => { - root.render( - (instance = current)} />, - ); - }); - }).toErrorDev([ - 'ComponentInFooBarContext uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.', - 'Component uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.', + await act(() => { + root.render( + (instance = current)} />, + ); + }); + assertConsoleErrorDev([ + 'ComponentInFooBarContext uses the legacy childContextTypes API which will soon be removed. ' + + 'Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' + + ' in ComponentInFooBarContext (at **)', + 'Component uses the legacy contextTypes API which will soon be removed. ' + + 'Use React.createContext() with static contextType instead. (https://react.dev/link/legacy-context)\n' + + (gate(flags => flags.enableOwnerStacks) + ? ' in ComponentInFooBarContext (at **)' + : ' in Component (at **)'), ]); expect(instance.childRef.current.context).toEqual({foo: 'abc'}); }); @@ -144,13 +150,18 @@ describe('ReactContextValidator', () => { const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); - await expect(async () => { - await act(() => { - root.render(); - }); - }).toErrorDev([ - 'Parent uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.', - 'Component uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.', + await act(() => { + root.render(); + }); + assertConsoleErrorDev([ + 'Parent uses the legacy childContextTypes API which will soon be removed. ' + + 'Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' + + ' in Parent (at **)', + 'Component uses the legacy contextTypes API which will soon be removed. ' + + 'Use React.createContext() with static contextType instead. (https://react.dev/link/legacy-context)\n' + + (gate(flags => flags.enableOwnerStacks) + ? ' in Parent (at **)' + : ' in Component (at **)'), ]); expect(constructorContext).toEqual({foo: 'abc'}); @@ -191,33 +202,33 @@ describe('ReactContextValidator', () => { } } - await expect(async () => { - const container = document.createElement('div'); - const root = ReactDOMClient.createRoot(container); - await act(() => { - root.render(); - }); - }).toErrorDev([ - 'ComponentA uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.', - 'ComponentA.childContextTypes is specified but there is no getChildContext() method on the instance. You can either define getChildContext() on ComponentA or remove childContextTypes from it.', + const root = ReactDOMClient.createRoot(document.createElement('div')); + await act(() => { + root.render(); + }); + assertConsoleErrorDev([ + 'ComponentA uses the legacy childContextTypes API which will soon be removed. ' + + 'Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' + + ' in ComponentA (at **)', + 'ComponentA.childContextTypes is specified but there is no getChildContext() method on the instance. ' + + 'You can either define getChildContext() on ComponentA or remove childContextTypes from it.\n' + + ' in ComponentA (at **)', ]); // Warnings should be deduped by component type - let container = document.createElement('div'); - let root = ReactDOMClient.createRoot(container); await act(() => { root.render(); }); - - await expect(async () => { - container = document.createElement('div'); - root = ReactDOMClient.createRoot(container); - await act(() => { - root.render(); - }); - }).toErrorDev([ - 'ComponentB uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.', - 'ComponentB.childContextTypes is specified but there is no getChildContext() method on the instance. You can either define getChildContext() on ComponentB or remove childContextTypes from it.', + await act(() => { + root.render(); + }); + assertConsoleErrorDev([ + 'ComponentB uses the legacy childContextTypes API which will soon be removed. ' + + 'Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' + + ' in ComponentB (at **)', + 'ComponentB.childContextTypes is specified but there is no getChildContext() method on the instance. ' + + 'You can either define getChildContext() on ComponentB or remove childContextTypes from it.\n' + + ' in ComponentB (at **)', ]); }); @@ -260,17 +271,34 @@ describe('ReactContextValidator', () => { foo: PropTypes.string.isRequired, }; - await expect(async () => { - const container = document.createElement('div'); - const root = ReactDOMClient.createRoot(container); - await act(() => { - root.render(); - }); - }).toErrorDev([ - 'ParentContextProvider uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.', - 'MiddleMissingContext uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.', - 'MiddleMissingContext.childContextTypes is specified but there is no getChildContext() method on the instance. You can either define getChildContext() on MiddleMissingContext or remove childContextTypes from it.', - 'ChildContextConsumer uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.', + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render(); + }); + assertConsoleErrorDev([ + 'ParentContextProvider uses the legacy childContextTypes API which will soon be removed. ' + + 'Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' + + ' in ParentContextProvider (at **)', + 'MiddleMissingContext uses the legacy childContextTypes API which will soon be removed. ' + + 'Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' + + (gate(flags => flags.enableOwnerStacks) + ? '' + : ' in MiddleMissingContext (at **)\n') + + ' in ParentContextProvider (at **)', + 'MiddleMissingContext.childContextTypes is specified but there is no getChildContext() method on the instance. ' + + 'You can either define getChildContext() on MiddleMissingContext or remove childContextTypes from it.\n' + + (gate(flags => flags.enableOwnerStacks) + ? '' + : ' in MiddleMissingContext (at **)\n') + + ' in ParentContextProvider (at **)', + 'ChildContextConsumer uses the legacy contextTypes API which will soon be removed. ' + + 'Use React.createContext() with static contextType instead. (https://react.dev/link/legacy-context)\n' + + (gate(flags => flags.enableOwnerStacks) + ? '' + : ' in ChildContextConsumer (at **)\n') + + ' in MiddleMissingContext (at **)\n' + + ' in ParentContextProvider (at **)', ]); expect(childContext.bar).toBeUndefined(); expect(childContext.foo).toBe('FOO'); @@ -428,25 +456,28 @@ describe('ReactContextValidator', () => { } } - await expect(async () => { - const container = document.createElement('div'); - const root = ReactDOMClient.createRoot(container); - await act(() => { - root.render( - - - , - ); - }); - }).toErrorDev([ - 'ParentContextProvider uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead', - 'ComponentA uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.', - 'ComponentA declares both contextTypes and contextType static properties. The legacy contextTypes property will be ignored.', + const root = ReactDOMClient.createRoot(document.createElement('div')); + await act(() => { + root.render( + + + , + ); + }); + + assertConsoleErrorDev([ + 'ParentContextProvider uses the legacy childContextTypes API which will soon be removed. ' + + 'Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' + + ' in ParentContextProvider (at **)', + 'ComponentA declares both contextTypes and contextType static properties. ' + + 'The legacy contextTypes property will be ignored.\n' + + ' in ComponentA (at **)', + 'ComponentA uses the legacy contextTypes API which will soon be removed. ' + + 'Use React.createContext() with static contextType instead. (https://react.dev/link/legacy-context)\n' + + ' in ComponentA (at **)', ]); // Warnings should be deduped by component type - let container = document.createElement('div'); - let root = ReactDOMClient.createRoot(container); await act(() => { root.render( @@ -455,19 +486,20 @@ describe('ReactContextValidator', () => { ); }); - await expect(async () => { - container = document.createElement('div'); - root = ReactDOMClient.createRoot(container); - await act(() => { - root.render( - - - , - ); - }); - }).toErrorDev([ - 'ComponentB declares both contextTypes and contextType static properties. The legacy contextTypes property will be ignored.', - 'ComponentB uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.', + await act(() => { + root.render( + + + , + ); + }); + assertConsoleErrorDev([ + 'ComponentB declares both contextTypes and contextType static properties. ' + + 'The legacy contextTypes property will be ignored.\n' + + ' in ComponentB (at **)', + 'ComponentB uses the legacy contextTypes API which will soon be removed. ' + + 'Use React.createContext() with static contextType instead. (https://react.dev/link/legacy-context)\n' + + ' in ComponentB (at **)', ]); }); @@ -481,20 +513,17 @@ describe('ReactContextValidator', () => { } } - await expect(async () => { - const container = document.createElement('div'); - const root = ReactDOMClient.createRoot(container); - await act(() => { - root.render(); - }); - }).toErrorDev( + const root = ReactDOMClient.createRoot(document.createElement('div')); + await act(() => { + root.render(); + }); + assertConsoleErrorDev([ 'ComponentA defines an invalid contextType. ' + 'contextType should point to the Context object returned by React.createContext(). ' + - 'Did you accidentally pass the Context.Consumer instead?', - ); + 'Did you accidentally pass the Context.Consumer instead?\n' + + ' in ComponentA (at **)', + ]); - let container = document.createElement('div'); - let root = ReactDOMClient.createRoot(container); await act(() => { root.render(); }); @@ -505,8 +534,6 @@ describe('ReactContextValidator', () => { return
; } } - container = document.createElement('div'); - root = ReactDOMClient.createRoot(container); await act(() => { root.render(); }); @@ -539,23 +566,22 @@ describe('ReactContextValidator', () => { } await expect(async () => { - await expect(async () => { - const container = document.createElement('div'); - const root = ReactDOMClient.createRoot(container); - await act(() => { - root.render(); - }); - }).rejects.toThrow( - "Cannot read properties of undefined (reading 'world')", - ); - }).toErrorDev( + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render(); + }); + }).rejects.toThrow("Cannot read properties of undefined (reading 'world')"); + + assertConsoleErrorDev([ 'Foo defines an invalid contextType. ' + 'contextType should point to the Context object returned by React.createContext(). ' + 'However, it is set to undefined. ' + 'This can be caused by a typo or by mixing up named and default imports. ' + 'This can also happen due to a circular dependency, ' + - 'so try moving the createContext() call to a separate file.', - ); + 'so try moving the createContext() call to a separate file.\n' + + ' in Foo (at **)', + ]); }); it('should warn when class contextType is an object', async () => { @@ -571,20 +597,19 @@ describe('ReactContextValidator', () => { } await expect(async () => { - await expect(async () => { - const container = document.createElement('div'); - const root = ReactDOMClient.createRoot(container); - await act(() => { - root.render(); - }); - }).rejects.toThrow( - "Cannot read properties of undefined (reading 'hello')", - ); - }).toErrorDev( + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render(); + }); + }).rejects.toThrow("Cannot read properties of undefined (reading 'hello')"); + + assertConsoleErrorDev([ 'Foo defines an invalid contextType. ' + 'contextType should point to the Context object returned by React.createContext(). ' + - 'However, it is set to an object with keys {x, y}.', - ); + 'However, it is set to an object with keys {x, y}.\n' + + ' in Foo (at **)', + ]); }); it('should warn when class contextType is a primitive', async () => { @@ -596,20 +621,19 @@ describe('ReactContextValidator', () => { } await expect(async () => { - await expect(async () => { - const container = document.createElement('div'); - const root = ReactDOMClient.createRoot(container); - await act(() => { - root.render(); - }); - }).rejects.toThrow( - "Cannot read properties of undefined (reading 'world')", - ); - }).toErrorDev( + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render(); + }); + }).rejects.toThrow("Cannot read properties of undefined (reading 'world')"); + + assertConsoleErrorDev([ 'Foo defines an invalid contextType. ' + 'contextType should point to the Context object returned by React.createContext(). ' + - 'However, it is set to a string.', - ); + 'However, it is set to a string.\n' + + ' in Foo (at **)', + ]); }); it('should warn if you define contextType on a function component', async () => { @@ -625,31 +649,26 @@ describe('ReactContextValidator', () => { } ComponentB.contextType = Context; - await expect(async () => { - const container = document.createElement('div'); - const root = ReactDOMClient.createRoot(container); - await act(() => { - root.render(); - }); - }).toErrorDev( - 'ComponentA: Function components do not support contextType.', - ); + const root = ReactDOMClient.createRoot(document.createElement('div')); + await act(() => { + root.render(); + }); + assertConsoleErrorDev([ + 'ComponentA: Function components do not support contextType.\n' + + ' in ComponentA (at **)', + ]); // Warnings should be deduped by component type - let container = document.createElement('div'); - let root = ReactDOMClient.createRoot(container); await act(() => { root.render(); }); - await expect(async () => { - container = document.createElement('div'); - root = ReactDOMClient.createRoot(container); - await act(() => { - root.render(); - }); - }).toErrorDev( - 'ComponentB: Function components do not support contextType.', - ); + await act(() => { + root.render(); + }); + assertConsoleErrorDev([ + 'ComponentB: Function components do not support contextType.\n' + + ' in ComponentB (at **)', + ]); }); }); diff --git a/packages/react/src/__tests__/ReactCreateElement-test.js b/packages/react/src/__tests__/ReactCreateElement-test.js index 392f89979e769..44952536ac470 100644 --- a/packages/react/src/__tests__/ReactCreateElement-test.js +++ b/packages/react/src/__tests__/ReactCreateElement-test.js @@ -13,6 +13,8 @@ let act; let React; let ReactDOMClient; +let assertConsoleErrorDev; +let assertConsoleWarnDev; // NOTE: This module tests the old, "classic" JSX runtime, React.createElement. // Do not use JSX syntax in this module; call React.createElement directly. @@ -22,7 +24,11 @@ describe('ReactCreateElement', () => { beforeEach(() => { jest.resetModules(); - act = require('internal-test-utils').act; + ({ + act, + assertConsoleErrorDev, + assertConsoleWarnDev, + } = require('internal-test-utils')); React = require('react'); ReactDOMClient = require('react-dom/client'); @@ -63,25 +69,34 @@ describe('ReactCreateElement', () => { } } const root = ReactDOMClient.createRoot(document.createElement('div')); - await expect(async () => { - await act(() => { - root.render(React.createElement(Parent)); - }); - }).toErrorDev( + await act(() => { + root.render(React.createElement(Parent)); + }); + assertConsoleErrorDev([ 'Child: `key` is not a prop. Trying to access it will result ' + 'in `undefined` being returned. If you need to access the same ' + 'value within the child component, you should pass it as a different ' + - 'prop. (https://react.dev/link/special-props)', - ); + 'prop. (https://react.dev/link/special-props)\n' + + (gate(flags => flags.enableOwnerStacks) + ? [' in Parent (at **)'] + : [ + ' in Child (at **)\n' + + ' in div (at **)\n' + + ' in Parent (at **)', + ]), + ]); }); it('should warn when `key` is being accessed on a host element', () => { const element = React.createElement('div', {key: '3'}); - expect(() => void element.props.key).toErrorDev( - 'div: `key` is not a prop. Trying to access it will result ' + - 'in `undefined` being returned. If you need to access the same ' + - 'value within the child component, you should pass it as a different ' + - 'prop. (https://react.dev/link/special-props)', + void element.props.key; + assertConsoleErrorDev( + [ + 'div: `key` is not a prop. Trying to access it will result ' + + 'in `undefined` being returned. If you need to access the same ' + + 'value within the child component, you should pass it as a different ' + + 'prop. (https://react.dev/link/special-props)', + ], {withoutStack: true}, ); }); @@ -141,9 +156,16 @@ describe('ReactCreateElement', () => { foo: '56', }); expect(element.type).toBe(ComponentClass); - expect(() => expect(element.ref).toBe(ref)).toErrorDev( - 'Accessing element.ref was removed in React 19', - {withoutStack: true}, + expect(element.ref).toBe(ref); + assertConsoleErrorDev( + [ + 'Accessing element.ref was removed in React 19. ref is now a ' + + 'regular prop. It will be removed from the JSX Element ' + + 'type in a future release.', + ], + { + withoutStack: true, + }, ); const expectation = {foo: '56', ref}; Object.freeze(expectation); @@ -412,11 +434,13 @@ describe('ReactCreateElement', () => { it('warns if outdated JSX transform is detected', async () => { // Warns if __self is detected, because that's only passed by a compiler - expect(() => { - React.createElement('div', {className: 'foo', __self: this}); - }).toWarnDev( - 'Your app (or one of its dependencies) is using an outdated ' + - 'JSX transform.', + React.createElement('div', {className: 'foo', __self: this}); + assertConsoleWarnDev( + [ + 'Your app (or one of its dependencies) is using an outdated JSX ' + + 'transform. Update to the modern JSX transform for ' + + 'faster performance: https://react.dev/link/new-jsx-transform', + ], { withoutStack: true, }, diff --git a/packages/react/src/__tests__/ReactCreateRef-test.js b/packages/react/src/__tests__/ReactCreateRef-test.js index 616c62e00c88f..bf7166d8cf80a 100644 --- a/packages/react/src/__tests__/ReactCreateRef-test.js +++ b/packages/react/src/__tests__/ReactCreateRef-test.js @@ -12,6 +12,7 @@ let React; let ReactDOM; let ReactDOMClient; +let assertConsoleErrorDev; describe('ReactCreateRef', () => { beforeEach(() => { @@ -20,6 +21,7 @@ describe('ReactCreateRef', () => { React = require('react'); ReactDOM = require('react-dom'); ReactDOMClient = require('react-dom/client'); + ({assertConsoleErrorDev} = require('internal-test-utils')); }); it('should warn in dev if an invalid ref object is provided', () => { @@ -34,38 +36,36 @@ describe('ReactCreateRef', () => { } const root = ReactDOMClient.createRoot(document.createElement('div')); - expect(() => - ReactDOM.flushSync(() => { - root.render( - -
- , - ); - }), - ).toErrorDev( + ReactDOM.flushSync(() => { + root.render( + +
+ , + ); + }); + assertConsoleErrorDev([ 'Unexpected ref object provided for div. ' + 'Use either a ref-setter function or React.createRef().\n' + ' in div (at **)' + (gate(flags => flags.enableOwnerStacks) ? '' : '\n in Wrapper (at **)'), - ); + ]); - expect(() => - ReactDOM.flushSync(() => { - root.render( - - - , - ); - }), - ).toErrorDev( + ReactDOM.flushSync(() => { + root.render( + + + , + ); + }); + assertConsoleErrorDev([ 'Unexpected ref object provided for ExampleComponent. ' + 'Use either a ref-setter function or React.createRef().\n' + ' in ExampleComponent (at **)' + (gate(flags => flags.enableOwnerStacks) ? '' : '\n in Wrapper (at **)'), - ); + ]); }); }); diff --git a/packages/react/src/__tests__/ReactES6Class-test.js b/packages/react/src/__tests__/ReactES6Class-test.js index eceeab18c5bb0..640ae7ef37542 100644 --- a/packages/react/src/__tests__/ReactES6Class-test.js +++ b/packages/react/src/__tests__/ReactES6Class-test.js @@ -14,6 +14,7 @@ let React; let ReactDOM; let ReactDOMClient; let assertConsoleErrorDev; +let assertConsoleWarnDev; describe('ReactES6Class', () => { let container; @@ -31,7 +32,10 @@ describe('ReactES6Class', () => { React = require('react'); ReactDOM = require('react-dom'); ReactDOMClient = require('react-dom/client'); - ({assertConsoleErrorDev} = require('internal-test-utils')); + ({ + assertConsoleErrorDev, + assertConsoleWarnDev, + } = require('internal-test-utils')); container = document.createElement('div'); root = ReactDOMClient.createRoot(container); attachedListener = null; @@ -69,14 +73,15 @@ describe('ReactES6Class', () => { } window.addEventListener('error', errorHandler); try { - expect(() => { - ReactDOM.flushSync(() => root.render()); - }).toErrorDev([ + ReactDOM.flushSync(() => root.render()); + assertConsoleErrorDev([ // A failed component renders twice in DEV in concurrent mode 'No `render` method found on the Foo instance: ' + - 'you may have forgotten to define `render`.', + 'you may have forgotten to define `render`.\n' + + ' in Foo (at **)', 'No `render` method found on the Foo instance: ' + - 'you may have forgotten to define `render`.', + 'you may have forgotten to define `render`.\n' + + ' in Foo (at **)', ]); } finally { window.removeEventListener('error', errorHandler); @@ -158,12 +163,12 @@ describe('ReactES6Class', () => { return
; } } - expect(() => { - ReactDOM.flushSync(() => root.render()); - }).toErrorDev( + ReactDOM.flushSync(() => root.render()); + assertConsoleErrorDev([ 'Foo: getDerivedStateFromProps() is defined as an instance method ' + - 'and will be ignored. Instead, declare it as a static method.', - ); + 'and will be ignored. Instead, declare it as a static method.\n' + + ' in Foo (at **)', + ]); }); it('warns if getDerivedStateFromError is not static', () => { @@ -175,12 +180,12 @@ describe('ReactES6Class', () => { return
; } } - expect(() => { - ReactDOM.flushSync(() => root.render()); - }).toErrorDev( + ReactDOM.flushSync(() => root.render()); + assertConsoleErrorDev([ 'Foo: getDerivedStateFromError() is defined as an instance method ' + - 'and will be ignored. Instead, declare it as a static method.', - ); + 'and will be ignored. Instead, declare it as a static method.\n' + + ' in Foo (at **)', + ]); }); it('warns if getSnapshotBeforeUpdate is static', () => { @@ -190,12 +195,12 @@ describe('ReactES6Class', () => { return
; } } - expect(() => { - ReactDOM.flushSync(() => root.render()); - }).toErrorDev( + ReactDOM.flushSync(() => root.render()); + assertConsoleErrorDev([ 'Foo: getSnapshotBeforeUpdate() is defined as a static method ' + - 'and will be ignored. Instead, declare it as an instance method.', - ); + 'and will be ignored. Instead, declare it as an instance method.\n' + + ' in Foo (at **)', + ]); }); it('warns if state not initialized before static getDerivedStateFromProps', () => { @@ -210,14 +215,14 @@ describe('ReactES6Class', () => { return
; } } - expect(() => { - ReactDOM.flushSync(() => root.render()); - }).toErrorDev( + ReactDOM.flushSync(() => root.render()); + assertConsoleErrorDev([ '`Foo` uses `getDerivedStateFromProps` but its initial state is ' + 'undefined. This is not recommended. Instead, define the initial state by ' + 'assigning an object to `this.state` in the constructor of `Foo`. ' + - 'This ensures that `getDerivedStateFromProps` arguments have a consistent shape.', - ); + 'This ensures that `getDerivedStateFromProps` arguments have a consistent shape.\n' + + ' in Foo (at **)', + ]); }); it('updates initial state with values returned by static getDerivedStateFromProps', () => { @@ -266,11 +271,13 @@ describe('ReactES6Class', () => { super(props, context); this.state = {tag: context.tag, className: this.context.className}; } + render() { const Tag = this.state.tag; return ; } } + Foo.contextTypes = { tag: PropTypes.string, className: PropTypes.string, @@ -280,10 +287,12 @@ describe('ReactES6Class', () => { getChildContext() { return {tag: 'span', className: 'foo'}; } + render() { return ; } } + Outer.childContextTypes = { tag: PropTypes.string, className: PropTypes.string, @@ -291,8 +300,15 @@ describe('ReactES6Class', () => { runTest(, 'SPAN', 'foo'); assertConsoleErrorDev([ - 'Outer uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.', - 'Foo uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.', + 'Outer uses the legacy childContextTypes API which will soon be removed. ' + + 'Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' + + ' in Outer (at **)', + 'Foo uses the legacy contextTypes API which will soon be removed. ' + + 'Use React.createContext() with static contextType instead. (https://react.dev/link/legacy-context)\n' + + (gate(flags => flags.enableOwnerStacks) + ? '' + : ' in Foo (at **)\n') + + ' in Outer (at **)', ]); }); } @@ -327,9 +343,10 @@ describe('ReactES6Class', () => { return ; } } - expect(() => runTest(, 'SPAN', '')).toErrorDev( - 'Foo.state: must be set to an object or null', - ); + runTest(, 'SPAN', ''); + assertConsoleErrorDev([ + 'Foo.state: must be set to an object or null\n in Foo (at **)', + ]); }); }); @@ -480,11 +497,22 @@ describe('ReactES6Class', () => { } } - expect(() => runTest(, 'SPAN', 'foo')).toErrorDev([ - 'getInitialState was defined on Foo, a plain JavaScript class.', - 'getDefaultProps was defined on Foo, a plain JavaScript class.', - 'contextType was defined as an instance property on Foo.', - 'contextTypes was defined as an instance property on Foo.', + runTest(, 'SPAN', 'foo'); + assertConsoleErrorDev([ + 'getInitialState was defined on Foo, a plain JavaScript class. ' + + 'This is only supported for classes created using React.createClass. ' + + 'Did you mean to define a state property instead?\n' + + ' in Foo (at **)', + 'getDefaultProps was defined on Foo, a plain JavaScript class. ' + + 'This is only supported for classes created using React.createClass. ' + + 'Use a static property to define defaultProps instead.\n' + + ' in Foo (at **)', + 'contextType was defined as an instance property on Foo. ' + + 'Use a static property to define contextType instead.\n' + + ' in Foo (at **)', + 'contextTypes was defined as an instance property on Foo. ' + + 'Use a static property to define contextTypes instead.\n' + + ' in Foo (at **)', ]); expect(getInitialStateWasCalled).toBe(false); expect(getDefaultPropsWasCalled).toBe(false); @@ -514,11 +542,13 @@ describe('ReactES6Class', () => { } } - expect(() => runTest(, 'SPAN', 'foo')).toErrorDev( + runTest(, 'SPAN', 'foo'); + assertConsoleErrorDev([ 'NamedComponent has a method called componentShouldUpdate(). Did you ' + 'mean shouldComponentUpdate()? The name is phrased as a question ' + - 'because the function is expected to return a value.', - ); + 'because the function is expected to return a value.\n' + + ' in NamedComponent (at **)', + ]); }); it('should warn when misspelling componentWillReceiveProps', () => { @@ -531,10 +561,12 @@ describe('ReactES6Class', () => { } } - expect(() => runTest(, 'SPAN', 'foo')).toErrorDev( + runTest(, 'SPAN', 'foo'); + assertConsoleErrorDev([ 'NamedComponent has a method called componentWillRecieveProps(). Did ' + - 'you mean componentWillReceiveProps()?', - ); + 'you mean componentWillReceiveProps()?\n' + + ' in NamedComponent (at **)', + ]); }); it('should warn when misspelling UNSAFE_componentWillReceiveProps', () => { @@ -547,23 +579,33 @@ describe('ReactES6Class', () => { } } - expect(() => runTest(, 'SPAN', 'foo')).toErrorDev( + runTest(, 'SPAN', 'foo'); + assertConsoleErrorDev([ 'NamedComponent has a method called UNSAFE_componentWillRecieveProps(). ' + - 'Did you mean UNSAFE_componentWillReceiveProps()?', - ); + 'Did you mean UNSAFE_componentWillReceiveProps()?\n' + + ' in NamedComponent (at **)', + ]); }); it('should throw AND warn when trying to access classic APIs', () => { const ref = React.createRef(); runTest(, 'DIV', 'foo'); - expect(() => - expect(() => ref.current.replaceState({})).toThrow(), - ).toWarnDev( - 'replaceState(...) is deprecated in plain JavaScript React classes', + + expect(() => ref.current.replaceState({})).toThrow(); + assertConsoleWarnDev( + [ + 'replaceState(...) is deprecated in plain JavaScript React classes. ' + + 'Refactor your code to use setState instead (see https://github.com/facebook/react/issues/3236).', + ], {withoutStack: true}, ); - expect(() => expect(() => ref.current.isMounted()).toThrow()).toWarnDev( - 'isMounted(...) is deprecated in plain JavaScript React classes', + expect(() => ref.current.isMounted()).toThrow(); + assertConsoleWarnDev( + [ + 'isMounted(...) is deprecated in plain JavaScript React classes. ' + + 'Instead, make sure to clean up subscriptions and pending requests in ' + + 'componentWillUnmount to prevent memory leaks.', + ], {withoutStack: true}, ); }); @@ -575,20 +617,31 @@ describe('ReactES6Class', () => { return
; } } + Bar.contextTypes = {bar: PropTypes.string}; + class Foo extends React.Component { getChildContext() { return {bar: 'bar-through-context'}; } + render() { return ; } } + Foo.childContextTypes = {bar: PropTypes.string}; runTest(, 'DIV', 'bar-through-context'); assertConsoleErrorDev([ - 'Foo uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.', - 'Bar uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.', + 'Foo uses the legacy childContextTypes API which will soon be removed. ' + + 'Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' + + ' in Foo (at **)', + 'Bar uses the legacy contextTypes API which will soon be removed. ' + + 'Use React.createContext() with static contextType instead. (https://react.dev/link/legacy-context)\n' + + (gate(flags => flags.enableOwnerStacks) + ? '' + : ' in Bar (at **)\n') + + ' in Foo (at **)', ]); }); } diff --git a/packages/react/src/__tests__/ReactElementClone-test.js b/packages/react/src/__tests__/ReactElementClone-test.js index fb0bfe2df8d6b..b2e791f8268c9 100644 --- a/packages/react/src/__tests__/ReactElementClone-test.js +++ b/packages/react/src/__tests__/ReactElementClone-test.js @@ -12,6 +12,7 @@ let act; let React; let ReactDOMClient; +let assertConsoleErrorDev; describe('ReactElementClone', () => { let ComponentClass; @@ -19,7 +20,7 @@ describe('ReactElementClone', () => { beforeEach(() => { jest.resetModules(); - act = require('internal-test-utils').act; + ({act, assertConsoleErrorDev} = require('internal-test-utils')); React = require('react'); ReactDOMClient = require('react-dom/client'); @@ -314,11 +315,14 @@ describe('ReactElementClone', () => { it('warns for keys for arrays of elements in rest args', async () => { const root = ReactDOMClient.createRoot(document.createElement('div')); - await expect(async () => { - await act(() => { - root.render(React.cloneElement(
, null, [
,
])); - }); - }).toErrorDev('Each child in a list should have a unique "key" prop.'); + await act(() => { + root.render(React.cloneElement(
, null, [
,
])); + }); + assertConsoleErrorDev([ + 'Each child in a list should have a unique "key" prop.\n\n' + + 'Check the top-level render call using
. See https://react.dev/link/warning-keys for more information.\n' + + ' in div (at **)', + ]); }); it('does not warns for arrays of elements with keys', async () => { @@ -363,9 +367,16 @@ describe('ReactElementClone', () => { expect(clone.type).toBe(ComponentClass); expect(clone.key).toBe('12'); expect(clone.props.ref).toBe('34'); - expect(() => expect(clone.ref).toBe('34')).toErrorDev( - 'Accessing element.ref was removed in React 19', - {withoutStack: true}, + expect(clone.ref).toBe('34'); + assertConsoleErrorDev( + [ + 'Accessing element.ref was removed in React 19. ref is now a ' + + 'regular prop. It will be removed from the JSX Element ' + + 'type in a future release.', + ], + { + withoutStack: true, + }, ); expect(clone.props).toEqual({foo: 'ef', ref: '34'}); if (__DEV__) { diff --git a/packages/react/src/__tests__/ReactElementValidator-test.internal.js b/packages/react/src/__tests__/ReactElementValidator-test.internal.js index 4383a6472d350..1191aa7ec2743 100644 --- a/packages/react/src/__tests__/ReactElementValidator-test.internal.js +++ b/packages/react/src/__tests__/ReactElementValidator-test.internal.js @@ -18,6 +18,7 @@ let React; let ReactDOMClient; let act; +let assertConsoleErrorDev; describe('ReactElementValidator', () => { let ComponentClass; @@ -27,7 +28,7 @@ describe('ReactElementValidator', () => { React = require('react'); ReactDOMClient = require('react-dom/client'); - act = require('internal-test-utils').act; + ({act, assertConsoleErrorDev} = require('internal-test-utils')); ComponentClass = class extends React.Component { render() { return React.createElement('div', null, this.props.children); @@ -37,16 +38,27 @@ describe('ReactElementValidator', () => { it('warns for keys for arrays of elements in rest args', async () => { const root = ReactDOMClient.createRoot(document.createElement('div')); - await expect(async () => { - await act(() => - root.render( - React.createElement(ComponentClass, null, [ - React.createElement(ComponentClass), - React.createElement(ComponentClass), - ]), - ), - ); - }).toErrorDev('Each child in a list should have a unique "key" prop.'); + await act(() => + root.render( + React.createElement(ComponentClass, null, [ + React.createElement(ComponentClass), + React.createElement(ComponentClass), + ]), + ), + ); + assertConsoleErrorDev( + gate(flags => flags.enableOwnerStacks) + ? [ + 'Each child in a list should have a unique "key" prop.\n\n' + + 'Check the render method of `ComponentClass`. See https://react.dev/link/warning-keys for more information.\n' + + ' in ComponentClass (at **)', + ] + : [ + 'Each child in a list should have a unique "key" prop.\n\n' + + 'Check the top-level render call using . See https://react.dev/link/warning-keys for more information.\n' + + ' in ComponentClass (at **)', + ], + ); }); it('warns for keys for arrays of elements with owner info', async () => { @@ -67,18 +79,23 @@ describe('ReactElementValidator', () => { } } - await expect(async () => { - const root = ReactDOMClient.createRoot(document.createElement('div')); - await act(() => root.render(React.createElement(ComponentWrapper))); - }).toErrorDev( + const root = ReactDOMClient.createRoot(document.createElement('div')); + await act(() => root.render(React.createElement(ComponentWrapper))); + assertConsoleErrorDev([ 'Each child in a list should have a unique "key" prop.' + '\n\nCheck the render method of `' + (gate(flags => flags.enableOwnerStacks) ? 'ComponentClass' : 'InnerClass') + '`. ' + - 'It was passed a child from ComponentWrapper. ', - ); + 'It was passed a child from ComponentWrapper. ' + + 'See https://react.dev/link/warning-keys for more information.\n' + + (gate(flags => flags.enableOwnerStacks) + ? ' in ComponentWrapper (at **)' + : ' in ComponentClass (at **)\n' + + ' in InnerClass (at **)\n' + + ' in ComponentWrapper (at **)'), + ]); }); it('warns for keys for arrays with no owner or parent info', async () => { @@ -89,36 +106,35 @@ describe('ReactElementValidator', () => { const divs = [
,
]; - await expect(async () => { - const root = ReactDOMClient.createRoot(document.createElement('div')); - await act(() => root.render({divs})); - }).toErrorDev( + const root = ReactDOMClient.createRoot(document.createElement('div')); + await act(() => root.render({divs})); + assertConsoleErrorDev([ gate(flags => flags.enableOwnerStacks) ? // For owner stacks the parent being validated is the div. 'Each child in a list should have a unique ' + - '"key" prop.' + - '\n\nCheck the top-level render call using
. ' + - 'See https://react.dev/link/warning-keys for more information.\n' + - ' in div (at **)' + '"key" prop.' + + '\n\nCheck the top-level render call using
. ' + + 'See https://react.dev/link/warning-keys for more information.\n' + + ' in div (at **)' : 'Each child in a list should have a unique ' + - '"key" prop. See https://react.dev/link/warning-keys for more information.\n' + - ' in div (at **)', - ); + '"key" prop. See https://react.dev/link/warning-keys for more information.\n' + + ' in div (at **)', + ]); }); it('warns for keys for arrays of elements with no owner info', async () => { const divs = [
,
]; - await expect(async () => { - const root = ReactDOMClient.createRoot(document.createElement('div')); + const root = ReactDOMClient.createRoot(document.createElement('div')); - await act(() => root.render(
{divs}
)); - }).toErrorDev( + await act(() => root.render(
{divs}
)); + assertConsoleErrorDev([ 'Each child in a list should have a unique ' + - '"key" prop.\n\nCheck the top-level render call using
. See ' + - 'https://react.dev/link/warning-keys for more information.\n' + + '"key" prop.' + + '\n\nCheck the top-level render call using
. ' + + 'See https://react.dev/link/warning-keys for more information.\n' + ' in div (at **)', - ); + ]); }); it('warns for keys with component stack info', async () => { @@ -134,10 +150,9 @@ describe('ReactElementValidator', () => { return } />; } - await expect(async () => { - const root = ReactDOMClient.createRoot(document.createElement('div')); - await act(() => root.render()); - }).toErrorDev( + const root = ReactDOMClient.createRoot(document.createElement('div')); + await act(() => root.render()); + assertConsoleErrorDev([ 'Each child in a list should have a unique ' + '"key" prop.\n\nCheck the render method of `Component`. See ' + 'https://react.dev/link/warning-keys for more information.\n' + @@ -147,7 +162,7 @@ describe('ReactElementValidator', () => { ? '' : ' in Parent (at **)\n') + ' in GrandParent (at **)', - ); + ]); }); it('does not warn for keys when passing children down', async () => { @@ -187,21 +202,37 @@ describe('ReactElementValidator', () => { }, }; - await expect(async () => { - const root = ReactDOMClient.createRoot(document.createElement('div')); - await act(() => - root.render(React.createElement(ComponentClass, null, iterable)), - ); - }).toErrorDev( + const root = ReactDOMClient.createRoot(document.createElement('div')); + await act(() => + root.render(React.createElement(ComponentClass, null, iterable)), + ); + assertConsoleErrorDev( gate(flag => flag.enableOwnerStacks) - ? 'Each child in a list should have a unique "key" prop.' + ? [ + 'Each child in a list should have a unique "key" prop.\n\n' + + 'Check the render method of `ComponentClass`. It was passed a child from div. ' + + 'See https://react.dev/link/warning-keys for more information.\n' + + ' in ComponentClass (at **)', + ] : // Since each pass generates a new element, it doesn't get marked as // validated and it gets rechecked each time. - [ - 'Each child in a list should have a unique "key" prop.', - 'Each child in a list should have a unique "key" prop.', - 'Each child in a list should have a unique "key" prop.', + 'Each child in a list should have a unique "key" prop.\n\n' + + 'Check the top-level render call using . ' + + 'See https://react.dev/link/warning-keys for more information.\n' + + ' in ComponentClass (at **)', + + 'Each child in a list should have a unique "key" prop.\n\n' + + 'Check the render method of `ComponentClass`. ' + + 'See https://react.dev/link/warning-keys for more information.\n' + + ' in ComponentClass (at **)\n' + + ' in ComponentClass (at **)', + 'Each child in a list should have a unique "key" prop.\n\n' + + 'Check the render method of `ComponentClass`. It was passed a child from div. ' + + 'See https://react.dev/link/warning-keys for more information.\n' + + ' in ComponentClass (at **)\n' + + ' in div (at **)\n' + + ' in ComponentClass (at **)', ], ); }); @@ -254,86 +285,102 @@ describe('ReactElementValidator', () => { function ParentComp() { return React.createElement(MyComp); } - await expect(async () => { - const root = ReactDOMClient.createRoot(document.createElement('div')); - await act(() => root.render(React.createElement(ParentComp))); - }).toErrorDev( + const root = ReactDOMClient.createRoot(document.createElement('div')); + await act(() => root.render(React.createElement(ParentComp))); + assertConsoleErrorDev([ 'Each child in a list should have a unique "key" prop.' + '\n\nCheck the render method of `ParentComp`. It was passed a child from MyComp. ' + 'See https://react.dev/link/warning-keys for more information.\n' + ' in div (at **)\n' + ' in MyComp (at **)\n' + ' in ParentComp (at **)', - ); + ]); }); it('gives a helpful error when passing invalid types', async () => { function Foo() {} const errors = []; - await expect(async () => { - const root = ReactDOMClient.createRoot(document.createElement('div'), { - onUncaughtError(error) { - errors.push(error.message); - }, - }); - const cases = [ - React.createElement(undefined), - React.createElement(null), - React.createElement(true), - React.createElement({x: 17}), - React.createElement({}), - React.createElement(React.createElement('div')), - React.createElement(React.createElement(Foo)), - React.createElement( - React.createElement(React.createContext().Consumer), - ), - React.createElement({$$typeof: 'non-react-thing'}), - ]; - for (let i = 0; i < cases.length; i++) { - await act(() => root.render(cases[i])); - } - }).toErrorDev( - gate(flag => flag.enableOwnerStacks) - ? // We don't need these extra warnings because we already have the errors. - [] - : [ - 'React.createElement: 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.', - 'React.createElement: type is invalid -- expected a string ' + - '(for built-in components) or a class/function (for composite ' + - 'components) but got: null.', - 'React.createElement: type is invalid -- expected a string ' + - '(for built-in components) or a class/function (for composite ' + - 'components) but got: boolean.', - 'React.createElement: type is invalid -- expected a string ' + - '(for built-in components) or a class/function (for composite ' + - 'components) but got: object.', - 'React.createElement: 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.', - 'React.createElement: 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?', - 'React.createElement: 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?', - 'React.createElement: 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?', - 'React.createElement: type is invalid -- expected a string ' + - '(for built-in components) or a class/function (for composite ' + - 'components) but got: object.', - ], - {withoutStack: true}, - ); + const root = ReactDOMClient.createRoot(document.createElement('div'), { + onUncaughtError(error) { + errors.push(error.message); + }, + }); + const cases = [ + [ + () => React.createElement(undefined), + 'React.createElement: 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.', + ], + [ + () => React.createElement(null), + 'React.createElement: type is invalid -- expected a string ' + + '(for built-in components) or a class/function (for composite ' + + 'components) but got: null.', + ], + [ + () => React.createElement(true), + 'React.createElement: type is invalid -- expected a string ' + + '(for built-in components) or a class/function (for composite ' + + 'components) but got: boolean.', + ], + [ + () => React.createElement({x: 17}), + 'React.createElement: type is invalid -- expected a string ' + + '(for built-in components) or a class/function (for composite ' + + 'components) but got: object.', + ], + [ + () => React.createElement({}), + 'React.createElement: 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.', + ], + [ + () => React.createElement(React.createElement('div')), + 'React.createElement: 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?', + ], + [ + () => React.createElement(React.createElement(Foo)), + 'React.createElement: 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?', + ], + [ + () => + React.createElement( + React.createElement(React.createContext().Consumer), + ), + 'React.createElement: 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?', + ], + [ + () => React.createElement({$$typeof: 'non-react-thing'}), + 'React.createElement: type is invalid -- expected a string ' + + '(for built-in components) or a class/function (for composite ' + + 'components) but got: object.', + ], + ]; + for (let i = 0; i < cases.length; i++) { + await act(async () => root.render(cases[i][0]())); + assertConsoleErrorDev( + gate(flag => flag.enableOwnerStacks) + ? // We don't need these extra warnings because we already have the errors. + [] + : [cases[i][1]], + {withoutStack: true}, + ); + } expect(errors).toEqual( __DEV__ @@ -414,15 +461,14 @@ describe('ReactElementValidator', () => { } await expect(async () => { - await expect(async () => { - const root = ReactDOMClient.createRoot(document.createElement('div')); - await act(() => root.render(React.createElement(ParentComp))); - }).rejects.toThrowError( - 'Element type is invalid: expected a string (for built-in components) ' + - 'or a class/function (for composite components) but got: null.' + - (__DEV__ ? '\n\nCheck the render method of `ParentComp`.' : ''), - ); - }).toErrorDev( + const root = ReactDOMClient.createRoot(document.createElement('div')); + await act(() => root.render(React.createElement(ParentComp))); + }).rejects.toThrowError( + 'Element type is invalid: expected a string (for built-in components) ' + + 'or a class/function (for composite components) but got: null.' + + (__DEV__ ? '\n\nCheck the render method of `ParentComp`.' : ''), + ); + assertConsoleErrorDev( gate(flag => flag.enableOwnerStacks) ? // We don't need these extra warnings because we already have the errors. [] @@ -446,13 +492,13 @@ describe('ReactElementValidator', () => { } } - await expect(async () => { - const root = ReactDOMClient.createRoot(document.createElement('div')); - await act(() => root.render(React.createElement(Foo))); - }).toErrorDev( + const root = ReactDOMClient.createRoot(document.createElement('div')); + await act(() => root.render(React.createElement(Foo))); + assertConsoleErrorDev([ 'Invalid prop `a` supplied to `React.Fragment`. React.Fragment ' + - 'can only have `key` and `children` props.', - ); + 'can only have `key` and `children` props.\n' + + ' in Foo (at **)', + ]); }); it('does not warn when using DOM node as children', async () => { @@ -512,9 +558,8 @@ describe('ReactElementValidator', () => { it('does not blow up on key warning with undefined type', () => { const Foo = undefined; - expect(() => { - void ({[
]}); - }).toErrorDev( + void ({[
]}); + assertConsoleErrorDev( gate(flags => flags.enableOwnerStacks) ? [] : [ diff --git a/packages/react/src/__tests__/ReactJSXElementValidator-test.js b/packages/react/src/__tests__/ReactJSXElementValidator-test.js index 12e54e5c969cc..d4e78f1b1cd46 100644 --- a/packages/react/src/__tests__/ReactJSXElementValidator-test.js +++ b/packages/react/src/__tests__/ReactJSXElementValidator-test.js @@ -14,6 +14,7 @@ let act; let React; let ReactDOMClient; +let assertConsoleErrorDev; describe('ReactJSXElementValidator', () => { let Component; @@ -22,7 +23,7 @@ describe('ReactJSXElementValidator', () => { beforeEach(() => { jest.resetModules(); - act = require('internal-test-utils').act; + ({act, assertConsoleErrorDev} = require('internal-test-utils')); React = require('react'); ReactDOMClient = require('react-dom/client'); @@ -41,14 +42,21 @@ describe('ReactJSXElementValidator', () => { }); it('warns for keys for arrays of elements in children position', async () => { - await expect(async () => { - const container = document.createElement('div'); - const root = ReactDOMClient.createRoot(container); - - await act(() => { - root.render({[, ]}); - }); - }).toErrorDev('Each child in a list should have a unique "key" prop.'); + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + + await act(() => { + root.render({[, ]}); + }); + assertConsoleErrorDev([ + gate(flags => flags.enableOwnerStacks) + ? 'Each child in a list should have a unique "key" prop.\n\n' + + 'Check the render method of `Component`. See https://react.dev/link/warning-keys for more information.\n' + + ' in Component (at **)' + : 'Each child in a list should have a unique "key" prop.\n\n' + + 'Check the top-level render call using . See https://react.dev/link/warning-keys for more information.\n' + + ' in Component (at **)', + ]); }); it('warns for keys for arrays of elements with owner info', async () => { @@ -64,20 +72,24 @@ describe('ReactJSXElementValidator', () => { } } - await expect(async () => { - const container = document.createElement('div'); - const root = ReactDOMClient.createRoot(container); - await act(() => { - root.render(); - }); - }).toErrorDev([ + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render(); + }); + assertConsoleErrorDev([ 'Each child in a list should have a unique "key" prop.' + '\n\nCheck the render method of `' + (gate(flag => flag.enableOwnerStacks) ? 'Component' : 'InnerComponent') + '`. ' + - 'It was passed a child from ComponentWrapper. ', + 'It was passed a child from ComponentWrapper. See https://react.dev/link/warning-keys for more information.\n' + + (gate(flag => flag.enableOwnerStacks) + ? ' in ComponentWrapper (at **)' + : ' in Component (at **)\n' + + ' in InnerComponent (at **)\n' + + ' in ComponentWrapper (at **)'), ]); }); @@ -94,22 +106,38 @@ describe('ReactJSXElementValidator', () => { }, }; - await expect(async () => { - const container = document.createElement('div'); - const root = ReactDOMClient.createRoot(container); + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); - await act(() => { - root.render({iterable}); - }); - }).toErrorDev( + await act(() => { + root.render({iterable}); + }); + assertConsoleErrorDev( gate(flag => flag.enableOwnerStacks) - ? ['Each child in a list should have a unique "key" prop.'] + ? [ + 'Each child in a list should have a unique "key" prop.\n\n' + + 'Check the render method of `Component`. It was passed a child from div. ' + + 'See https://react.dev/link/warning-keys for more information.\n' + + ' in Component (at **)', + ] : // Since each pass generates a new element, it doesn't get marked as // validated and it gets rechecked each time. [ - 'Each child in a list should have a unique "key" prop.', - 'Each child in a list should have a unique "key" prop.', - 'Each child in a list should have a unique "key" prop.', + 'Each child in a list should have a unique "key" prop.\n\n' + + 'Check the top-level render call using . ' + + 'See https://react.dev/link/warning-keys for more information.\n' + + ' in Component (at **)', + 'Each child in a list should have a unique "key" prop.\n\n' + + 'Check the render method of `Component`. ' + + 'See https://react.dev/link/warning-keys for more information.\n' + + ' in Component (at **)\n' + + ' in Component (at **)', + 'Each child in a list should have a unique "key" prop.\n\n' + + 'Check the render method of `Component`. It was passed a child from div. ' + + 'See https://react.dev/link/warning-keys for more information.\n' + + ' in Component (at **)\n' + + ' in div (at **)\n' + + ' in Component (at **)', ], ); }); @@ -198,21 +226,20 @@ describe('ReactJSXElementValidator', () => { return ; } } - await expect(async () => { - const container = document.createElement('div'); - const root = ReactDOMClient.createRoot(container); - - await act(() => { - root.render(); - }); - }).toErrorDev( + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + + await act(() => { + root.render(); + }); + assertConsoleErrorDev([ 'Each child in a list should have a unique "key" prop.' + '\n\nCheck the render method of `ParentComp`. It was passed a child from MyComp. ' + 'See https://react.dev/link/warning-keys for more information.\n' + ' in div (at **)\n' + ' in MyComp (at **)\n' + ' in ParentComp (at **)', - ); + ]); }); it('warns for fragments with illegal attributes', async () => { @@ -222,16 +249,16 @@ describe('ReactJSXElementValidator', () => { } } - await expect(async () => { - const container = document.createElement('div'); - const root = ReactDOMClient.createRoot(container); - await act(() => { - root.render(); - }); - }).toErrorDev( + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render(); + }); + assertConsoleErrorDev([ 'Invalid prop `a` supplied to `React.Fragment`. React.Fragment ' + - 'can only have `key` and `children` props.', - ); + 'can only have `key` and `children` props.\n' + + ' in Foo (at **)', + ]); }); it('warns for fragments with refs', async () => { @@ -248,13 +275,16 @@ describe('ReactJSXElementValidator', () => { } } - await expect(async () => { - const container = document.createElement('div'); - const root = ReactDOMClient.createRoot(container); - await act(() => { - root.render(); - }); - }).toErrorDev('Invalid prop `ref` supplied to `React.Fragment`.'); + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render(); + }); + assertConsoleErrorDev([ + 'Invalid prop `ref` supplied to `React.Fragment`.' + + ' React.Fragment can only have `key` and `children` props.\n' + + ' in Foo (at **)', + ]); }); it('does not warn for fragments of multiple elements without keys', async () => { @@ -271,19 +301,24 @@ describe('ReactJSXElementValidator', () => { }); it('warns for fragments of multiple elements with same key', async () => { - await expect(async () => { - const container = document.createElement('div'); - const root = ReactDOMClient.createRoot(container); - await act(() => { - root.render( - <> - 1 - 2 - 3 - , - ); - }); - }).toErrorDev('Encountered two children with the same key, `a`.'); + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render( + <> + 1 + 2 + 3 + , + ); + }); + assertConsoleErrorDev([ + 'Encountered two children with the same key, `a`. ' + + 'Keys should be unique so that components maintain their identity across updates. ' + + 'Non-unique keys may cause children to be duplicated and/or omitted — ' + + 'the behavior is unsupported and could change in a future version.\n' + + ' in span (at **)', + ]); }); it('does not call lazy initializers eagerly', () => { diff --git a/packages/react/src/__tests__/ReactJSXRuntime-test.js b/packages/react/src/__tests__/ReactJSXRuntime-test.js index e3de4dbf5ead5..663c935caf1da 100644 --- a/packages/react/src/__tests__/ReactJSXRuntime-test.js +++ b/packages/react/src/__tests__/ReactJSXRuntime-test.js @@ -14,6 +14,7 @@ let ReactDOMClient; let JSXRuntime; let JSXDEVRuntime; let act; +let assertConsoleErrorDev; // NOTE: Prefer to call the JSXRuntime directly in these tests so we can be // certain that we are testing the runtime behavior, as opposed to the Babel @@ -26,7 +27,7 @@ describe('ReactJSXRuntime', () => { JSXRuntime = require('react/jsx-runtime'); JSXDEVRuntime = require('react/jsx-dev-runtime'); ReactDOMClient = require('react-dom/client'); - act = require('internal-test-utils').act; + ({act, assertConsoleErrorDev} = require('internal-test-utils')); }); it('allows static methods to be called using the type property', () => { @@ -205,41 +206,49 @@ describe('ReactJSXRuntime', () => { }); } } - await expect(async () => { - const root = ReactDOMClient.createRoot(container); - await act(() => { - root.render(JSXRuntime.jsx(Parent, {})); - }); - }).toErrorDev( + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render(JSXRuntime.jsx(Parent, {})); + }); + assertConsoleErrorDev([ 'Child: `key` is not a prop. Trying to access it will result ' + 'in `undefined` being returned. If you need to access the same ' + 'value within the child component, you should pass it as a different ' + - 'prop. (https://react.dev/link/special-props)', - ); + 'prop. (https://react.dev/link/special-props)\n' + + (gate(flags => flags.enableOwnerStacks) + ? ' in Parent (at **)' + : ' in Child (at **)\n' + + ' in div (at **)\n' + + ' in Parent (at **)'), + ]); }); it('warns when a jsxs is passed something that is not an array', async () => { const container = document.createElement('div'); - await expect(async () => { - const root = ReactDOMClient.createRoot(container); - await act(() => { - root.render(JSXRuntime.jsxs('div', {children: 'foo'}, null)); - }); - }).toErrorDev( - 'React.jsx: Static children should always be an array. ' + - 'You are likely explicitly calling React.jsxs or React.jsxDEV. ' + - 'Use the Babel transform instead.', + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render(JSXRuntime.jsxs('div', {children: 'foo'}, null)); + }); + assertConsoleErrorDev( + [ + 'React.jsx: Static children should always be an array. ' + + 'You are likely explicitly calling React.jsxs or React.jsxDEV. ' + + 'Use the Babel transform instead.', + ], {withoutStack: true}, ); }); it('should warn when `key` is being accessed on a host element', () => { const element = JSXRuntime.jsxs('div', {}, '3'); - expect(() => void element.props.key).toErrorDev( - 'div: `key` is not a prop. Trying to access it will result ' + - 'in `undefined` being returned. If you need to access the same ' + - 'value within the child component, you should pass it as a different ' + - 'prop. (https://react.dev/link/special-props)', + void element.props.key; + assertConsoleErrorDev( + [ + 'div: `key` is not a prop. Trying to access it will result ' + + 'in `undefined` being returned. If you need to access the same ' + + 'value within the child component, you should pass it as a different ' + + 'prop. (https://react.dev/link/special-props)', + ], {withoutStack: true}, ); }); @@ -263,19 +272,18 @@ describe('ReactJSXRuntime', () => { }); } } - await expect(async () => { - const root = ReactDOMClient.createRoot(container); - await act(() => { - root.render(JSXRuntime.jsx(Parent, {})); - }); - }).toErrorDev( + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render(JSXRuntime.jsx(Parent, {})); + }); + assertConsoleErrorDev([ 'Each child in a list should have a unique "key" prop.\n\n' + 'Check the render method of `Parent`. See https://react.dev/link/warning-keys for more information.\n' + (gate(flags => flags.enableOwnerStacks) ? '' : ' in Child (at **)\n') + ' in Parent (at **)', - ); + ]); }); it('should warn when keys are passed as part of props', async () => { @@ -292,19 +300,19 @@ describe('ReactJSXRuntime', () => { }); } } - await expect(async () => { - const root = ReactDOMClient.createRoot(container); - await act(() => { - root.render(JSXRuntime.jsx(Parent, {})); - }); - }).toErrorDev( + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render(JSXRuntime.jsx(Parent, {})); + }); + assertConsoleErrorDev([ 'A props object containing a "key" prop is being spread into JSX:\n' + ' let props = {key: someKey, prop: ...};\n' + ' \n' + 'React keys must be passed directly to JSX without using spread:\n' + ' let props = {prop: ...};\n' + - ' ', - ); + ' \n' + + ' in Parent (at **)', + ]); }); it('should not warn when unkeyed children are passed to jsxs', async () => { @@ -368,13 +376,18 @@ describe('ReactJSXRuntime', () => { key: 'key', }; - let elementWithSpreadKey; - expect(() => { - elementWithSpreadKey = __DEV__ - ? JSXDEVRuntime.jsxDEV('div', configWithKey) - : JSXRuntime.jsx('div', configWithKey); - }).toErrorDev( - 'A props object containing a "key" prop is being spread into JSX', + const elementWithSpreadKey = __DEV__ + ? JSXDEVRuntime.jsxDEV('div', configWithKey) + : JSXRuntime.jsx('div', configWithKey); + assertConsoleErrorDev( + [ + 'A props object containing a "key" prop is being spread into JSX:\n' + + ' let props = {key: someKey, foo: ..., bar: ...};\n' + + '
\n' + + 'React keys must be passed directly to JSX without using spread:\n' + + ' let props = {foo: ..., bar: ...};\n' + + '
', + ], {withoutStack: true}, ); expect(elementWithSpreadKey.props).not.toBe(configWithKey); diff --git a/packages/react/src/__tests__/ReactJSXTransformIntegration-test.js b/packages/react/src/__tests__/ReactJSXTransformIntegration-test.js index f0caf6b494ae1..5075c4a9a0bc5 100644 --- a/packages/react/src/__tests__/ReactJSXTransformIntegration-test.js +++ b/packages/react/src/__tests__/ReactJSXTransformIntegration-test.js @@ -12,6 +12,7 @@ let React; let ReactDOMClient; let act; +let assertConsoleErrorDev; // TODO: Historically this module was used to confirm that the JSX transform // produces the correct output. However, most users (and indeed our own test @@ -29,7 +30,7 @@ describe('ReactJSXTransformIntegration', () => { React = require('react'); ReactDOMClient = require('react-dom/client'); - act = require('internal-test-utils').act; + ({act, assertConsoleErrorDev} = require('internal-test-utils')); Component = class extends React.Component { render() { @@ -112,8 +113,13 @@ describe('ReactJSXTransformIntegration', () => { const ref = React.createRef(); const element = ; expect(element.type).toBe(Component); - expect(() => expect(element.ref).toBe(ref)).toErrorDev( - 'Accessing element.ref was removed in React 19', + expect(element.ref).toBe(ref); + assertConsoleErrorDev( + [ + 'Accessing element.ref was removed in React 19. ref is now a ' + + 'regular prop. It will be removed from the JSX Element ' + + 'type in a future release.', + ], {withoutStack: true}, ); const expectation = {foo: '56', ref}; diff --git a/packages/react/src/__tests__/ReactProfilerComponent-test.internal.js b/packages/react/src/__tests__/ReactProfilerComponent-test.internal.js index d8fc623a3dafc..20395118c5c60 100644 --- a/packages/react/src/__tests__/ReactProfilerComponent-test.internal.js +++ b/packages/react/src/__tests__/ReactProfilerComponent-test.internal.js @@ -14,6 +14,7 @@ let ReactDOMClient; let ReactFeatureFlags; let act; let container; +let assertConsoleErrorDev; function loadModules({ enableProfilerTimer = true, @@ -31,6 +32,7 @@ function loadModules({ ReactDOMClient = require('react-dom/client'); const InternalTestUtils = require('internal-test-utils'); act = InternalTestUtils.act; + assertConsoleErrorDev = InternalTestUtils.assertConsoleErrorDev; } describe('Profiler', () => { @@ -54,12 +56,13 @@ describe('Profiler', () => { if (__DEV__ && enableProfilerTimer) { it('should warn if required params are missing', async () => { const root = ReactDOMClient.createRoot(container); - await expect(async () => { - await act(() => { - root.render(); - }); - }).toErrorDev( - 'Profiler must specify an "id" of type `string` as a prop. Received the type `undefined` instead.', + await act(() => { + root.render(); + }); + assertConsoleErrorDev( + [ + 'Profiler must specify an "id" of type `string` as a prop. Received the type `undefined` instead.', + ], { withoutStack: true, }, diff --git a/packages/react/src/__tests__/ReactPureComponent-test.js b/packages/react/src/__tests__/ReactPureComponent-test.js index 9efdf183ee375..c54945ff250c2 100644 --- a/packages/react/src/__tests__/ReactPureComponent-test.js +++ b/packages/react/src/__tests__/ReactPureComponent-test.js @@ -10,13 +10,13 @@ 'use strict'; let act; - +let assertConsoleErrorDev; let React; let ReactDOMClient; describe('ReactPureComponent', () => { beforeEach(() => { - act = require('internal-test-utils').act; + ({act, assertConsoleErrorDev} = require('internal-test-utils')); React = require('react'); ReactDOMClient = require('react-dom/client'); @@ -90,16 +90,15 @@ describe('ReactPureComponent', () => { const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); - await expect(async () => { - await act(() => { - root.render(); - }); - }).toErrorDev( - '' + - 'Component has a method called shouldComponentUpdate(). ' + + await act(() => { + root.render(); + }); + assertConsoleErrorDev([ + 'Component has a method called shouldComponentUpdate(). ' + 'shouldComponentUpdate should not be used when extending React.PureComponent. ' + - 'Please extend React.Component if shouldComponentUpdate is used.', - ); + 'Please extend React.Component if shouldComponentUpdate is used.\n' + + ' in Component (at **)', + ]); await act(() => { root.render(); }); @@ -133,15 +132,14 @@ describe('ReactPureComponent', () => { } } const root = ReactDOMClient.createRoot(document.createElement('div')); - await expect(async () => { - await act(() => { - root.render(); - }); - }).toErrorDev( - '' + - 'PureComponent has a method called shouldComponentUpdate(). ' + + await act(() => { + root.render(); + }); + assertConsoleErrorDev([ + 'PureComponent has a method called shouldComponentUpdate(). ' + 'shouldComponentUpdate should not be used when extending React.PureComponent. ' + - 'Please extend React.Component if shouldComponentUpdate is used.', - ); + 'Please extend React.Component if shouldComponentUpdate is used.\n' + + ' in PureComponent (at **)', + ]); }); }); diff --git a/packages/react/src/__tests__/ReactStartTransition-test.js b/packages/react/src/__tests__/ReactStartTransition-test.js index 9e689ac6e7105..00387dbb267ac 100644 --- a/packages/react/src/__tests__/ReactStartTransition-test.js +++ b/packages/react/src/__tests__/ReactStartTransition-test.js @@ -12,6 +12,7 @@ let React; let ReactTestRenderer; let act; +let assertConsoleWarnDev; let useState; let useTransition; @@ -22,7 +23,7 @@ describe('ReactStartTransition', () => { jest.resetModules(); React = require('react'); ReactTestRenderer = require('react-test-renderer'); - act = require('internal-test-utils').act; + ({act, assertConsoleWarnDev} = require('internal-test-utils')); useState = React.useState; useTransition = React.useTransition; }); @@ -53,15 +54,14 @@ describe('ReactStartTransition', () => { }); }); - await expect(async () => { - await act(() => { - React.startTransition(() => { - subs.forEach(setState => { - setState(state => state + 1); - }); + await act(() => { + React.startTransition(() => { + subs.forEach(setState => { + setState(state => state + 1); }); }); - }).toWarnDev( + }); + assertConsoleWarnDev( [ 'Detected a large number of updates inside startTransition. ' + 'If this is due to a subscription please re-write it to use React provided hooks. ' + @@ -70,15 +70,14 @@ describe('ReactStartTransition', () => { {withoutStack: true}, ); - await expect(async () => { - await act(() => { - triggerHookTransition(() => { - subs.forEach(setState => { - setState(state => state + 1); - }); + await act(() => { + triggerHookTransition(() => { + subs.forEach(setState => { + setState(state => state + 1); }); }); - }).toWarnDev( + }); + assertConsoleWarnDev( [ 'Detected a large number of updates inside startTransition. ' + 'If this is due to a subscription please re-write it to use React provided hooks. ' + diff --git a/packages/react/src/__tests__/ReactStrictMode-test.js b/packages/react/src/__tests__/ReactStrictMode-test.js index f28c70a871665..6d44a28881fad 100644 --- a/packages/react/src/__tests__/ReactStrictMode-test.js +++ b/packages/react/src/__tests__/ReactStrictMode-test.js @@ -19,6 +19,7 @@ let useMemo; let useState; let useReducer; let assertConsoleErrorDev; +let assertConsoleWarnDev; describe('ReactStrictMode', () => { beforeEach(() => { @@ -27,7 +28,11 @@ describe('ReactStrictMode', () => { ReactDOM = require('react-dom'); ReactDOMClient = require('react-dom/client'); ReactDOMServer = require('react-dom/server'); - ({act, assertConsoleErrorDev} = require('internal-test-utils')); + ({ + act, + assertConsoleErrorDev, + assertConsoleWarnDev, + } = require('internal-test-utils')); useMemo = React.useMemo; useState = React.useState; useReducer = React.useReducer; @@ -40,20 +45,19 @@ describe('ReactStrictMode', () => { const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); - await expect(async () => { - await act(() => { - root.render( - - - , - ); - }); - }).toErrorDev( + await act(() => { + root.render( + + + , + ); + }); + assertConsoleErrorDev([ 'Invalid ARIA attribute `ariaTypo`. ' + 'ARIA attributes follow the pattern aria-* and must be lowercase.\n' + ' in div (at **)\n' + ' in Foo (at **)', - ); + ]); }); it('should appear in the SSR component stack', () => { @@ -61,18 +65,17 @@ describe('ReactStrictMode', () => { return
; } - expect(() => { - ReactDOMServer.renderToString( - - - , - ); - }).toErrorDev( + ReactDOMServer.renderToString( + + + , + ); + assertConsoleErrorDev([ 'Invalid ARIA attribute `ariaTypo`. ' + 'ARIA attributes follow the pattern aria-* and must be lowercase.\n' + ' in div (at **)\n' + ' in Foo (at **)', - ); + ]); }); // @gate __DEV__ @@ -620,9 +623,8 @@ describe('Concurrent Mode', () => { const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); - await expect( - async () => await act(() => root.render()), - ).toErrorDev( + await act(() => root.render()); + assertConsoleErrorDev( [ `Using UNSAFE_componentWillMount in strict mode is not recommended and may indicate bugs in your code. See https://react.dev/link/unsafe-component-lifecycles for details. @@ -681,31 +683,29 @@ Please update the following components: App`, const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); - await expect(async () => { - await expect( - async () => await act(() => root.render()), - ).toErrorDev( - [ - `Using UNSAFE_componentWillMount in strict mode is not recommended and may indicate bugs in your code. See https://react.dev/link/unsafe-component-lifecycles for details. + await act(() => root.render()); + assertConsoleErrorDev( + [ + `Using UNSAFE_componentWillMount in strict mode is not recommended and may indicate bugs in your code. See https://react.dev/link/unsafe-component-lifecycles for details. * Move code with side effects to componentDidMount, and set initial state in the constructor. Please update the following components: App`, - `Using UNSAFE_componentWillReceiveProps in strict mode is not recommended and may indicate bugs in your code. See https://react.dev/link/unsafe-component-lifecycles for details. + `Using UNSAFE_componentWillReceiveProps in strict mode is not recommended and may indicate bugs in your code. See https://react.dev/link/unsafe-component-lifecycles for details. * Move data fetching code or side effects to componentDidUpdate. * If you're updating state whenever props change, refactor your code to use memoization techniques or move it to static getDerivedStateFromProps. Learn more at: https://react.dev/link/derived-state Please update the following components: Child`, - `Using UNSAFE_componentWillUpdate in strict mode is not recommended and may indicate bugs in your code. See https://react.dev/link/unsafe-component-lifecycles for details. + `Using UNSAFE_componentWillUpdate in strict mode is not recommended and may indicate bugs in your code. See https://react.dev/link/unsafe-component-lifecycles for details. * Move data fetching code or side effects to componentDidUpdate. Please update the following components: App`, - ], - {withoutStack: true}, - ); - }).toWarnDev( + ], + {withoutStack: true}, + ); + assertConsoleWarnDev( [ `componentWillMount has been renamed, and is not recommended for use. See https://react.dev/link/unsafe-component-lifecycles for details. @@ -752,17 +752,25 @@ Please update the following components: Parent`, const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); - await expect(async () => { - await act(() => root.render()); - }).toErrorDev( - 'Using UNSAFE_componentWillMount in strict mode is not recommended', + await act(() => root.render()); + assertConsoleErrorDev( + [ + 'Using UNSAFE_componentWillMount in strict mode is not recommended and may indicate bugs in your code. ' + + 'See https://react.dev/link/unsafe-component-lifecycles for details.\n\n' + + '* Move code with side effects to componentDidMount, and set initial state in the constructor.\n\n' + + 'Please update the following components: Foo', + ], {withoutStack: true}, ); - await expect(async () => { - await act(() => root.render()); - }).toErrorDev( - 'Using UNSAFE_componentWillMount in strict mode is not recommended', + await act(() => root.render()); + assertConsoleErrorDev( + [ + 'Using UNSAFE_componentWillMount in strict mode is not recommended and may indicate bugs in your code. ' + + 'See https://react.dev/link/unsafe-component-lifecycles for details.\n\n' + + '* Move code with side effects to componentDidMount, and set initial state in the constructor.\n\n' + + 'Please update the following components: Bar', + ], {withoutStack: true}, ); @@ -810,12 +818,20 @@ Please update the following components: Parent`, const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); - await expect(async () => { - await act(() => { - root.render(); - }); - }).toErrorDev( - 'Using UNSAFE_componentWillReceiveProps in strict mode is not recommended', + await act(() => { + root.render(); + }); + assertConsoleErrorDev( + [ + 'Using UNSAFE_componentWillReceiveProps in strict mode is not recommended ' + + 'and may indicate bugs in your code. ' + + 'See https://react.dev/link/unsafe-component-lifecycles for details.\n\n' + + '* Move data fetching code or side effects to componentDidUpdate.\n' + + "* If you're updating state whenever props change, " + + 'refactor your code to use memoization techniques or move it to ' + + 'static getDerivedStateFromProps. Learn more at: https://react.dev/link/derived-state\n\n' + + 'Please update the following components: Bar, Foo', + ], {withoutStack: true}, ); diff --git a/packages/react/src/__tests__/ReactTypeScriptClass-test.ts b/packages/react/src/__tests__/ReactTypeScriptClass-test.ts index 00e3a8b3ff8a8..5f51cc6f38c77 100644 --- a/packages/react/src/__tests__/ReactTypeScriptClass-test.ts +++ b/packages/react/src/__tests__/ReactTypeScriptClass-test.ts @@ -16,9 +16,11 @@ import ReactDOM = require('react-dom'); import ReactDOMClient = require('react-dom/client'); import PropTypes = require('prop-types'); import ReactFeatureFlags = require('shared/ReactFeatureFlags'); +import TestUtils = require('internal-test-utils'); // Before Each - +const assertConsoleErrorDev = TestUtils.assertConsoleErrorDev; +const assertConsoleWarnDev = TestUtils.assertConsoleWarnDev; let container; let root; let attachedListener = null; @@ -313,19 +315,19 @@ class ClassicRefs extends React.Component { // Describe the actual test cases. -describe('ReactTypeScriptClass', function() { - beforeEach(function() { +describe('ReactTypeScriptClass', function () { + beforeEach(function () { container = document.createElement('div'); root = ReactDOMClient.createRoot(container); attachedListener = null; renderedName = null; }); - it('preserves the name of the class for use in error messages', function() { + it('preserves the name of the class for use in error messages', function () { expect(Empty.name).toBe('Empty'); }); - it('throws if no render function is defined', function() { + it('throws if no render function is defined', function () { class Foo extends React.Component {} const caughtErrors = []; function errorHandler(event) { @@ -334,14 +336,15 @@ describe('ReactTypeScriptClass', function() { } window.addEventListener('error', errorHandler); try { - expect(() => { - ReactDOM.flushSync(() => root.render(React.createElement(Empty))) - }).toErrorDev([ + ReactDOM.flushSync(() => root.render(React.createElement(Empty))); + assertConsoleErrorDev([ // A failed component renders twice in DEV in concurrent mode 'No `render` method found on the Empty instance: ' + - 'you may have forgotten to define `render`.', + 'you may have forgotten to define `render`.\n' + + ' in Empty (at **)', 'No `render` method found on the Empty instance: ' + - 'you may have forgotten to define `render`.', + 'you may have forgotten to define `render`.\n' + + ' in Empty (at **)', ]); } finally { window.removeEventListener('error', errorHandler); @@ -349,31 +352,31 @@ describe('ReactTypeScriptClass', function() { expect(caughtErrors.length).toBe(1); }); - it('renders a simple stateless component with prop', function() { + it('renders a simple stateless component with prop', function () { test(React.createElement(SimpleStateless, {bar: 'foo'}), 'DIV', 'foo'); test(React.createElement(SimpleStateless, {bar: 'bar'}), 'DIV', 'bar'); }); - it('renders based on state using initial values in this.props', function() { + it('renders based on state using initial values in this.props', function () { test( React.createElement(InitialState, {initialValue: 'foo'}), 'SPAN', - 'foo' + 'foo', ); }); - it('renders based on state using props in the constructor', function() { + it('renders based on state using props in the constructor', function () { const ref = React.createRef(); test( React.createElement(StateBasedOnProps, {initialValue: 'foo', ref: ref}), 'DIV', - 'foo' + 'foo', ); ReactDOM.flushSync(() => ref.current.changeState()); test(React.createElement(StateBasedOnProps), 'SPAN', 'bar'); }); - it('sets initial state with value returned by static getDerivedStateFromProps', function() { + it('sets initial state with value returned by static getDerivedStateFromProps', function () { class Foo extends React.Component { state = { foo: null, @@ -394,7 +397,7 @@ describe('ReactTypeScriptClass', function() { test(React.createElement(Foo, {foo: 'foo'}), 'DIV', 'foo bar'); }); - it('warns if getDerivedStateFromProps is not static', function() { + it('warns if getDerivedStateFromProps is not static', function () { class Foo extends React.Component { getDerivedStateFromProps() { return {}; @@ -403,17 +406,17 @@ describe('ReactTypeScriptClass', function() { return React.createElement('div', {}); } } - expect(function() { - ReactDOM.flushSync(() => - root.render(React.createElement(Foo, {foo: 'foo'})) - ); - }).toErrorDev( - 'Foo: getDerivedStateFromProps() is defined as an instance method ' + - 'and will be ignored. Instead, declare it as a static method.' + ReactDOM.flushSync(() => + root.render(React.createElement(Foo, {foo: 'foo'})), ); + assertConsoleErrorDev([ + 'Foo: getDerivedStateFromProps() is defined as an instance method ' + + 'and will be ignored. Instead, declare it as a static method.\n' + + ' in Foo (at **)', + ]); }); - it('warns if getDerivedStateFromError is not static', function() { + it('warns if getDerivedStateFromError is not static', function () { class Foo extends React.Component { getDerivedStateFromError() { return {}; @@ -422,34 +425,34 @@ describe('ReactTypeScriptClass', function() { return React.createElement('div'); } } - expect(function() { - ReactDOM.flushSync(() => - root.render(React.createElement(Foo, {foo: 'foo'})) - ); - }).toErrorDev( - 'Foo: getDerivedStateFromError() is defined as an instance method ' + - 'and will be ignored. Instead, declare it as a static method.' + ReactDOM.flushSync(() => + root.render(React.createElement(Foo, {foo: 'foo'})), ); + assertConsoleErrorDev([ + 'Foo: getDerivedStateFromError() is defined as an instance method ' + + 'and will be ignored. Instead, declare it as a static method.\n' + + ' in Foo (at **)', + ]); }); - it('warns if getSnapshotBeforeUpdate is static', function() { + it('warns if getSnapshotBeforeUpdate is static', function () { class Foo extends React.Component { static getSnapshotBeforeUpdate() {} render() { return React.createElement('div', {}); } } - expect(function() { - ReactDOM.flushSync(() => - root.render(React.createElement(Foo, {foo: 'foo'})) - ); - }).toErrorDev( - 'Foo: getSnapshotBeforeUpdate() is defined as a static method ' + - 'and will be ignored. Instead, declare it as an instance method.' + ReactDOM.flushSync(() => + root.render(React.createElement(Foo, {foo: 'foo'})), ); + assertConsoleErrorDev([ + 'Foo: getSnapshotBeforeUpdate() is defined as a static method ' + + 'and will be ignored. Instead, declare it as an instance method.\n' + + ' in Foo (at **)', + ]); }); - it('warns if state not initialized before static getDerivedStateFromProps', function() { + it('warns if state not initialized before static getDerivedStateFromProps', function () { class Foo extends React.Component { static getDerivedStateFromProps(nextProps, prevState) { return { @@ -463,19 +466,19 @@ describe('ReactTypeScriptClass', function() { }); } } - expect(function() { - ReactDOM.flushSync(() => - root.render(React.createElement(Foo, {foo: 'foo'})) - ); - }).toErrorDev( + ReactDOM.flushSync(() => + root.render(React.createElement(Foo, {foo: 'foo'})), + ); + assertConsoleErrorDev([ '`Foo` uses `getDerivedStateFromProps` but its initial state is ' + 'undefined. This is not recommended. Instead, define the initial state by ' + 'assigning an object to `this.state` in the constructor of `Foo`. ' + - 'This ensures that `getDerivedStateFromProps` arguments have a consistent shape.' - ); + 'This ensures that `getDerivedStateFromProps` arguments have a consistent shape.\n' + + ' in Foo (at **)', + ]); }); - it('updates initial state with values returned by static getDerivedStateFromProps', function() { + it('updates initial state with values returned by static getDerivedStateFromProps', function () { class Foo extends React.Component { state = { foo: 'foo', @@ -495,7 +498,7 @@ describe('ReactTypeScriptClass', function() { test(React.createElement(Foo), 'DIV', 'not-foo bar'); }); - it('renders updated state with values returned by static getDerivedStateFromProps', function() { + it('renders updated state with values returned by static getDerivedStateFromProps', function () { class Foo extends React.Component { state = { value: 'initial', @@ -517,66 +520,80 @@ describe('ReactTypeScriptClass', function() { }); if (!ReactFeatureFlags.disableLegacyContext) { - it('renders based on context in the constructor', function() { - expect(() => test(React.createElement(ProvideChildContextTypes), 'SPAN', 'foo')).toErrorDev([ - 'ProvideChildContextTypes uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.', - 'StateBasedOnContext uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.' + it('renders based on context in the constructor', function () { + test(React.createElement(ProvideChildContextTypes), 'SPAN', 'foo'); + assertConsoleErrorDev([ + 'ProvideChildContextTypes uses the legacy childContextTypes API which will soon be removed. ' + + 'Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' + + ' in ProvideChildContextTypes (at **)', + 'StateBasedOnContext uses the legacy contextTypes API which will soon be removed. ' + + 'Use React.createContext() with static contextType instead. (https://react.dev/link/legacy-context)\n' + + (ReactFeatureFlags.enableOwnerStacks + ? ' in ProvideChildContextTypes.Object..ProvideChildContextTypes (at **)' + : ' in StateBasedOnContext (at **)\n') + + ' in ProvideChildContextTypes (at **)', ]); }); } - it('renders only once when setting state in componentWillMount', function() { + it('renders only once when setting state in componentWillMount', function () { renderCount = 0; test(React.createElement(RenderOnce, {initialValue: 'foo'}), 'SPAN', 'bar'); expect(renderCount).toBe(1); }); - it('should warn with non-object in the initial state property', function() { - expect(() => test(React.createElement(ArrayState), 'SPAN', '')).toErrorDev( - 'ArrayState.state: must be set to an object or null' - ); - expect(() => test(React.createElement(StringState), 'SPAN', '')).toErrorDev( - 'StringState.state: must be set to an object or null' - ); - expect(() => test(React.createElement(NumberState), 'SPAN', '')).toErrorDev( - 'NumberState.state: must be set to an object or null' - ); + it('should warn with non-object in the initial state property', function () { + test(React.createElement(ArrayState), 'SPAN', ''); + assertConsoleErrorDev([ + 'ArrayState.state: must be set to an object or null\n' + + ' in ArrayState (at **)', + ]); + test(React.createElement(StringState), 'SPAN', ''); + assertConsoleErrorDev([ + 'StringState.state: must be set to an object or null\n' + + ' in StringState (at **)', + ]); + test(React.createElement(NumberState), 'SPAN', ''); + assertConsoleErrorDev([ + 'NumberState.state: must be set to an object or null\n' + + ' in NumberState (at **)', + ]); }); - it('should render with null in the initial state property', function() { + it('should render with null in the initial state property', function () { test(React.createElement(NullState), 'SPAN', ''); }); - it('setState through an event handler', function() { + it('setState through an event handler', function () { test( React.createElement(BoundEventHandler, {initialValue: 'foo'}), 'DIV', - 'foo' + 'foo', ); ReactDOM.flushSync(() => attachedListener()); expect(renderedName).toBe('bar'); }); - it('should not implicitly bind event handlers', function() { + it('should not implicitly bind event handlers', function () { test( React.createElement(UnboundEventHandler, {initialValue: 'foo'}), 'DIV', - 'foo' + 'foo', ); expect(attachedListener).toThrow(); }); - it('renders using forceUpdate even when there is no state', function() { + it('renders using forceUpdate even when there is no state', function () { test( React.createElement(ForceUpdateWithNoState, {initialValue: 'foo'}), 'DIV', - 'foo' + 'foo', ); ReactDOM.flushSync(() => attachedListener()); expect(renderedName).toBe('bar'); }); - it('will call all the normal life cycle methods', function() { + it('will call all the normal life cycle methods', function () { lifeCycles = []; test(React.createElement(NormalLifeCycles, {value: 'foo'}), 'SPAN', 'foo'); expect(lifeCycles).toEqual(['will-mount', 'did-mount']); @@ -604,22 +621,29 @@ describe('ReactTypeScriptClass', function() { it( 'warns when classic properties are defined on the instance, ' + 'but does not invoke them.', - function() { + function () { getInitialStateWasCalled = false; getDefaultPropsWasCalled = false; - expect(() => - test(React.createElement(ClassicProperties), 'SPAN', 'foo') - ).toErrorDev([ - 'getInitialState was defined on ClassicProperties, ' + - 'a plain JavaScript class.', - 'getDefaultProps was defined on ClassicProperties, ' + - 'a plain JavaScript class.', - 'contextTypes was defined as an instance property on ClassicProperties.', - 'contextType was defined as an instance property on ClassicProperties.', + test(React.createElement(ClassicProperties), 'SPAN', 'foo'); + assertConsoleErrorDev([ + 'getInitialState was defined on ClassicProperties, a plain JavaScript class. ' + + 'This is only supported for classes created using React.createClass. ' + + 'Did you mean to define a state property instead?\n' + + ' in ClassicProperties (at **)', + 'getDefaultProps was defined on ClassicProperties, a plain JavaScript class. ' + + 'This is only supported for classes created using React.createClass. ' + + 'Use a static property to define defaultProps instead.\n' + + ' in ClassicProperties (at **)', + 'contextType was defined as an instance property on ClassicProperties. ' + + 'Use a static property to define contextType instead.\n' + + ' in ClassicProperties (at **)', + 'contextTypes was defined as an instance property on ClassicProperties. ' + + 'Use a static property to define contextTypes instead.\n' + + ' in ClassicProperties (at **)', ]); expect(getInitialStateWasCalled).toBe(false); expect(getDefaultPropsWasCalled).toBe(false); - } + }, ); } @@ -638,63 +662,73 @@ describe('ReactTypeScriptClass', function() { } test(React.createElement(Example), 'SPAN', 'foo'); - } + }, ); - it('should warn when misspelling shouldComponentUpdate', function() { - expect(() => - test(React.createElement(MisspelledComponent1), 'SPAN', 'foo') - ).toErrorDev( - '' + - 'MisspelledComponent1 has a method called componentShouldUpdate(). Did ' + + it('should warn when misspelling shouldComponentUpdate', function () { + test(React.createElement(MisspelledComponent1), 'SPAN', 'foo'); + assertConsoleErrorDev([ + 'MisspelledComponent1 has a method called componentShouldUpdate(). Did ' + 'you mean shouldComponentUpdate()? The name is phrased as a question ' + - 'because the function is expected to return a value.' - ); + 'because the function is expected to return a value.\n' + + ' in MisspelledComponent1 (at **)', + ]); }); - it('should warn when misspelling componentWillReceiveProps', function() { - expect(() => - test(React.createElement(MisspelledComponent2), 'SPAN', 'foo') - ).toErrorDev( - '' + - 'MisspelledComponent2 has a method called componentWillRecieveProps(). ' + - 'Did you mean componentWillReceiveProps()?' - ); + it('should warn when misspelling componentWillReceiveProps', function () { + test(React.createElement(MisspelledComponent2), 'SPAN', 'foo'); + assertConsoleErrorDev([ + 'MisspelledComponent2 has a method called componentWillRecieveProps(). ' + + 'Did you mean componentWillReceiveProps()?\n' + + ' in MisspelledComponent2 (at **)', + ]); }); - it('should warn when misspelling UNSAFE_componentWillReceiveProps', function() { - expect(() => - test(React.createElement(MisspelledComponent3), 'SPAN', 'foo') - ).toErrorDev( - '' + - 'MisspelledComponent3 has a method called UNSAFE_componentWillRecieveProps(). ' + - 'Did you mean UNSAFE_componentWillReceiveProps()?' - ); + it('should warn when misspelling UNSAFE_componentWillReceiveProps', function () { + test(React.createElement(MisspelledComponent3), 'SPAN', 'foo'); + assertConsoleErrorDev([ + 'MisspelledComponent3 has a method called UNSAFE_componentWillRecieveProps(). ' + + 'Did you mean UNSAFE_componentWillReceiveProps()?\n' + + ' in MisspelledComponent3 (at **)', + ]); }); - it('should throw AND warn when trying to access classic APIs', function() { + it('should throw AND warn when trying to access classic APIs', function () { const ref = React.createRef(); test(React.createElement(Inner, {name: 'foo', ref: ref}), 'DIV', 'foo'); - expect(() => - expect(() => ref.current.replaceState({})).toThrow() - ).toWarnDev( - 'replaceState(...) is deprecated in plain JavaScript React classes', - {withoutStack: true} + expect(() => ref.current.replaceState({})).toThrow(); + assertConsoleWarnDev( + [ + 'replaceState(...) is deprecated in plain JavaScript React classes. ' + + 'Refactor your code to use setState instead (see https://github.com/facebook/react/issues/3236).', + ], + {withoutStack: true}, ); - expect(() => - expect(() => ref.current.isMounted()).toThrow() - ).toWarnDev( - 'isMounted(...) is deprecated in plain JavaScript React classes', - {withoutStack: true} + expect(() => ref.current.isMounted()).toThrow(); + assertConsoleWarnDev( + [ + 'isMounted(...) is deprecated in plain JavaScript React classes. ' + + 'Instead, make sure to clean up subscriptions and pending requests in ' + + 'componentWillUnmount to prevent memory leaks.', + ], + {withoutStack: true}, ); }); if (!ReactFeatureFlags.disableLegacyContext) { it('supports this.context passed via getChildContext', () => { - expect(() => test(React.createElement(ProvideContext), 'DIV', 'bar-through-context')).toErrorDev([ - 'ProvideContext uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.', - 'ReadContext uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.', -] ); + test(React.createElement(ProvideContext), 'DIV', 'bar-through-context'); + assertConsoleErrorDev([ + 'ProvideContext uses the legacy childContextTypes API which will soon be removed. ' + + 'Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' + + ' in ProvideContext (at **)', + 'ReadContext uses the legacy contextTypes API which will soon be removed. ' + + 'Use React.createContext() with static contextType instead. (https://react.dev/link/legacy-context)\n' + + (ReactFeatureFlags.enableOwnerStacks + ? ' in ProvideContext.Object..ProvideContext (at **)' + : ' in ReadContext (at **)\n') + + ' in ProvideContext (at **)', + ]); }); } }); diff --git a/packages/react/src/__tests__/createReactClassIntegration-test.js b/packages/react/src/__tests__/createReactClassIntegration-test.js index 4aabc11b52e8d..6bd3f0692168c 100644 --- a/packages/react/src/__tests__/createReactClassIntegration-test.js +++ b/packages/react/src/__tests__/createReactClassIntegration-test.js @@ -11,6 +11,7 @@ let act; let assertConsoleErrorDev; +let assertConsoleWarnDev; let PropTypes; let React; @@ -20,7 +21,11 @@ let createReactClass; describe('create-react-class-integration', () => { beforeEach(() => { jest.resetModules(); - ({act, assertConsoleErrorDev} = require('internal-test-utils')); + ({ + act, + assertConsoleErrorDev, + assertConsoleWarnDev, + } = require('internal-test-utils')); PropTypes = require('prop-types'); React = require('react'); ReactDOMClient = require('react-dom/client'); @@ -53,124 +58,130 @@ describe('create-react-class-integration', () => { }); it('should warn on invalid prop types', () => { - expect(() => - createReactClass({ - displayName: 'Component', - propTypes: { - prop: null, - }, - render: function () { - return {this.props.prop}; - }, - }), - ).toErrorDev( - 'Component: prop type `prop` is invalid; ' + - 'it must be a function, usually from React.PropTypes.', + createReactClass({ + displayName: 'Component', + propTypes: { + prop: null, + }, + render: function () { + return {this.props.prop}; + }, + }); + assertConsoleErrorDev( + [ + 'Warning: Component: prop type `prop` is invalid; ' + + 'it must be a function, usually from React.PropTypes.', + ], {withoutStack: true}, ); }); it('should warn on invalid context types', () => { - expect(() => - createReactClass({ - displayName: 'Component', - contextTypes: { - prop: null, - }, - render: function () { - return {this.props.prop}; - }, - }), - ).toErrorDev( - 'Component: context type `prop` is invalid; ' + - 'it must be a function, usually from React.PropTypes.', + createReactClass({ + displayName: 'Component', + contextTypes: { + prop: null, + }, + render: function () { + return {this.props.prop}; + }, + }); + assertConsoleErrorDev( + [ + 'Warning: Component: context type `prop` is invalid; ' + + 'it must be a function, usually from React.PropTypes.', + ], {withoutStack: true}, ); }); it('should throw on invalid child context types', () => { - expect(() => - createReactClass({ - displayName: 'Component', - childContextTypes: { - prop: null, - }, - render: function () { - return {this.props.prop}; - }, - }), - ).toErrorDev( - 'Component: child context type `prop` is invalid; ' + - 'it must be a function, usually from React.PropTypes.', + createReactClass({ + displayName: 'Component', + childContextTypes: { + prop: null, + }, + render: function () { + return {this.props.prop}; + }, + }); + assertConsoleErrorDev( + [ + 'Warning: Component: child context type `prop` is invalid; it must be a function, usually from React.PropTypes.', + ], {withoutStack: true}, ); }); it('should warn when misspelling shouldComponentUpdate', () => { - expect(() => - createReactClass({ - componentShouldUpdate: function () { - return false; - }, - render: function () { - return
; - }, - }), - ).toErrorDev( - 'A component has a method called componentShouldUpdate(). Did you ' + - 'mean shouldComponentUpdate()? The name is phrased as a question ' + - 'because the function is expected to return a value.', + createReactClass({ + componentShouldUpdate: function () { + return false; + }, + render: function () { + return
; + }, + }); + assertConsoleErrorDev( + [ + 'Warning: A component has a method called componentShouldUpdate(). Did you ' + + 'mean shouldComponentUpdate()? The name is phrased as a question ' + + 'because the function is expected to return a value.', + ], {withoutStack: true}, ); - expect(() => - createReactClass({ - displayName: 'NamedComponent', - componentShouldUpdate: function () { - return false; - }, - render: function () { - return
; - }, - }), - ).toErrorDev( - 'NamedComponent has a method called componentShouldUpdate(). Did you ' + - 'mean shouldComponentUpdate()? The name is phrased as a question ' + - 'because the function is expected to return a value.', + createReactClass({ + displayName: 'NamedComponent', + componentShouldUpdate: function () { + return false; + }, + render: function () { + return
; + }, + }); + assertConsoleErrorDev( + [ + 'Warning: NamedComponent has a method called componentShouldUpdate(). Did you ' + + 'mean shouldComponentUpdate()? The name is phrased as a question ' + + 'because the function is expected to return a value.', + ], {withoutStack: true}, ); }); it('should warn when misspelling componentWillReceiveProps', () => { - expect(() => - createReactClass({ - componentWillRecieveProps: function () { - return false; - }, - render: function () { - return
; - }, - }), - ).toErrorDev( - 'A component has a method called componentWillRecieveProps(). Did you ' + - 'mean componentWillReceiveProps()?', + createReactClass({ + componentWillRecieveProps: function () { + return false; + }, + render: function () { + return
; + }, + }); + assertConsoleErrorDev( + [ + 'Warning: A component has a method called componentWillRecieveProps(). Did you ' + + 'mean componentWillReceiveProps()?', + ], {withoutStack: true}, ); }); it('should warn when misspelling UNSAFE_componentWillReceiveProps', () => { - expect(() => - createReactClass({ - UNSAFE_componentWillRecieveProps: function () { - return false; - }, - render: function () { - return
; - }, - }), - ).toErrorDev( - 'A component has a method called UNSAFE_componentWillRecieveProps(). ' + - 'Did you mean UNSAFE_componentWillReceiveProps()?', + createReactClass({ + UNSAFE_componentWillRecieveProps: function () { + return false; + }, + render: function () { + return
; + }, + }); + assertConsoleErrorDev( + [ + 'Warning: A component has a method called UNSAFE_componentWillRecieveProps(). ' + + 'Did you mean UNSAFE_componentWillReceiveProps()?', + ], {withoutStack: true}, ); }); @@ -201,23 +212,22 @@ describe('create-react-class-integration', () => { // TODO: Consider actually moving these to statics or drop this unit test. // eslint-disable-next-line jest/no-disabled-tests it.skip('should warn when using deprecated non-static spec keys', () => { - expect(() => - createReactClass({ - mixins: [{}], - propTypes: { - foo: PropTypes.string, - }, - contextTypes: { - foo: PropTypes.string, - }, - childContextTypes: { - foo: PropTypes.string, - }, - render: function () { - return
; - }, - }), - ).toErrorDev([ + createReactClass({ + mixins: [{}], + propTypes: { + foo: PropTypes.string, + }, + contextTypes: { + foo: PropTypes.string, + }, + childContextTypes: { + foo: PropTypes.string, + }, + render: function () { + return
; + }, + }); + assertConsoleErrorDev([ '`mixins` is now a static property and should ' + 'be defined inside "statics".', '`propTypes` is now a static property and should ' + @@ -399,9 +409,12 @@ describe('create-react-class-integration', () => { }, }); - expect(() => expect(() => Component()).toThrow()).toErrorDev( - 'Something is calling a React component directly. Use a ' + - 'factory or JSX instead. See: https://fb.me/react-legacyfactory', + expect(() => Component()).toThrow(); + assertConsoleErrorDev( + [ + 'Warning: Something is calling a React component directly. Use a ' + + 'factory or JSX instead. See: https://fb.me/react-legacyfactory', + ], {withoutStack: true}, ); }); @@ -504,15 +517,15 @@ describe('create-react-class-integration', () => { return
; }, }); - await expect(async () => { - const root = ReactDOMClient.createRoot(document.createElement('div')); - await act(() => { - root.render(); - }); - }).toErrorDev( + const root = ReactDOMClient.createRoot(document.createElement('div')); + await act(() => { + root.render(); + }); + assertConsoleErrorDev([ 'Foo: getDerivedStateFromProps() is defined as an instance method ' + - 'and will be ignored. Instead, declare it as a static method.', - ); + 'and will be ignored. Instead, declare it as a static method.\n' + + ' in Foo (at **)', + ]); }); it('warns if getDerivedStateFromError is not static', async () => { @@ -525,15 +538,15 @@ describe('create-react-class-integration', () => { return
; }, }); - await expect(async () => { - const root = ReactDOMClient.createRoot(document.createElement('div')); - await act(() => { - root.render(); - }); - }).toErrorDev( + const root = ReactDOMClient.createRoot(document.createElement('div')); + await act(() => { + root.render(); + }); + assertConsoleErrorDev([ 'Foo: getDerivedStateFromError() is defined as an instance method ' + - 'and will be ignored. Instead, declare it as a static method.', - ); + 'and will be ignored. Instead, declare it as a static method.\n' + + ' in Foo (at **)', + ]); }); it('warns if getSnapshotBeforeUpdate is static', async () => { @@ -548,15 +561,15 @@ describe('create-react-class-integration', () => { return
; }, }); - await expect(async () => { - const root = ReactDOMClient.createRoot(document.createElement('div')); - await act(() => { - root.render(); - }); - }).toErrorDev( + const root = ReactDOMClient.createRoot(document.createElement('div')); + await act(() => { + root.render(); + }); + assertConsoleErrorDev([ 'Foo: getSnapshotBeforeUpdate() is defined as a static method ' + - 'and will be ignored. Instead, declare it as an instance method.', - ); + 'and will be ignored. Instead, declare it as an instance method.\n' + + ' in Foo (at **)', + ]); }); it('should warn if state is not properly initialized before getDerivedStateFromProps', async () => { @@ -571,17 +584,17 @@ describe('create-react-class-integration', () => { return null; }, }); - await expect(async () => { - const root = ReactDOMClient.createRoot(document.createElement('div')); - await act(() => { - root.render(); - }); - }).toErrorDev( + const root = ReactDOMClient.createRoot(document.createElement('div')); + await act(() => { + root.render(); + }); + assertConsoleErrorDev([ '`Component` uses `getDerivedStateFromProps` but its initial state is ' + 'null. This is not recommended. Instead, define the initial state by ' + 'assigning an object to `this.state` in the constructor of `Component`. ' + - 'This ensures that `getDerivedStateFromProps` arguments have a consistent shape.', - ); + 'This ensures that `getDerivedStateFromProps` arguments have a consistent shape.\n' + + ' in Component (at **)', + ]); }); it('should not invoke deprecated lifecycles (cWM/cWRP/cWU) if new static gDSFP is present', async () => { @@ -609,30 +622,52 @@ describe('create-react-class-integration', () => { }); Component.displayName = 'Component'; - await expect(async () => { - await expect(async () => { - const root = ReactDOMClient.createRoot(document.createElement('div')); - await act(() => { - root.render(); - }); - }).toErrorDev( - 'Unsafe legacy lifecycles will not be called for components using new component APIs.\n\n' + - 'Component uses getDerivedStateFromProps() but also contains the following legacy lifecycles:\n' + - ' componentWillMount\n' + - ' componentWillReceiveProps\n' + - ' componentWillUpdate\n\n' + - 'The above lifecycles should be removed. Learn more about this warning here:\n' + - 'https://react.dev/link/unsafe-component-lifecycles', - ); - }).toWarnDev( + const root = ReactDOMClient.createRoot(document.createElement('div')); + await act(() => { + root.render(); + }); + assertConsoleErrorDev([ + 'Unsafe legacy lifecycles will not be called for components using new component APIs.\n\n' + + 'Component uses getDerivedStateFromProps() but also contains the following legacy lifecycles:\n' + + ' componentWillMount\n' + + ' componentWillReceiveProps\n' + + ' componentWillUpdate\n\n' + + 'The above lifecycles should be removed. Learn more about this warning here:\n' + + 'https://react.dev/link/unsafe-component-lifecycles\n' + + ' in Component (at **)', + ]); + assertConsoleWarnDev( [ - 'componentWillMount has been renamed', - 'componentWillReceiveProps has been renamed', - 'componentWillUpdate has been renamed', + 'componentWillMount has been renamed, and is not recommended for use. ' + + 'See https://react.dev/link/unsafe-component-lifecycles for details.\n\n' + + '* Move code with side effects to componentDidMount, and set initial state in the constructor.\n' + + '* Rename componentWillMount to UNSAFE_componentWillMount to suppress ' + + 'this warning in non-strict mode. In React 18.x, only the UNSAFE_ name will work. ' + + 'To rename all deprecated lifecycles to their new names, you can run ' + + '`npx react-codemod rename-unsafe-lifecycles` in your project source folder.\n' + + '\nPlease update the following components: Component', + 'componentWillReceiveProps has been renamed, and is not recommended for use. ' + + 'See https://react.dev/link/unsafe-component-lifecycles for details.\n\n' + + '* Move data fetching code or side effects to componentDidUpdate.\n' + + "* If you're updating state whenever props change, refactor your " + + 'code to use memoization techniques or move it to ' + + 'static getDerivedStateFromProps. Learn more at: https://react.dev/link/derived-state\n' + + '* Rename componentWillReceiveProps to UNSAFE_componentWillReceiveProps to suppress ' + + 'this warning in non-strict mode. In React 18.x, only the UNSAFE_ name will work. ' + + 'To rename all deprecated lifecycles to their new names, you can run ' + + '`npx react-codemod rename-unsafe-lifecycles` in your project source folder.\n' + + '\nPlease update the following components: Component', + 'componentWillUpdate has been renamed, and is not recommended for use. ' + + 'See https://react.dev/link/unsafe-component-lifecycles for details.\n\n' + + '* Move data fetching code or side effects to componentDidUpdate.\n' + + '* Rename componentWillUpdate to UNSAFE_componentWillUpdate to suppress ' + + 'this warning in non-strict mode. In React 18.x, only the UNSAFE_ name will work. ' + + 'To rename all deprecated lifecycles to their new names, you can run ' + + '`npx react-codemod rename-unsafe-lifecycles` in your project source folder.\n' + + '\nPlease update the following components: Component', ], {withoutStack: true}, ); - const root = ReactDOMClient.createRoot(document.createElement('div')); await act(() => { root.render(); }); @@ -659,26 +694,49 @@ describe('create-react-class-integration', () => { }); Component.displayName = 'Component'; - await expect(async () => { - await expect(async () => { - const root = ReactDOMClient.createRoot(document.createElement('div')); - await act(() => { - root.render(); - }); - }).toErrorDev( - 'Unsafe legacy lifecycles will not be called for components using new component APIs.\n\n' + - 'Component uses getSnapshotBeforeUpdate() but also contains the following legacy lifecycles:\n' + - ' componentWillMount\n' + - ' componentWillReceiveProps\n' + - ' componentWillUpdate\n\n' + - 'The above lifecycles should be removed. Learn more about this warning here:\n' + - 'https://react.dev/link/unsafe-component-lifecycles', - ); - }).toWarnDev( + const root = ReactDOMClient.createRoot(document.createElement('div')); + await act(() => { + root.render(); + }); + assertConsoleErrorDev([ + 'Unsafe legacy lifecycles will not be called for components using new component APIs.\n\n' + + 'Component uses getSnapshotBeforeUpdate() but also contains the following legacy lifecycles:\n' + + ' componentWillMount\n' + + ' componentWillReceiveProps\n' + + ' componentWillUpdate\n\n' + + 'The above lifecycles should be removed. Learn more about this warning here:\n' + + 'https://react.dev/link/unsafe-component-lifecycles\n' + + ' in Component (at **)', + ]); + assertConsoleWarnDev( [ - 'componentWillMount has been renamed', - 'componentWillReceiveProps has been renamed', - 'componentWillUpdate has been renamed', + 'componentWillMount has been renamed, and is not recommended for use. ' + + 'See https://react.dev/link/unsafe-component-lifecycles for details.\n\n' + + '* Move code with side effects to componentDidMount, and set initial state in the constructor.\n' + + '* Rename componentWillMount to UNSAFE_componentWillMount to suppress ' + + 'this warning in non-strict mode. In React 18.x, only the UNSAFE_ name will work. ' + + 'To rename all deprecated lifecycles to their new names, you can run ' + + '`npx react-codemod rename-unsafe-lifecycles` in your project source folder.\n' + + '\nPlease update the following components: Component', + 'componentWillReceiveProps has been renamed, and is not recommended for use. ' + + 'See https://react.dev/link/unsafe-component-lifecycles for details.\n\n' + + '* Move data fetching code or side effects to componentDidUpdate.\n' + + "* If you're updating state whenever props change, refactor your " + + 'code to use memoization techniques or move it to ' + + 'static getDerivedStateFromProps. Learn more at: https://react.dev/link/derived-state\n' + + '* Rename componentWillReceiveProps to UNSAFE_componentWillReceiveProps to suppress ' + + 'this warning in non-strict mode. In React 18.x, only the UNSAFE_ name will work. ' + + 'To rename all deprecated lifecycles to their new names, you can run ' + + '`npx react-codemod rename-unsafe-lifecycles` in your project source folder.\n' + + '\nPlease update the following components: Component', + 'componentWillUpdate has been renamed, and is not recommended for use. ' + + 'See https://react.dev/link/unsafe-component-lifecycles for details.\n\n' + + '* Move data fetching code or side effects to componentDidUpdate.\n' + + '* Rename componentWillUpdate to UNSAFE_componentWillUpdate to suppress ' + + 'this warning in non-strict mode. In React 18.x, only the UNSAFE_ name will work. ' + + 'To rename all deprecated lifecycles to their new names, you can run ' + + '`npx react-codemod rename-unsafe-lifecycles` in your project source folder.\n' + + '\nPlease update the following components: Component', ], {withoutStack: true}, ); @@ -721,15 +779,38 @@ describe('create-react-class-integration', () => { const root = ReactDOMClient.createRoot(document.createElement('div')); - await expect(async () => { - await act(() => { - root.render(); - }); - }).toWarnDev( + await act(() => { + root.render(); + }); + assertConsoleWarnDev( [ - 'componentWillMount has been renamed', - 'componentWillReceiveProps has been renamed', - 'componentWillUpdate has been renamed', + 'componentWillMount has been renamed, and is not recommended for use. ' + + 'See https://react.dev/link/unsafe-component-lifecycles for details.\n\n' + + '* Move code with side effects to componentDidMount, and set initial state in the constructor.\n' + + '* Rename componentWillMount to UNSAFE_componentWillMount to suppress ' + + 'this warning in non-strict mode. In React 18.x, only the UNSAFE_ name will work. ' + + 'To rename all deprecated lifecycles to their new names, you can run ' + + '`npx react-codemod rename-unsafe-lifecycles` in your project source folder.\n' + + '\nPlease update the following components: Component', + 'componentWillReceiveProps has been renamed, and is not recommended for use. ' + + 'See https://react.dev/link/unsafe-component-lifecycles for details.\n\n' + + '* Move data fetching code or side effects to componentDidUpdate.\n' + + "* If you're updating state whenever props change, refactor your " + + 'code to use memoization techniques or move it to ' + + 'static getDerivedStateFromProps. Learn more at: https://react.dev/link/derived-state\n' + + '* Rename componentWillReceiveProps to UNSAFE_componentWillReceiveProps to suppress ' + + 'this warning in non-strict mode. In React 18.x, only the UNSAFE_ name will work. ' + + 'To rename all deprecated lifecycles to their new names, you can run ' + + '`npx react-codemod rename-unsafe-lifecycles` in your project source folder.\n' + + '\nPlease update the following components: Component', + 'componentWillUpdate has been renamed, and is not recommended for use. ' + + 'See https://react.dev/link/unsafe-component-lifecycles for details.\n\n' + + '* Move data fetching code or side effects to componentDidUpdate.\n' + + '* Rename componentWillUpdate to UNSAFE_componentWillUpdate to suppress ' + + 'this warning in non-strict mode. In React 18.x, only the UNSAFE_ name will work. ' + + 'To rename all deprecated lifecycles to their new names, you can run ' + + '`npx react-codemod rename-unsafe-lifecycles` in your project source folder.\n' + + '\nPlease update the following components: Component', ], {withoutStack: true}, ); @@ -803,14 +884,16 @@ describe('create-react-class-integration', () => { const root = ReactDOMClient.createRoot(document.createElement('div')); - await expect(async () => { - await act(() => { - root.render(); - }); - }).toErrorDev( - 'MyComponent: isMounted is deprecated. Instead, make sure to ' + - 'clean up subscriptions and pending requests in componentWillUnmount ' + - 'to prevent memory leaks.', + await act(() => { + root.render(); + }); + assertConsoleErrorDev( + [ + 'Warning: MyComponent: isMounted is deprecated. Instead, make sure to ' + + 'clean up subscriptions and pending requests in componentWillUnmount ' + + 'to prevent memory leaks.\n' + + ' in MyComponent (at **)', + ], // This now has a component stack even though it's part of a third-party library. ); diff --git a/scripts/jest/spec-equivalence-reporter/setupTests.js b/scripts/jest/spec-equivalence-reporter/setupTests.js index 487e0d3003078..c3eaf5690afcb 100644 --- a/scripts/jest/spec-equivalence-reporter/setupTests.js +++ b/scripts/jest/spec-equivalence-reporter/setupTests.js @@ -7,6 +7,11 @@ 'use strict'; +const { + patchConsoleMethods, + resetAllUnexpectedConsoleCalls, + flushAllUnexpectedConsoleCalls, +} = require('internal-test-utils/consoleMock'); const spyOn = jest.spyOn; // Spying on console methods in production builds can mask errors. @@ -36,6 +41,11 @@ global.spyOnProd = function (...args) { } }; +// Patch the console to assert that all console error/warn/log calls assert. +patchConsoleMethods({includeLog: !!process.env.CI}); +beforeEach(resetAllUnexpectedConsoleCalls); +afterEach(flushAllUnexpectedConsoleCalls); + expect.extend({ ...require('../matchers/reactTestMatchers'), ...require('../matchers/toThrow'), From 97d794958f5b19b66a980f737facd890463f0cb8 Mon Sep 17 00:00:00 2001 From: Ricky Date: Mon, 23 Dec 2024 18:11:04 -0500 Subject: [PATCH 0013/1160] [assert helpers] Remove toWarnDev from fixtures/dom (#31894) This is unused and never was: https://github.com/facebook/react/commit/e6a0473c3c6f501dbe291f60b9ee35760ab99eed --- fixtures/dom/src/__tests__/nested-act-test.js | 2 - fixtures/dom/src/toWarnDev.js | 284 ------------------ 2 files changed, 286 deletions(-) delete mode 100644 fixtures/dom/src/toWarnDev.js diff --git a/fixtures/dom/src/__tests__/nested-act-test.js b/fixtures/dom/src/__tests__/nested-act-test.js index 6c7f60c2e2f13..4a26e272cb37d 100644 --- a/fixtures/dom/src/__tests__/nested-act-test.js +++ b/fixtures/dom/src/__tests__/nested-act-test.js @@ -14,8 +14,6 @@ let TestAct; global.__DEV__ = process.env.NODE_ENV !== 'production'; -expect.extend(require('../toWarnDev')); - describe('unmocked scheduler', () => { beforeEach(() => { jest.resetModules(); diff --git a/fixtures/dom/src/toWarnDev.js b/fixtures/dom/src/toWarnDev.js deleted file mode 100644 index 6e275241e98fa..0000000000000 --- a/fixtures/dom/src/toWarnDev.js +++ /dev/null @@ -1,284 +0,0 @@ -// copied from scripts/jest/matchers/toWarnDev.js -'use strict'; - -const {diff: jestDiff} = require('jest-diff'); -const util = require('util'); - -function shouldIgnoreConsoleError(format, args) { - if (__DEV__) { - if (typeof format === 'string') { - if (format.indexOf('The above error occurred') === 0) { - // This looks like an error addendum from ReactFiberErrorLogger. - // Ignore it too. - return true; - } - } - } else { - if ( - format != null && - typeof format.message === 'string' && - typeof format.stack === 'string' && - args.length === 0 - ) { - // In production, ReactFiberErrorLogger logs error objects directly. - // They are noisy too so we'll try to ignore them. - return true; - } - } - // Looks legit - return false; -} - -function normalizeCodeLocInfo(str) { - return str && str.replace(/at .+?:\d+/g, 'at **'); -} - -const createMatcherFor = consoleMethod => - function matcher(callback, expectedMessages, options = {}) { - if (__DEV__) { - // Warn about incorrect usage of matcher. - if (typeof expectedMessages === 'string') { - expectedMessages = [expectedMessages]; - } else if (!Array.isArray(expectedMessages)) { - throw Error( - `toWarnDev() requires a parameter of type string or an array of strings ` + - `but was given ${typeof expectedMessages}.` - ); - } - if ( - options != null && - (typeof options !== 'object' || Array.isArray(options)) - ) { - throw new Error( - 'toWarnDev() second argument, when present, should be an object. ' + - 'Did you forget to wrap the messages into an array?' - ); - } - if (arguments.length > 3) { - // `matcher` comes from Jest, so it's more than 2 in practice - throw new Error( - 'toWarnDev() received more than two arguments. ' + - 'Did you forget to wrap the messages into an array?' - ); - } - - const withoutStack = options.withoutStack; - const warningsWithoutComponentStack = []; - const warningsWithComponentStack = []; - const unexpectedWarnings = []; - - let lastWarningWithMismatchingFormat = null; - let lastWarningWithExtraComponentStack = null; - - // Catch errors thrown by the callback, - // But only rethrow them if all test expectations have been satisfied. - // Otherwise an Error in the callback can mask a failed expectation, - // and result in a test that passes when it shouldn't. - let caughtError; - - const isLikelyAComponentStack = message => - typeof message === 'string' && message.includes('\n in '); - - const consoleSpy = (format, ...args) => { - // Ignore uncaught errors reported by jsdom - // and React addendums because they're too noisy. - if ( - consoleMethod === 'error' && - shouldIgnoreConsoleError(format, args) - ) { - return; - } - - const message = util.format(format, ...args); - const normalizedMessage = normalizeCodeLocInfo(message); - - // Remember if the number of %s interpolations - // doesn't match the number of arguments. - // We'll fail the test if it happens. - let argIndex = 0; - format.replace(/%s/g, () => argIndex++); - if (argIndex !== args.length) { - lastWarningWithMismatchingFormat = { - format, - args, - expectedArgCount: argIndex, - }; - } - - // Protect against accidentally passing a component stack - // to warning() which already injects the component stack. - if ( - args.length >= 2 && - isLikelyAComponentStack(args[args.length - 1]) && - isLikelyAComponentStack(args[args.length - 2]) - ) { - lastWarningWithExtraComponentStack = { - format, - }; - } - - for (let index = 0; index < expectedMessages.length; index++) { - const expectedMessage = expectedMessages[index]; - if ( - normalizedMessage === expectedMessage || - normalizedMessage.includes(expectedMessage) - ) { - if (isLikelyAComponentStack(normalizedMessage)) { - warningsWithComponentStack.push(normalizedMessage); - } else { - warningsWithoutComponentStack.push(normalizedMessage); - } - expectedMessages.splice(index, 1); - return; - } - } - - let errorMessage; - if (expectedMessages.length === 0) { - errorMessage = - 'Unexpected warning recorded: ' + - this.utils.printReceived(normalizedMessage); - } else if (expectedMessages.length === 1) { - errorMessage = - 'Unexpected warning recorded: ' + - jestDiff(expectedMessages[0], normalizedMessage); - } else { - errorMessage = - 'Unexpected warning recorded: ' + - jestDiff(expectedMessages, [normalizedMessage]); - } - - // Record the call stack for unexpected warnings. - // We don't throw an Error here though, - // Because it might be suppressed by ReactFiberScheduler. - unexpectedWarnings.push(new Error(errorMessage)); - }; - - // TODO Decide whether we need to support nested toWarn* expectations. - // If we don't need it, add a check here to see if this is already our spy, - // And throw an error. - const originalMethod = console[consoleMethod]; - - // Avoid using Jest's built-in spy since it can't be removed. - console[consoleMethod] = consoleSpy; - - try { - callback(); - } catch (error) { - caughtError = error; - } finally { - // Restore the unspied method so that unexpected errors fail tests. - console[consoleMethod] = originalMethod; - - // Any unexpected Errors thrown by the callback should fail the test. - // This should take precedence since unexpected errors could block warnings. - if (caughtError) { - throw caughtError; - } - - // Any unexpected warnings should be treated as a failure. - if (unexpectedWarnings.length > 0) { - return { - message: () => unexpectedWarnings[0].stack, - pass: false, - }; - } - - // Any remaining messages indicate a failed expectations. - if (expectedMessages.length > 0) { - return { - message: () => - `Expected warning was not recorded:\n ${this.utils.printReceived( - expectedMessages[0] - )}`, - pass: false, - }; - } - - if (typeof withoutStack === 'number') { - // We're expecting a particular number of warnings without stacks. - if (withoutStack !== warningsWithoutComponentStack.length) { - return { - message: () => - `Expected ${withoutStack} warnings without a component stack but received ${warningsWithoutComponentStack.length}:\n` + - warningsWithoutComponentStack.map(warning => - this.utils.printReceived(warning) - ), - pass: false, - }; - } - } else if (withoutStack === true) { - // We're expecting that all warnings won't have the stack. - // If some warnings have it, it's an error. - if (warningsWithComponentStack.length > 0) { - return { - message: () => - `Received warning unexpectedly includes a component stack:\n ${this.utils.printReceived( - warningsWithComponentStack[0] - )}\nIf this warning intentionally includes the component stack, remove ` + - `{withoutStack: true} from the toWarnDev() call. If you have a mix of ` + - `warnings with and without stack in one toWarnDev() call, pass ` + - `{withoutStack: N} where N is the number of warnings without stacks.`, - pass: false, - }; - } - } else if (withoutStack === false || withoutStack === undefined) { - // We're expecting that all warnings *do* have the stack (default). - // If some warnings don't have it, it's an error. - if (warningsWithoutComponentStack.length > 0) { - return { - message: () => - `Received warning unexpectedly does not include a component stack:\n ${this.utils.printReceived( - warningsWithoutComponentStack[0] - )}\nIf this warning intentionally omits the component stack, add ` + - `{withoutStack: true} to the toWarnDev() call.`, - pass: false, - }; - } - } else { - throw Error( - `The second argument for toWarnDev(), when specified, must be an object. It may have a ` + - `property called "withoutStack" whose value may be undefined, boolean, or a number. ` + - `Instead received ${typeof withoutStack}.` - ); - } - - if (lastWarningWithMismatchingFormat !== null) { - return { - message: () => - `Received ${ - lastWarningWithMismatchingFormat.args.length - } arguments for a message with ${ - lastWarningWithMismatchingFormat.expectedArgCount - } placeholders:\n ${this.utils.printReceived( - lastWarningWithMismatchingFormat.format - )}`, - pass: false, - }; - } - - if (lastWarningWithExtraComponentStack !== null) { - return { - message: () => - `Received more than one component stack for a warning:\n ${this.utils.printReceived( - lastWarningWithExtraComponentStack.format - )}\nDid you accidentally pass a stack to warning() as the last argument? ` + - `Don't forget warning() already injects the component stack automatically.`, - pass: false, - }; - } - - return {pass: true}; - } - } else { - // Any uncaught errors or warnings should fail tests in production mode. - callback(); - - return {pass: true}; - } - }; - -module.exports = { - toLowPriorityWarnDev: createMatcherFor('warn'), - toWarnDev: createMatcherFor('error'), -}; From fc8a898dd126198305fce458edd084c5d9c4b67a Mon Sep 17 00:00:00 2001 From: lauren Date: Thu, 26 Dec 2024 14:58:37 -0500 Subject: [PATCH 0014/1160] [compiler] Fix broken fire snapshot (#31920) This was not committed in #31811 --- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/facebook/react/pull/31920). * #31919 * #31918 * #31917 * #31916 * #31915 * __->__ #31920 --- ...rror.invalid-rewrite-deps-spread.expect.md | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-rewrite-deps-spread.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-rewrite-deps-spread.expect.md index 7c1b55f61d909..91c5523564cdd 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-rewrite-deps-spread.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-rewrite-deps-spread.expect.md @@ -12,9 +12,12 @@ function Component(props) { const deps = [foo, props]; - useEffect(() => { - fire(foo(props)); - }, ...deps); + useEffect( + () => { + fire(foo(props)); + }, + ...deps + ); return null; } @@ -25,13 +28,13 @@ function Component(props) { ## Error ``` - 11 | useEffect(() => { - 12 | fire(foo(props)); -> 13 | }, ...deps); - | ^^^^ Invariant: Cannot compile `fire`. You must use an array literal for an effect dependency array when that effect uses `fire()` (13:13) - 14 | - 15 | return null; - 16 | } + 13 | fire(foo(props)); + 14 | }, +> 15 | ...deps + | ^^^^ Invariant: Cannot compile `fire`. You must use an array literal for an effect dependency array when that effect uses `fire()` (15:15) + 16 | ); + 17 | + 18 | return null; ``` \ No newline at end of file From 4309bde2b4faa044dc6266142378a330c030e053 Mon Sep 17 00:00:00 2001 From: lauren Date: Fri, 27 Dec 2024 14:27:43 -0500 Subject: [PATCH 0015/1160] [rcr] Relax react peer dep requirement (#31915) There's no real reason to restrict the React peer dep to non-experimental, so relax it. --- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/facebook/react/pull/31915). * #31919 * #31918 * #31917 * #31916 * __->__ #31915 * #31920 --- compiler/packages/react-compiler-runtime/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compiler/packages/react-compiler-runtime/package.json b/compiler/packages/react-compiler-runtime/package.json index 575ec847a5807..d72f168c56aad 100644 --- a/compiler/packages/react-compiler-runtime/package.json +++ b/compiler/packages/react-compiler-runtime/package.json @@ -9,7 +9,7 @@ "src" ], "peerDependencies": { - "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + "react": "^17.0.0 || ^18.0.0 || ^19.0.0 || ^0.0.0-experimental" }, "scripts": { "build": "rimraf dist && rollup --config --bundleConfigAsCjs", From d4ac7689f94f8ed53b779a651d62a2b9af20e6c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Sat, 28 Dec 2024 02:01:49 -0500 Subject: [PATCH 0016/1160] Add Profiler mode to fixtures even if React DevTools is not installed (#31877) Currently you need to do one of either: 1. Install React DevTools 2. Install React Refresh 3. Add Profiler component To opt in to component level profiling. It was a bit confusing that some of the fixtures was doing 2 which made them work while other was depending on if you had DevTools. Really React Refresh shouldn't really opt you in I think. --- fixtures/flight/src/index.js | 24 +++++++++++++++--------- fixtures/ssr/src/index.js | 8 +++++++- 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/fixtures/flight/src/index.js b/fixtures/flight/src/index.js index 755551047535b..f08f7a110bf61 100644 --- a/fixtures/flight/src/index.js +++ b/fixtures/flight/src/index.js @@ -1,5 +1,5 @@ import * as React from 'react'; -import {use, Suspense, useState, startTransition} from 'react'; +import {use, Suspense, useState, startTransition, Profiler} from 'react'; import ReactDOM from 'react-dom/client'; import {createFromFetch, encodeReply} from 'react-server-dom-webpack/client'; @@ -54,14 +54,20 @@ async function hydrateApp() { } ); - ReactDOM.hydrateRoot(document, , { - // TODO: This part doesn't actually work because the server only returns - // form state during the request that submitted the form. Which means it - // the state needs to be transported as part of the HTML stream. We intend - // to add a feature to Fizz for this, but for now it's up to the - // metaframework to implement correctly. - formState: formState, - }); + ReactDOM.hydrateRoot( + document, + + + , + { + // TODO: This part doesn't actually work because the server only returns + // form state during the request that submitted the form. Which means it + // the state needs to be transported as part of the HTML stream. We intend + // to add a feature to Fizz for this, but for now it's up to the + // metaframework to implement correctly. + formState: formState, + } + ); } // Remove this line to simulate MPA behavior diff --git a/fixtures/ssr/src/index.js b/fixtures/ssr/src/index.js index f6457ce570674..bac5be6ec62e2 100644 --- a/fixtures/ssr/src/index.js +++ b/fixtures/ssr/src/index.js @@ -1,6 +1,12 @@ import React from 'react'; +import {Profiler} from 'react'; import {hydrateRoot} from 'react-dom/client'; import App from './components/App'; -hydrateRoot(document, ); +hydrateRoot( + document, + + + +); From 50f00fd876b0b92b243cd8b54a222f9577446392 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Sat, 28 Dec 2024 02:02:16 -0500 Subject: [PATCH 0017/1160] [Flight] Mark Errored Server Components (#31879) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is similar to #31876 but for Server Components. It marks them as errored and puts the error message in the Summary properties. Screenshot 2024-12-20 at 5 05 35 PM This only looks at the current chunk for rejections. That means that there might still be promises deeper that rejected but it's only the immediate return value of the Server Component that's considered a rejection of the component itself. --- .../react-client/src/ReactFlightClient.js | 38 ++++++++++++---- .../src/ReactFlightPerformanceTrack.js | 43 +++++++++++++++++++ 2 files changed, 73 insertions(+), 8 deletions(-) diff --git a/packages/react-client/src/ReactFlightClient.js b/packages/react-client/src/ReactFlightClient.js index 30d340ac56700..7c424d45b3183 100644 --- a/packages/react-client/src/ReactFlightClient.js +++ b/packages/react-client/src/ReactFlightClient.js @@ -73,6 +73,7 @@ import { markAllTracksInOrder, logComponentRender, logDedupedComponentRender, + logComponentErrored, } from './ReactFlightPerformanceTrack'; import { @@ -2876,6 +2877,7 @@ function flushComponentPerformance( if (debugInfo) { let endTime = 0; + let isLastComponent = true; for (let i = debugInfo.length - 1; i >= 0; i--) { const info = debugInfo[i]; if (typeof info.time === 'number') { @@ -2890,17 +2892,37 @@ function flushComponentPerformance( const startTimeInfo = debugInfo[i - 1]; if (typeof startTimeInfo.time === 'number') { const startTime = startTimeInfo.time; - logComponentRender( - componentInfo, - trackIdx, - startTime, - endTime, - childrenEndTime, - response._rootEnvironmentName, - ); + if ( + isLastComponent && + root.status === ERRORED && + root.reason !== response._closedReason + ) { + // If this is the last component to render before this chunk rejected, then conceptually + // this component errored. If this was a cancellation then it wasn't this component that + // errored. + logComponentErrored( + componentInfo, + trackIdx, + startTime, + endTime, + childrenEndTime, + response._rootEnvironmentName, + root.reason, + ); + } else { + logComponentRender( + componentInfo, + trackIdx, + startTime, + endTime, + childrenEndTime, + response._rootEnvironmentName, + ); + } // Track the root most component of the result for deduping logging. result.component = componentInfo; } + isLastComponent = false; } } } diff --git a/packages/react-client/src/ReactFlightPerformanceTrack.js b/packages/react-client/src/ReactFlightPerformanceTrack.js index d2860e407cc65..123878526d3eb 100644 --- a/packages/react-client/src/ReactFlightPerformanceTrack.js +++ b/packages/react-client/src/ReactFlightPerformanceTrack.js @@ -102,6 +102,49 @@ export function logComponentRender( } } +export function logComponentErrored( + componentInfo: ReactComponentInfo, + trackIdx: number, + startTime: number, + endTime: number, + childrenEndTime: number, + rootEnv: string, + error: mixed, +): void { + if (supportsUserTiming) { + const properties = []; + if (__DEV__) { + const message = + typeof error === 'object' && + error !== null && + typeof error.message === 'string' + ? // eslint-disable-next-line react-internal/safe-string-coercion + String(error.message) + : // eslint-disable-next-line react-internal/safe-string-coercion + String(error); + properties.push(['Error', message]); + } + const env = componentInfo.env; + const name = componentInfo.name; + const isPrimaryEnv = env === rootEnv; + const entryName = + isPrimaryEnv || env === undefined ? name : name + ' [' + env + ']'; + performance.measure(entryName, { + start: startTime < 0 ? 0 : startTime, + end: childrenEndTime, + detail: { + devtools: { + color: 'error', + track: trackNames[trackIdx], + trackGroup: COMPONENTS_TRACK, + tooltipText: entryName + ' Errored', + properties, + }, + }, + }); + } +} + export function logDedupedComponentRender( componentInfo: ReactComponentInfo, trackIdx: number, From c01b8058e6e2d50a5c1ed69f1bdd4c541bf4aa92 Mon Sep 17 00:00:00 2001 From: Ruslan Lesiutin Date: Sun, 29 Dec 2024 15:36:21 +0000 Subject: [PATCH 0018/1160] DevTools: fix Compiler inegration test with 18.2 (#31904) Currently failing with `TypeError: Invalid Version: 19`, looks like I've overlooked this one in https://github.com/facebook/react/pull/31241. --- scripts/ci/download_devtools_regression_build.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/ci/download_devtools_regression_build.js b/scripts/ci/download_devtools_regression_build.js index 7195c194927b9..bdb2c0441b5c4 100755 --- a/scripts/ci/download_devtools_regression_build.js +++ b/scripts/ci/download_devtools_regression_build.js @@ -110,7 +110,7 @@ async function downloadRegressionBuild() { ); } - if (semver.gte(reactVersion, '18.2.0') && semver.lt(reactVersion, '19')) { + if (semver.gte(reactVersion, '18.2.0') && semver.lt(reactVersion, '19.0.0')) { console.log(chalk.white(`Downloading react-compiler-runtime\n`)); await exec( `npm install --prefix ${REGRESSION_FOLDER} react-compiler-runtime` From 694d3e1aae4ae9e29fe8de8ad246662be65f5e0b Mon Sep 17 00:00:00 2001 From: Devon Govett Date: Tue, 31 Dec 2024 13:13:43 -0500 Subject: [PATCH 0019/1160] [Flight Parcel] Implement prepareDestinationForModule (#31799) Followup to #31725 This implements `prepareDestinationForModule` in the Parcel Flight client. On the Parcel side, the `` component now only inserts `` elements for stylesheets (along with a bootstrap script when needed), and React is responsible for inserting scripts. This ensures that components that are conditionally dynamic imported during render are also preloaded. CSS must be added to the RSC tree using `` to avoid FOUC. This must be manually rendered in both the top-level page, and in any component that is dynamic imported. It would be nice if there was a way for React to automatically insert CSS as well, but unfortunately `prepareDestinationForModule` only knows about client components and not CSS for server components. Perhaps there could be a way we could annotate components at code splitting boundaries with the resources they need? More thoughts in this thread: https://github.com/facebook/react/pull/31725#discussion_r1884867607 --- fixtures/flight-parcel/package.json | 8 +- fixtures/flight-parcel/src/server.tsx | 7 +- fixtures/flight-parcel/types.d.ts | 10 +- fixtures/flight-parcel/yarn.lock | 847 +++++++++--------- ...ctFlightClientConfig.dom-browser-parcel.js | 1 + ...ReactFlightClientConfig.dom-edge-parcel.js | 1 + ...ReactFlightClientConfig.dom-node-parcel.js | 1 + .../ReactFlightClientConfigBundlerParcel.js | 33 +- ...ctFlightClientConfigTargetParcelBrowser.js | 18 + ...actFlightClientConfigTargetParcelServer.js | 21 + scripts/flow/environment.js | 3 + 11 files changed, 497 insertions(+), 453 deletions(-) create mode 100644 packages/react-server-dom-parcel/src/client/ReactFlightClientConfigTargetParcelBrowser.js create mode 100644 packages/react-server-dom-parcel/src/client/ReactFlightClientConfigTargetParcelServer.js diff --git a/fixtures/flight-parcel/package.json b/fixtures/flight-parcel/package.json index c991e46bfe5e4..d2abb05a8f558 100644 --- a/fixtures/flight-parcel/package.json +++ b/fixtures/flight-parcel/package.json @@ -18,7 +18,7 @@ "scripts": { "predev": "cp -r ../../build/oss-experimental/* ./node_modules/", "prebuild": "cp -r ../../build/oss-experimental/* ./node_modules/", - "dev": "concurrently \"npm run dev:watch\" \"npm run dev:start\"", + "dev": "concurrently \"npm run dev:watch\" \"sleep 2 && npm run dev:start\"", "dev:watch": "NODE_ENV=development parcel watch", "dev:start": "NODE_ENV=development node dist/server.js", "build": "parcel build", @@ -28,8 +28,8 @@ "packageExports": true }, "dependencies": { - "@parcel/config-default": "2.0.0-dev.1789", - "@parcel/runtime-rsc": "2.13.3-dev.3412", + "@parcel/config-default": "2.0.0-dev.1795", + "@parcel/runtime-rsc": "2.13.3-dev.3418", "@types/parcel-env": "^0.0.6", "@types/express": "*", "@types/node": "^22.10.1", @@ -37,7 +37,7 @@ "@types/react-dom": "^19", "concurrently": "^7.3.0", "express": "^4.18.2", - "parcel": "2.0.0-dev.1787", + "parcel": "2.0.0-dev.1793", "process": "^0.11.10", "react": "experimental", "react-dom": "experimental", diff --git a/fixtures/flight-parcel/src/server.tsx b/fixtures/flight-parcel/src/server.tsx index eaf367216e2a7..810fbb628ad3d 100644 --- a/fixtures/flight-parcel/src/server.tsx +++ b/fixtures/flight-parcel/src/server.tsx @@ -15,8 +15,8 @@ import {injectRSCPayload} from 'rsc-html-stream/server'; // Client dependencies, used for SSR. // These must run in the same environment as client components (e.g. same instance of React). -import {createFromReadableStream} from 'react-server-dom-parcel/client' with {env: 'react-client'}; -import {renderToReadableStream as renderHTMLToReadableStream} from 'react-dom/server' with {env: 'react-client'}; +import {createFromReadableStream} from 'react-server-dom-parcel/client.edge' with {env: 'react-client'}; +import {renderToReadableStream as renderHTMLToReadableStream} from 'react-dom/server.edge' with {env: 'react-client'}; import ReactClient, {ReactElement} from 'react' with {env: 'react-client'}; // Page components. These must have "use server-entry" so they are treated as code splitting entry points. @@ -66,8 +66,9 @@ async function render( // Use client react to render the RSC payload to HTML. let [s1, s2] = stream.tee(); - let data = createFromReadableStream(s1); + let data: Promise; function Content() { + data ??= createFromReadableStream(s1); return ReactClient.use(data); } diff --git a/fixtures/flight-parcel/types.d.ts b/fixtures/flight-parcel/types.d.ts index 93c632db7bd73..ab7748dc8cb06 100644 --- a/fixtures/flight-parcel/types.d.ts +++ b/fixtures/flight-parcel/types.d.ts @@ -2,13 +2,16 @@ declare module 'react-server-dom-parcel/client' { export function createFromFetch(res: Promise): Promise; - export function createFromReadableStream(stream: ReadableStream): Promise; export function encodeReply(value: any): Promise; type CallServerCallback = (id: string, args: any[]) => Promise; export function setServerCallback(cb: CallServerCallback): void; } +declare module 'react-server-dom-parcel/client.edge' { + export function createFromReadableStream(stream: ReadableStream): Promise; +} + declare module 'react-server-dom-parcel/server.edge' { export function renderToReadableStream(value: any): ReadableStream; export function loadServerAction(id: string): Promise<(...args: any[]) => any>; @@ -17,5 +20,10 @@ declare module 'react-server-dom-parcel/server.edge' { } declare module '@parcel/runtime-rsc' { + import {JSX} from 'react'; export function Resources(): JSX.Element; } + +declare module 'react-dom/server.edge' { + export * from 'react-dom/server'; +} diff --git a/fixtures/flight-parcel/yarn.lock b/fixtures/flight-parcel/yarn.lock index 326acb6a22693..384a25dd74176 100644 --- a/fixtures/flight-parcel/yarn.lock +++ b/fixtures/flight-parcel/yarn.lock @@ -104,100 +104,100 @@ resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.3.tgz#0aa5502d547b57abfc4ac492de68e2006e417242" integrity sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ== -"@parcel/bundler-default@2.0.0-dev.1789+0b82b13d6": - version "2.0.0-dev.1789" - resolved "https://registry.yarnpkg.com/@parcel/bundler-default/-/bundler-default-2.0.0-dev.1789.tgz#815e0e6c96046621eead2fb0c71c1ac3337b39ec" - integrity sha512-UCXy0G/kbJnB5fLbzU9dRdmRT3dbPAFvyHSWgOXeNZG1qn2ckGXQJFEoIfhtvpoGPUb4b0EM+vM4mOwnkXYziQ== - dependencies: - "@parcel/diagnostic" "2.0.0-dev.1789+0b82b13d6" - "@parcel/graph" "3.3.3-dev.3412+0b82b13d6" - "@parcel/plugin" "2.0.0-dev.1789+0b82b13d6" - "@parcel/rust" "2.13.3-dev.3412+0b82b13d6" - "@parcel/utils" "2.0.0-dev.1789+0b82b13d6" +"@parcel/bundler-default@2.0.0-dev.1795+9f297b15c": + version "2.0.0-dev.1795" + resolved "https://registry.yarnpkg.com/@parcel/bundler-default/-/bundler-default-2.0.0-dev.1795.tgz#0fff101f37e3defe676c4b66bce234e563c25825" + integrity sha512-nK82Osr6A6ZjYeRfy2KT6197rD+5SzMPQ9LqMLbh8h/WoKye8ywAYSyeckDp7GQZa8aqFZXpVqUtmI0SvdUcIQ== + dependencies: + "@parcel/diagnostic" "2.0.0-dev.1795+9f297b15c" + "@parcel/graph" "3.3.3-dev.3418+9f297b15c" + "@parcel/plugin" "2.0.0-dev.1795+9f297b15c" + "@parcel/rust" "2.13.3-dev.3418+9f297b15c" + "@parcel/utils" "2.0.0-dev.1795+9f297b15c" nullthrows "^1.1.1" -"@parcel/cache@2.0.0-dev.1789+0b82b13d6": - version "2.0.0-dev.1789" - resolved "https://registry.yarnpkg.com/@parcel/cache/-/cache-2.0.0-dev.1789.tgz#19e63615bba7b1a0871fef38263ed89759722bd8" - integrity sha512-/Vzv6w8JE+vgPA+5zXIntgXRLeg9YA6nOMhA2OGyjHnMouNeyUggPjalTtv+upxkPwRyME4DT4fq8Pzx/EsC6w== +"@parcel/cache@2.0.0-dev.1795+9f297b15c": + version "2.0.0-dev.1795" + resolved "https://registry.yarnpkg.com/@parcel/cache/-/cache-2.0.0-dev.1795.tgz#2d7a97fa5c276b6cff60561e38b91cb88a8e834a" + integrity sha512-RUkCwGK/2qpGFtZslaNPp+/uPnh2YsFVk2XoLpw3H0JY9K8dDawlkfhFKz0r5nYfUYvhFiO1QPyC+20NWX1ovw== dependencies: - "@parcel/fs" "2.0.0-dev.1789+0b82b13d6" - "@parcel/logger" "2.0.0-dev.1789+0b82b13d6" - "@parcel/utils" "2.0.0-dev.1789+0b82b13d6" + "@parcel/fs" "2.0.0-dev.1795+9f297b15c" + "@parcel/logger" "2.0.0-dev.1795+9f297b15c" + "@parcel/utils" "2.0.0-dev.1795+9f297b15c" lmdb "2.8.5" -"@parcel/codeframe@2.0.0-dev.1789+0b82b13d6": - version "2.0.0-dev.1789" - resolved "https://registry.yarnpkg.com/@parcel/codeframe/-/codeframe-2.0.0-dev.1789.tgz#60ce9718e8d7d472d472f2d1e8bbd9dc8af7e8ac" - integrity sha512-a6loCDmJHg/87bAL7+Nu4fN09ByXqO16vUGpO/YCZk0Ub6CypCqE7UX+t4OQFmpImGy52OkbuMSeAFNPKb+FyQ== +"@parcel/codeframe@2.0.0-dev.1795+9f297b15c": + version "2.0.0-dev.1795" + resolved "https://registry.yarnpkg.com/@parcel/codeframe/-/codeframe-2.0.0-dev.1795.tgz#a9957966df6a4ac65b01f7ccf94db06414cf7bb2" + integrity sha512-xNk/Xw3mqo+qjmKrNKECqEmOILsrCBU40iUkldE0LgaRip9wR0fODtj2+KacP12ttCHbrgCdrela/AFV3FinKQ== dependencies: chalk "^4.1.2" -"@parcel/compressor-raw@2.13.3-dev.3412+0b82b13d6": - version "2.13.3-dev.3412" - resolved "https://registry.yarnpkg.com/@parcel/compressor-raw/-/compressor-raw-2.13.3-dev.3412.tgz#b61fa5341a51c28587e82c86f8dbc3e0ae3452ec" - integrity sha512-rF+LwyX8X9o48cIfVOZGl1YcR1VZlH/C0TkLab8yEDeue7PwKaVJczdJDR1z+1KHQt7bZRtAu+Thz/PjSeHP4w== - dependencies: - "@parcel/plugin" "2.0.0-dev.1789+0b82b13d6" - -"@parcel/config-default@2.0.0-dev.1789", "@parcel/config-default@2.0.0-dev.1789+0b82b13d6": - version "2.0.0-dev.1789" - resolved "https://registry.yarnpkg.com/@parcel/config-default/-/config-default-2.0.0-dev.1789.tgz#5dc90f7733cf5b49a62448dfe5603de47ce9c364" - integrity sha512-mfXeHjrCQaT+8N9TjAmNHq2rA7JPTux1hDjVg5A84gkwXTkO2V4j8GTARq6Bdz2MoH7PuKcDNwsU8eQpMzpTEg== - dependencies: - "@parcel/bundler-default" "2.0.0-dev.1789+0b82b13d6" - "@parcel/compressor-raw" "2.13.3-dev.3412+0b82b13d6" - "@parcel/namer-default" "2.0.0-dev.1789+0b82b13d6" - "@parcel/optimizer-css" "2.13.3-dev.3412+0b82b13d6" - "@parcel/optimizer-htmlnano" "2.0.0-dev.1789+0b82b13d6" - "@parcel/optimizer-image" "2.13.3-dev.3412+0b82b13d6" - "@parcel/optimizer-svgo" "2.13.3-dev.3412+0b82b13d6" - "@parcel/optimizer-swc" "2.13.3-dev.3412+0b82b13d6" - "@parcel/packager-css" "2.0.0-dev.1789+0b82b13d6" - "@parcel/packager-html" "2.0.0-dev.1789+0b82b13d6" - "@parcel/packager-js" "2.0.0-dev.1789+0b82b13d6" - "@parcel/packager-raw" "2.0.0-dev.1789+0b82b13d6" - "@parcel/packager-svg" "2.13.3-dev.3412+0b82b13d6" - "@parcel/packager-wasm" "2.13.3-dev.3412+0b82b13d6" - "@parcel/reporter-dev-server" "2.0.0-dev.1789+0b82b13d6" - "@parcel/resolver-default" "2.0.0-dev.1789+0b82b13d6" - "@parcel/runtime-browser-hmr" "2.0.0-dev.1789+0b82b13d6" - "@parcel/runtime-js" "2.0.0-dev.1789+0b82b13d6" - "@parcel/runtime-react-refresh" "2.0.0-dev.1789+0b82b13d6" - "@parcel/runtime-service-worker" "2.13.3-dev.3412+0b82b13d6" - "@parcel/transformer-babel" "2.0.0-dev.1789+0b82b13d6" - "@parcel/transformer-css" "2.0.0-dev.1789+0b82b13d6" - "@parcel/transformer-html" "2.0.0-dev.1789+0b82b13d6" - "@parcel/transformer-image" "2.13.3-dev.3412+0b82b13d6" - "@parcel/transformer-js" "2.0.0-dev.1789+0b82b13d6" - "@parcel/transformer-json" "2.0.0-dev.1789+0b82b13d6" - "@parcel/transformer-postcss" "2.0.0-dev.1789+0b82b13d6" - "@parcel/transformer-posthtml" "2.0.0-dev.1789+0b82b13d6" - "@parcel/transformer-raw" "2.0.0-dev.1789+0b82b13d6" - "@parcel/transformer-react-refresh-wrap" "2.0.0-dev.1789+0b82b13d6" - "@parcel/transformer-svg" "2.13.3-dev.3412+0b82b13d6" - -"@parcel/core@2.0.0-dev.1787+0b82b13d6": - version "2.0.0-dev.1787" - resolved "https://registry.yarnpkg.com/@parcel/core/-/core-2.0.0-dev.1787.tgz#95c757595d4a9e6dbdf7a7efe3337bfc1c6bd616" - integrity sha512-yyLhl32e4e5cJLFMOPRIMiOZRxh9K7t/KU3E3wOok+BURevMFtPhDlLy94BbJAvYycCw91uiY1bQdyej+MPcQg== +"@parcel/compressor-raw@2.13.3-dev.3418+9f297b15c": + version "2.13.3-dev.3418" + resolved "https://registry.yarnpkg.com/@parcel/compressor-raw/-/compressor-raw-2.13.3-dev.3418.tgz#65823d6bf7d99611f2d8f6d75e3a9c5b9b2ffd7e" + integrity sha512-pz4SaczIsQhMiaCZUehoEXQHZJoBd/T5kvoOcbg7S1chS1CouIwlT6vY4bseRlXoprDBEmK07HCItPF2W2SX+w== + dependencies: + "@parcel/plugin" "2.0.0-dev.1795+9f297b15c" + +"@parcel/config-default@2.0.0-dev.1795", "@parcel/config-default@2.0.0-dev.1795+9f297b15c": + version "2.0.0-dev.1795" + resolved "https://registry.yarnpkg.com/@parcel/config-default/-/config-default-2.0.0-dev.1795.tgz#bae822637ff74e20f19aca3d26348416da9180f3" + integrity sha512-iaXwuLUZJ/L31I5TTeFE/mq6FL6gw15Vh27oEb/KS4pc6eYMvZRzyrIDCEJ6vxpi8VO4r0ncc7fOQqroT+vGIA== + dependencies: + "@parcel/bundler-default" "2.0.0-dev.1795+9f297b15c" + "@parcel/compressor-raw" "2.13.3-dev.3418+9f297b15c" + "@parcel/namer-default" "2.0.0-dev.1795+9f297b15c" + "@parcel/optimizer-css" "2.13.3-dev.3418+9f297b15c" + "@parcel/optimizer-htmlnano" "2.0.0-dev.1795+9f297b15c" + "@parcel/optimizer-image" "2.13.3-dev.3418+9f297b15c" + "@parcel/optimizer-svgo" "2.13.3-dev.3418+9f297b15c" + "@parcel/optimizer-swc" "2.13.3-dev.3418+9f297b15c" + "@parcel/packager-css" "2.0.0-dev.1795+9f297b15c" + "@parcel/packager-html" "2.0.0-dev.1795+9f297b15c" + "@parcel/packager-js" "2.0.0-dev.1795+9f297b15c" + "@parcel/packager-raw" "2.0.0-dev.1795+9f297b15c" + "@parcel/packager-svg" "2.13.3-dev.3418+9f297b15c" + "@parcel/packager-wasm" "2.13.3-dev.3418+9f297b15c" + "@parcel/reporter-dev-server" "2.0.0-dev.1795+9f297b15c" + "@parcel/resolver-default" "2.0.0-dev.1795+9f297b15c" + "@parcel/runtime-browser-hmr" "2.0.0-dev.1795+9f297b15c" + "@parcel/runtime-js" "2.0.0-dev.1795+9f297b15c" + "@parcel/runtime-react-refresh" "2.0.0-dev.1795+9f297b15c" + "@parcel/runtime-service-worker" "2.13.3-dev.3418+9f297b15c" + "@parcel/transformer-babel" "2.0.0-dev.1795+9f297b15c" + "@parcel/transformer-css" "2.0.0-dev.1795+9f297b15c" + "@parcel/transformer-html" "2.0.0-dev.1795+9f297b15c" + "@parcel/transformer-image" "2.13.3-dev.3418+9f297b15c" + "@parcel/transformer-js" "2.0.0-dev.1795+9f297b15c" + "@parcel/transformer-json" "2.0.0-dev.1795+9f297b15c" + "@parcel/transformer-postcss" "2.0.0-dev.1795+9f297b15c" + "@parcel/transformer-posthtml" "2.0.0-dev.1795+9f297b15c" + "@parcel/transformer-raw" "2.0.0-dev.1795+9f297b15c" + "@parcel/transformer-react-refresh-wrap" "2.0.0-dev.1795+9f297b15c" + "@parcel/transformer-svg" "2.13.3-dev.3418+9f297b15c" + +"@parcel/core@2.0.0-dev.1793+9f297b15c": + version "2.0.0-dev.1793" + resolved "https://registry.yarnpkg.com/@parcel/core/-/core-2.0.0-dev.1793.tgz#a911d743f26a3b15476936289b2c1847c21e6e9b" + integrity sha512-0O+crHFX4eoogpP/22sIPtjv4stNbzdeBZIoKqWl6Wndk63UdxVIEn6OvGokelgt/9qng9VoulMxAsrjKodemA== dependencies: "@mischnic/json-sourcemap" "^0.1.0" - "@parcel/cache" "2.0.0-dev.1789+0b82b13d6" - "@parcel/diagnostic" "2.0.0-dev.1789+0b82b13d6" - "@parcel/events" "2.0.0-dev.1789+0b82b13d6" - "@parcel/feature-flags" "2.13.3-dev.3412+0b82b13d6" - "@parcel/fs" "2.0.0-dev.1789+0b82b13d6" - "@parcel/graph" "3.3.3-dev.3412+0b82b13d6" - "@parcel/logger" "2.0.0-dev.1789+0b82b13d6" - "@parcel/package-manager" "2.0.0-dev.1789+0b82b13d6" - "@parcel/plugin" "2.0.0-dev.1789+0b82b13d6" - "@parcel/profiler" "2.13.3-dev.3412+0b82b13d6" - "@parcel/rust" "2.13.3-dev.3412+0b82b13d6" + "@parcel/cache" "2.0.0-dev.1795+9f297b15c" + "@parcel/diagnostic" "2.0.0-dev.1795+9f297b15c" + "@parcel/events" "2.0.0-dev.1795+9f297b15c" + "@parcel/feature-flags" "2.13.3-dev.3418+9f297b15c" + "@parcel/fs" "2.0.0-dev.1795+9f297b15c" + "@parcel/graph" "3.3.3-dev.3418+9f297b15c" + "@parcel/logger" "2.0.0-dev.1795+9f297b15c" + "@parcel/package-manager" "2.0.0-dev.1795+9f297b15c" + "@parcel/plugin" "2.0.0-dev.1795+9f297b15c" + "@parcel/profiler" "2.13.3-dev.3418+9f297b15c" + "@parcel/rust" "2.13.3-dev.3418+9f297b15c" "@parcel/source-map" "^2.1.1" - "@parcel/types" "2.0.0-dev.1789+0b82b13d6" - "@parcel/utils" "2.0.0-dev.1789+0b82b13d6" - "@parcel/workers" "2.0.0-dev.1789+0b82b13d6" + "@parcel/types" "2.0.0-dev.1795+9f297b15c" + "@parcel/utils" "2.0.0-dev.1795+9f297b15c" + "@parcel/workers" "2.0.0-dev.1795+9f297b15c" base-x "^3.0.8" browserslist "^4.6.6" clone "^2.1.1" @@ -208,318 +208,319 @@ nullthrows "^1.1.1" semver "^7.5.2" -"@parcel/diagnostic@2.0.0-dev.1789+0b82b13d6": - version "2.0.0-dev.1789" - resolved "https://registry.yarnpkg.com/@parcel/diagnostic/-/diagnostic-2.0.0-dev.1789.tgz#0be5c2e675c7488f79e26efc3cd8a77500ae3cf5" - integrity sha512-TvTGBOCIeRDgWShhKRL1G/p78kCrqEP7m6k2QHuY3tHWO7rsXr25zUDec4G/u0knpuUwxaivuX5f7b5jPPD7Rg== +"@parcel/diagnostic@2.0.0-dev.1795+9f297b15c": + version "2.0.0-dev.1795" + resolved "https://registry.yarnpkg.com/@parcel/diagnostic/-/diagnostic-2.0.0-dev.1795.tgz#273e049c8f88a19e824c5edf097e391a5b87b0c2" + integrity sha512-uWhJ7ojynh8wL0wr4EcR2wDVCxalsaLunEF/IfIpuWv5F+wEc8cTLsd4eAGjzMOO5jzWP2pK5XSK9TbWl4hTpQ== dependencies: "@mischnic/json-sourcemap" "^0.1.0" nullthrows "^1.1.1" -"@parcel/events@2.0.0-dev.1789+0b82b13d6": - version "2.0.0-dev.1789" - resolved "https://registry.yarnpkg.com/@parcel/events/-/events-2.0.0-dev.1789.tgz#16776859ec582ed05dd24e5edd34a1f7e78170ed" - integrity sha512-jHuRvZrNerTjY7KvqVhtdZgyC4vv2QTE599Fh2EZeEXU7d+SQmDIoMeZwqUPy9Q8FW+SOVvgl1YQ6Mhc/NNyNA== - -"@parcel/feature-flags@2.13.3-dev.3412+0b82b13d6": - version "2.13.3-dev.3412" - resolved "https://registry.yarnpkg.com/@parcel/feature-flags/-/feature-flags-2.13.3-dev.3412.tgz#e823b957d364158d7c189ba179ad20142b8237ba" - integrity sha512-GClA//tlUQHuiBe+Lw/zDP85Ri5F/fZshOZ9Z6q/x3CWrDmdZVwInkQWC9CYsILXPGU7b4ZW6XdEsB6Rbrnt+Q== - -"@parcel/fs@2.0.0-dev.1789+0b82b13d6": - version "2.0.0-dev.1789" - resolved "https://registry.yarnpkg.com/@parcel/fs/-/fs-2.0.0-dev.1789.tgz#49c1b1a9e41ccf70334265adaec696e8f71ce92f" - integrity sha512-9dLlvCCUyY1g6A8tGfIx03Q/T1gZlKWjQ0n3Tla5HiWe+4QG5X0vuToOjVxr66Ju1GJImwj3qNzMv0wkmhj7mw== - dependencies: - "@parcel/feature-flags" "2.13.3-dev.3412+0b82b13d6" - "@parcel/rust" "2.13.3-dev.3412+0b82b13d6" - "@parcel/types-internal" "2.13.3-dev.3412+0b82b13d6" - "@parcel/utils" "2.0.0-dev.1789+0b82b13d6" +"@parcel/events@2.0.0-dev.1795+9f297b15c": + version "2.0.0-dev.1795" + resolved "https://registry.yarnpkg.com/@parcel/events/-/events-2.0.0-dev.1795.tgz#d609260e0a6faa3bf0169936e770002c440e1f24" + integrity sha512-YREBS/8Yulm8wlZbwpbeez38BrOknSrjzBzj1nxL9gIoWLeB9c0zopZK0LFxaC+JDSzbtAsKYb94FoEe08Mo4w== + +"@parcel/feature-flags@2.13.3-dev.3418+9f297b15c": + version "2.13.3-dev.3418" + resolved "https://registry.yarnpkg.com/@parcel/feature-flags/-/feature-flags-2.13.3-dev.3418.tgz#2da678e6725449d7b70428073a0ef660d1c7dd3e" + integrity sha512-oTxIVXGJFjqTmefaugqV2nRsuXpN2aYTP/aJmm1mk06EyrkXTdAMrKoFsPimEshxvu8lGZ2fvZdF3pyWkR39FQ== + +"@parcel/fs@2.0.0-dev.1795+9f297b15c": + version "2.0.0-dev.1795" + resolved "https://registry.yarnpkg.com/@parcel/fs/-/fs-2.0.0-dev.1795.tgz#1dc534e806cc81f79e9aab578fa01bdf63b055aa" + integrity sha512-bY417DPxe921P/lYKiZhBv0fwCtgm5x0vUOhd9xPlAGasQrtqSUMoBwLcRStHd8e1xMrxp9UEJDfESL5+N/UuQ== + dependencies: + "@parcel/feature-flags" "2.13.3-dev.3418+9f297b15c" + "@parcel/rust" "2.13.3-dev.3418+9f297b15c" + "@parcel/types-internal" "2.13.3-dev.3418+9f297b15c" + "@parcel/utils" "2.0.0-dev.1795+9f297b15c" "@parcel/watcher" "^2.0.7" - "@parcel/workers" "2.0.0-dev.1789+0b82b13d6" + "@parcel/workers" "2.0.0-dev.1795+9f297b15c" -"@parcel/graph@3.3.3-dev.3412+0b82b13d6": - version "3.3.3-dev.3412" - resolved "https://registry.yarnpkg.com/@parcel/graph/-/graph-3.3.3-dev.3412.tgz#3913c41f7052c73c4157ddf80b0d6c134e0d45c9" - integrity sha512-ADEMR+Bg+nIjaliyYCGUPK7iyiaNU0x3MNRylg9GMXoB4bfUqCGsWzpR4FYGbSUijVnDOfQfaoHZVI5hwopATg== +"@parcel/graph@3.3.3-dev.3418+9f297b15c": + version "3.3.3-dev.3418" + resolved "https://registry.yarnpkg.com/@parcel/graph/-/graph-3.3.3-dev.3418.tgz#d62963b948b3ec3384ef6da136a23282c974736e" + integrity sha512-L7pOceNtP6dYx82MLrzMBe8uiUXPENoOaj/s6qCvPxjr8x6/Jo8qhIaz0f1htU1AtRMiTZjNL7Vw9Datwe8chg== dependencies: - "@parcel/feature-flags" "2.13.3-dev.3412+0b82b13d6" + "@parcel/feature-flags" "2.13.3-dev.3418+9f297b15c" nullthrows "^1.1.1" -"@parcel/logger@2.0.0-dev.1789+0b82b13d6": - version "2.0.0-dev.1789" - resolved "https://registry.yarnpkg.com/@parcel/logger/-/logger-2.0.0-dev.1789.tgz#fd1f9786210b10d3e3447e824fc17599b1cd7014" - integrity sha512-1StCxPppvriJLiI8bF/gmdHM00qnJTdq8vs83gA8l1OeL/g3LtAyRKznraRwBhMtagN5UB4JKBLIpPaUlTZlng== +"@parcel/logger@2.0.0-dev.1795+9f297b15c": + version "2.0.0-dev.1795" + resolved "https://registry.yarnpkg.com/@parcel/logger/-/logger-2.0.0-dev.1795.tgz#23ea704050e7d43c36f74ca9bd553d75f04c369f" + integrity sha512-haIJ2HRDef8D44nznCToiMB7OkpV2Er8NwOPQm9aJDyl1g4Lqro7cd5tZgEySHe3TlH6r8UWd6+C2ob+ws/jdA== dependencies: - "@parcel/diagnostic" "2.0.0-dev.1789+0b82b13d6" - "@parcel/events" "2.0.0-dev.1789+0b82b13d6" + "@parcel/diagnostic" "2.0.0-dev.1795+9f297b15c" + "@parcel/events" "2.0.0-dev.1795+9f297b15c" -"@parcel/markdown-ansi@2.0.0-dev.1789+0b82b13d6": - version "2.0.0-dev.1789" - resolved "https://registry.yarnpkg.com/@parcel/markdown-ansi/-/markdown-ansi-2.0.0-dev.1789.tgz#089789e3e6b4b272ad6e634f204a3da68dce1892" - integrity sha512-C4erPSJiVtHmbTf9LU9Q8fNwzdvZn9RPeZ8fbbl1k6vcCKBh7i/pTnKI/ijz/cFEa6ThBvim+0FR6BaBa9RtMA== +"@parcel/markdown-ansi@2.0.0-dev.1795+9f297b15c": + version "2.0.0-dev.1795" + resolved "https://registry.yarnpkg.com/@parcel/markdown-ansi/-/markdown-ansi-2.0.0-dev.1795.tgz#410d31e4e7f178ec5039645e8a0e9fa318cb622e" + integrity sha512-HWYo1qJYcmtjKR6VAD4KHVjQ+30kGyxCTHt9oWqxLQeLakXKevWO6tR+htzxyAOH/YFwWeZr3cKstayoFnNkqA== dependencies: chalk "^4.1.2" -"@parcel/namer-default@2.0.0-dev.1789+0b82b13d6": - version "2.0.0-dev.1789" - resolved "https://registry.yarnpkg.com/@parcel/namer-default/-/namer-default-2.0.0-dev.1789.tgz#1260d41e9721e06d4caedaada52dbebe6c0f6f57" - integrity sha512-xKRfmUeGQXpxbAQR/oDRt0ti1bA6OcEmd74MCNQiJnRErNA/qqxiRhqaOYNfIXD80s2cWmIuFy1s1kRB1NiCWA== +"@parcel/namer-default@2.0.0-dev.1795+9f297b15c": + version "2.0.0-dev.1795" + resolved "https://registry.yarnpkg.com/@parcel/namer-default/-/namer-default-2.0.0-dev.1795.tgz#a5f2957fed9b49c819d56d2be7b6e4e996b14176" + integrity sha512-nmbkhtDxaEEzKvAFbhZEX4A/qz35PtjMwIr79sGFmUir7APuITf0MG8d5L9NTXHMgtNXPq+0t1A1gjaCO6aC5g== dependencies: - "@parcel/diagnostic" "2.0.0-dev.1789+0b82b13d6" - "@parcel/plugin" "2.0.0-dev.1789+0b82b13d6" + "@parcel/diagnostic" "2.0.0-dev.1795+9f297b15c" + "@parcel/plugin" "2.0.0-dev.1795+9f297b15c" nullthrows "^1.1.1" -"@parcel/node-resolver-core@3.4.3-dev.3412+0b82b13d6": - version "3.4.3-dev.3412" - resolved "https://registry.yarnpkg.com/@parcel/node-resolver-core/-/node-resolver-core-3.4.3-dev.3412.tgz#dcf90ac15ae9b2e397a57d75aaed625c1b5e0a42" - integrity sha512-tE8S192z/uiDxRgTs8TVw6+XylHsgv7IAkFWw1RfKaWQHxl5Pi57yM6MIl42tgChgcUfhChqf5qkZAiBPgaQ0g== +"@parcel/node-resolver-core@3.4.3-dev.3418+9f297b15c": + version "3.4.3-dev.3418" + resolved "https://registry.yarnpkg.com/@parcel/node-resolver-core/-/node-resolver-core-3.4.3-dev.3418.tgz#cd25a6c86deefe2f4bc1007f26297c5518a196bd" + integrity sha512-qPpgrPc8KEQWbA9HrfNf9KiqFTHqO4YsMhD3DCtFenc5OJM5emt+a/ducqCW7e7W/az2kozhy/T9LJjYDP1PiQ== dependencies: "@mischnic/json-sourcemap" "^0.1.0" - "@parcel/diagnostic" "2.0.0-dev.1789+0b82b13d6" - "@parcel/fs" "2.0.0-dev.1789+0b82b13d6" - "@parcel/rust" "2.13.3-dev.3412+0b82b13d6" - "@parcel/utils" "2.0.0-dev.1789+0b82b13d6" + "@parcel/diagnostic" "2.0.0-dev.1795+9f297b15c" + "@parcel/fs" "2.0.0-dev.1795+9f297b15c" + "@parcel/rust" "2.13.3-dev.3418+9f297b15c" + "@parcel/utils" "2.0.0-dev.1795+9f297b15c" nullthrows "^1.1.1" semver "^7.5.2" -"@parcel/optimizer-css@2.13.3-dev.3412+0b82b13d6": - version "2.13.3-dev.3412" - resolved "https://registry.yarnpkg.com/@parcel/optimizer-css/-/optimizer-css-2.13.3-dev.3412.tgz#6f1d7e6963a49d6586c00a57bedc0dfcf00a5810" - integrity sha512-cNvDfgS6Kp8Y+uEsWgm+I7wnuo6myntvY3cD5osvhybFitmmobosoqNL6aMmMm1onVxYH4QMgqOW06lT5ggmvg== +"@parcel/optimizer-css@2.13.3-dev.3418+9f297b15c": + version "2.13.3-dev.3418" + resolved "https://registry.yarnpkg.com/@parcel/optimizer-css/-/optimizer-css-2.13.3-dev.3418.tgz#d0064debbe5b0fce075182d0c74836fec2b17ecc" + integrity sha512-i8Bl1dcl8+y275prrwzeJFnoovrev+GmOO4T0RMk3hZmcUW+lhUf1vtn/MENQ2sgfQKl8oD1KdMW6tvYeJ7g2g== dependencies: - "@parcel/diagnostic" "2.0.0-dev.1789+0b82b13d6" - "@parcel/plugin" "2.0.0-dev.1789+0b82b13d6" + "@parcel/diagnostic" "2.0.0-dev.1795+9f297b15c" + "@parcel/plugin" "2.0.0-dev.1795+9f297b15c" "@parcel/source-map" "^2.1.1" - "@parcel/utils" "2.0.0-dev.1789+0b82b13d6" + "@parcel/utils" "2.0.0-dev.1795+9f297b15c" browserslist "^4.6.6" lightningcss "^1.22.1" nullthrows "^1.1.1" -"@parcel/optimizer-htmlnano@2.0.0-dev.1789+0b82b13d6": - version "2.0.0-dev.1789" - resolved "https://registry.yarnpkg.com/@parcel/optimizer-htmlnano/-/optimizer-htmlnano-2.0.0-dev.1789.tgz#1874dd4677602ca89c76ca1c03712bf3ea4de229" - integrity sha512-sRCzArD9pwoliZ0h3KQN0ILKQpy9MyTJBuD8Ptwv/IakvwP0bg4dvisW5o3ELH2jyq9KSP3Gz/oJ2fQUF4xm8A== +"@parcel/optimizer-htmlnano@2.0.0-dev.1795+9f297b15c": + version "2.0.0-dev.1795" + resolved "https://registry.yarnpkg.com/@parcel/optimizer-htmlnano/-/optimizer-htmlnano-2.0.0-dev.1795.tgz#f7d3345dbcec03df9cc5db5f9913404ffd6051be" + integrity sha512-+2upe/5VvEe0d0K9ZFMuXwzbvolCCwZlQcu/P4hPAJEM/fs6A6zS3RA3hiVc18LNNz4UEM8sYp9X59dFHzYQnw== dependencies: - "@parcel/diagnostic" "2.0.0-dev.1789+0b82b13d6" - "@parcel/plugin" "2.0.0-dev.1789+0b82b13d6" - "@parcel/utils" "2.0.0-dev.1789+0b82b13d6" + "@parcel/diagnostic" "2.0.0-dev.1795+9f297b15c" + "@parcel/plugin" "2.0.0-dev.1795+9f297b15c" + "@parcel/utils" "2.0.0-dev.1795+9f297b15c" htmlnano "^2.0.0" nullthrows "^1.1.1" posthtml "^0.16.5" -"@parcel/optimizer-image@2.13.3-dev.3412+0b82b13d6": - version "2.13.3-dev.3412" - resolved "https://registry.yarnpkg.com/@parcel/optimizer-image/-/optimizer-image-2.13.3-dev.3412.tgz#3918e075c24749d72f29e12e7ef566d8cbc690ba" - integrity sha512-w+IN2lmrX3f4Vk+WUg+m4hUKFl5ae+0mzXa+0u7MIIaqe136kfSBE53I2E5reqQVoieWybuUfnQYbVUANKjUXg== - dependencies: - "@parcel/diagnostic" "2.0.0-dev.1789+0b82b13d6" - "@parcel/plugin" "2.0.0-dev.1789+0b82b13d6" - "@parcel/rust" "2.13.3-dev.3412+0b82b13d6" - "@parcel/utils" "2.0.0-dev.1789+0b82b13d6" - "@parcel/workers" "2.0.0-dev.1789+0b82b13d6" - -"@parcel/optimizer-svgo@2.13.3-dev.3412+0b82b13d6": - version "2.13.3-dev.3412" - resolved "https://registry.yarnpkg.com/@parcel/optimizer-svgo/-/optimizer-svgo-2.13.3-dev.3412.tgz#643b3596957e163e2c143ca8abfd74ed29489ccf" - integrity sha512-Qm+5F5iFtymzDpR2szA/SrdZgfMrTBmQxFee92XHxGjjlyIujcfBspCMLxkGb5HwUcqgbc+dIXOmOlf4FROCMg== - dependencies: - "@parcel/diagnostic" "2.0.0-dev.1789+0b82b13d6" - "@parcel/plugin" "2.0.0-dev.1789+0b82b13d6" - "@parcel/utils" "2.0.0-dev.1789+0b82b13d6" - -"@parcel/optimizer-swc@2.13.3-dev.3412+0b82b13d6": - version "2.13.3-dev.3412" - resolved "https://registry.yarnpkg.com/@parcel/optimizer-swc/-/optimizer-swc-2.13.3-dev.3412.tgz#49821dd7bdaf0f5f4f3f283f2986fa03fb0558af" - integrity sha512-hpHkrFOyRNRzAfjPpFgJpICVSOmIgfFc3+toR3O8vpV4vhQu8O6uZaRlIt+LZR/5FFI8kVA0RVPkUmTiYg4Arw== - dependencies: - "@parcel/diagnostic" "2.0.0-dev.1789+0b82b13d6" - "@parcel/plugin" "2.0.0-dev.1789+0b82b13d6" +"@parcel/optimizer-image@2.13.3-dev.3418+9f297b15c": + version "2.13.3-dev.3418" + resolved "https://registry.yarnpkg.com/@parcel/optimizer-image/-/optimizer-image-2.13.3-dev.3418.tgz#69de333f47da2e32446db9d29a1c66aa63b04137" + integrity sha512-mQhZLFCPLwjUUVde+obImTy6mVSOF2z/VIs8n94BVOAVz1L/sksWON4eRF3tCdpi43kK7Jr/hHn53gIaveZu7w== + dependencies: + "@parcel/diagnostic" "2.0.0-dev.1795+9f297b15c" + "@parcel/plugin" "2.0.0-dev.1795+9f297b15c" + "@parcel/rust" "2.13.3-dev.3418+9f297b15c" + "@parcel/utils" "2.0.0-dev.1795+9f297b15c" + "@parcel/workers" "2.0.0-dev.1795+9f297b15c" + +"@parcel/optimizer-svgo@2.13.3-dev.3418+9f297b15c": + version "2.13.3-dev.3418" + resolved "https://registry.yarnpkg.com/@parcel/optimizer-svgo/-/optimizer-svgo-2.13.3-dev.3418.tgz#1cd24b71a179a2ffd928f385ace5f4c8697bf795" + integrity sha512-+oGOLNwMtgYo1Vo4lzSOH9LQLekFkHaVmBq+B5WljedbszI2MklYBdrrEzBagjRzIYqinxB4j9t8DaLRvOeYag== + dependencies: + "@parcel/diagnostic" "2.0.0-dev.1795+9f297b15c" + "@parcel/plugin" "2.0.0-dev.1795+9f297b15c" + "@parcel/utils" "2.0.0-dev.1795+9f297b15c" + +"@parcel/optimizer-swc@2.13.3-dev.3418+9f297b15c": + version "2.13.3-dev.3418" + resolved "https://registry.yarnpkg.com/@parcel/optimizer-swc/-/optimizer-swc-2.13.3-dev.3418.tgz#cef3f416ba4e18edf66d18f5c1cd2b374cd6b502" + integrity sha512-mMUZCrY1g1xCgA5VdnucDhUefhQY0pS3rJht3pxs3ifj7AJnmTJzZeY2wj8KdmjrOrRLTqSiQa+ESyNg6q8aqg== + dependencies: + "@parcel/diagnostic" "2.0.0-dev.1795+9f297b15c" + "@parcel/plugin" "2.0.0-dev.1795+9f297b15c" "@parcel/source-map" "^2.1.1" - "@parcel/utils" "2.0.0-dev.1789+0b82b13d6" + "@parcel/utils" "2.0.0-dev.1795+9f297b15c" "@swc/core" "^1.7.26" nullthrows "^1.1.1" -"@parcel/package-manager@2.0.0-dev.1789+0b82b13d6": - version "2.0.0-dev.1789" - resolved "https://registry.yarnpkg.com/@parcel/package-manager/-/package-manager-2.0.0-dev.1789.tgz#1b1282ffb5e2a51998c095fdb0ef6b400cef14c2" - integrity sha512-6lZLvyS6W1i806rWXCj4GywmFKc2zt1s2Tk0sSI+tmyj1wZvYq3iBwhBYsZ9xFikec5eDS2oXfIITD0bnbDYxA== - dependencies: - "@parcel/diagnostic" "2.0.0-dev.1789+0b82b13d6" - "@parcel/fs" "2.0.0-dev.1789+0b82b13d6" - "@parcel/logger" "2.0.0-dev.1789+0b82b13d6" - "@parcel/node-resolver-core" "3.4.3-dev.3412+0b82b13d6" - "@parcel/types" "2.0.0-dev.1789+0b82b13d6" - "@parcel/utils" "2.0.0-dev.1789+0b82b13d6" - "@parcel/workers" "2.0.0-dev.1789+0b82b13d6" +"@parcel/package-manager@2.0.0-dev.1795+9f297b15c": + version "2.0.0-dev.1795" + resolved "https://registry.yarnpkg.com/@parcel/package-manager/-/package-manager-2.0.0-dev.1795.tgz#b76fc0398561e66f369f47bc960ca706b640e5f2" + integrity sha512-SR0LhO3CXNjKb1ufC+zwl1HvpIfWTRLey1MZ0sx8e+wmpJAzLFoFvjJb9ve3/gPEM3Wu12J30JizZ9ZPM5b7Yg== + dependencies: + "@parcel/diagnostic" "2.0.0-dev.1795+9f297b15c" + "@parcel/fs" "2.0.0-dev.1795+9f297b15c" + "@parcel/logger" "2.0.0-dev.1795+9f297b15c" + "@parcel/node-resolver-core" "3.4.3-dev.3418+9f297b15c" + "@parcel/types" "2.0.0-dev.1795+9f297b15c" + "@parcel/utils" "2.0.0-dev.1795+9f297b15c" + "@parcel/workers" "2.0.0-dev.1795+9f297b15c" "@swc/core" "^1.7.26" semver "^7.5.2" -"@parcel/packager-css@2.0.0-dev.1789+0b82b13d6": - version "2.0.0-dev.1789" - resolved "https://registry.yarnpkg.com/@parcel/packager-css/-/packager-css-2.0.0-dev.1789.tgz#8d076a20f628b021bbc25b8ede1e2f28cd518c2c" - integrity sha512-Oqq/9o+q/COi1k8pD5529XZJUR/n3QNLi0Rlvfclb1PFBurk21WL3huFlii1MXqdtxN0KqP8m9GMpbic+km7BQ== +"@parcel/packager-css@2.0.0-dev.1795+9f297b15c": + version "2.0.0-dev.1795" + resolved "https://registry.yarnpkg.com/@parcel/packager-css/-/packager-css-2.0.0-dev.1795.tgz#80a17e214d0e4b51c48daa45546e64c2c3a6ac3b" + integrity sha512-xDdNaDvriZ91s6EPTRbT9qUOYimXNHhlQ/rHuIDyiWhJSL3nHdGvljvJa2olZqDgafeU34ewqo7sVGqG9Ts5lg== dependencies: - "@parcel/diagnostic" "2.0.0-dev.1789+0b82b13d6" - "@parcel/plugin" "2.0.0-dev.1789+0b82b13d6" + "@parcel/diagnostic" "2.0.0-dev.1795+9f297b15c" + "@parcel/plugin" "2.0.0-dev.1795+9f297b15c" "@parcel/source-map" "^2.1.1" - "@parcel/utils" "2.0.0-dev.1789+0b82b13d6" + "@parcel/utils" "2.0.0-dev.1795+9f297b15c" lightningcss "^1.22.1" nullthrows "^1.1.1" -"@parcel/packager-html@2.0.0-dev.1789+0b82b13d6": - version "2.0.0-dev.1789" - resolved "https://registry.yarnpkg.com/@parcel/packager-html/-/packager-html-2.0.0-dev.1789.tgz#cb9c0e83c2a63283eaa25848e89ab608f6c63f82" - integrity sha512-jz14m6z5PoE5W58hzbWhle4A/TH5y4X9JTIume8ts13P+xbcSLpCxzqVT9OglleHIxlZbseiKG1PRBaa/ZDh8Q== +"@parcel/packager-html@2.0.0-dev.1795+9f297b15c": + version "2.0.0-dev.1795" + resolved "https://registry.yarnpkg.com/@parcel/packager-html/-/packager-html-2.0.0-dev.1795.tgz#3bc7a3c7ce970b0f5ad0f23a0367b0f16576f5ac" + integrity sha512-4Q4UJt3VP47GzcXg3bZwjOGNehkfRJw9YhfnOUzWQMr/+LTk0/syY7DAP3e7gUchchBLpaCt77tY/j6bLIFgtA== dependencies: - "@parcel/plugin" "2.0.0-dev.1789+0b82b13d6" - "@parcel/types" "2.0.0-dev.1789+0b82b13d6" - "@parcel/utils" "2.0.0-dev.1789+0b82b13d6" + "@parcel/plugin" "2.0.0-dev.1795+9f297b15c" + "@parcel/types" "2.0.0-dev.1795+9f297b15c" + "@parcel/utils" "2.0.0-dev.1795+9f297b15c" nullthrows "^1.1.1" posthtml "^0.16.5" -"@parcel/packager-js@2.0.0-dev.1789+0b82b13d6": - version "2.0.0-dev.1789" - resolved "https://registry.yarnpkg.com/@parcel/packager-js/-/packager-js-2.0.0-dev.1789.tgz#7b8f38f05c4fc83b31770aec0fb2b3b0624f515b" - integrity sha512-MSbdZsbD56258/okHC+D8PsSNyz5ziD6I6dEx9bOTq7x8RjFowa9yQS1fcN0ua+kSqqDkKk3B3lX6+jxvFeX8w== +"@parcel/packager-js@2.0.0-dev.1795+9f297b15c": + version "2.0.0-dev.1795" + resolved "https://registry.yarnpkg.com/@parcel/packager-js/-/packager-js-2.0.0-dev.1795.tgz#3cbd0720ee774188a8e2ef3d5547efd36d0bda9a" + integrity sha512-XEyYQiop3Y5/R2vFFcxpsVXbnmMGb4YYGKJC1XJOgI2MrNZL053s5Tk9AIMCYE94MfX/TARbiEonrO2jbISx+A== dependencies: - "@parcel/diagnostic" "2.0.0-dev.1789+0b82b13d6" - "@parcel/plugin" "2.0.0-dev.1789+0b82b13d6" - "@parcel/rust" "2.13.3-dev.3412+0b82b13d6" + "@parcel/diagnostic" "2.0.0-dev.1795+9f297b15c" + "@parcel/plugin" "2.0.0-dev.1795+9f297b15c" + "@parcel/rust" "2.13.3-dev.3418+9f297b15c" "@parcel/source-map" "^2.1.1" - "@parcel/types" "2.0.0-dev.1789+0b82b13d6" - "@parcel/utils" "2.0.0-dev.1789+0b82b13d6" + "@parcel/types" "2.0.0-dev.1795+9f297b15c" + "@parcel/utils" "2.0.0-dev.1795+9f297b15c" globals "^13.2.0" nullthrows "^1.1.1" -"@parcel/packager-raw@2.0.0-dev.1789+0b82b13d6": - version "2.0.0-dev.1789" - resolved "https://registry.yarnpkg.com/@parcel/packager-raw/-/packager-raw-2.0.0-dev.1789.tgz#017048d8cee41f6971c57f3ea9021f5cb76fc54b" - integrity sha512-dwWwzxp8rL9x5JLoF8gOukKp3cWXn4UpfwPEo1lgCElYRO+QEdui4mWPT6Ci8WAK4Nr/kDY6PqX4Vyf/erJpvg== +"@parcel/packager-raw@2.0.0-dev.1795+9f297b15c": + version "2.0.0-dev.1795" + resolved "https://registry.yarnpkg.com/@parcel/packager-raw/-/packager-raw-2.0.0-dev.1795.tgz#549c8a54f8fd5b6a693149508a118023afe1f66d" + integrity sha512-T9Dp9Qt5JREDD1N21QRfEEKMCuxZ0pa4G34J14sI7QTh2hDPDqeeFWYMkE0Pk+Yn3w2pWfS+Qod1DuRvm6WFZg== dependencies: - "@parcel/plugin" "2.0.0-dev.1789+0b82b13d6" + "@parcel/plugin" "2.0.0-dev.1795+9f297b15c" -"@parcel/packager-svg@2.13.3-dev.3412+0b82b13d6": - version "2.13.3-dev.3412" - resolved "https://registry.yarnpkg.com/@parcel/packager-svg/-/packager-svg-2.13.3-dev.3412.tgz#f6a8928c06347c1286bbfd9fb92f019cf9a9fd63" - integrity sha512-kUNENa6nhT84a/8L5u+S8MiN6EUVKiRdlD8aYUF0xxbiTYvt/uDboUxvHHPZuoNp9I1TmQqX7sQ1yivEVIccyA== +"@parcel/packager-svg@2.13.3-dev.3418+9f297b15c": + version "2.13.3-dev.3418" + resolved "https://registry.yarnpkg.com/@parcel/packager-svg/-/packager-svg-2.13.3-dev.3418.tgz#4c6a6b06d87955c444f2e1d861b80720ba02e816" + integrity sha512-RxeL6JRXld+SPTqds0mLswsgKCvkarM8pyBBXJLvP5a8G2jmc7OkgkaYonORz8EXGJCH21JAP6mxbwq4EjsuQQ== dependencies: - "@parcel/plugin" "2.0.0-dev.1789+0b82b13d6" - "@parcel/types" "2.0.0-dev.1789+0b82b13d6" - "@parcel/utils" "2.0.0-dev.1789+0b82b13d6" + "@parcel/plugin" "2.0.0-dev.1795+9f297b15c" + "@parcel/types" "2.0.0-dev.1795+9f297b15c" + "@parcel/utils" "2.0.0-dev.1795+9f297b15c" posthtml "^0.16.4" -"@parcel/packager-wasm@2.13.3-dev.3412+0b82b13d6": - version "2.13.3-dev.3412" - resolved "https://registry.yarnpkg.com/@parcel/packager-wasm/-/packager-wasm-2.13.3-dev.3412.tgz#c7eb96fcb65bbf1e550cadd8d05f521fe4651644" - integrity sha512-b3mYX+TOYySN0KGLOe8Vz+eas2vNGnF2qLvrd3h8FvrgxUDiOaWRFQZ1Ep13/+8VyIbSrOuTVznau7jz4Ee2Cg== +"@parcel/packager-wasm@2.13.3-dev.3418+9f297b15c": + version "2.13.3-dev.3418" + resolved "https://registry.yarnpkg.com/@parcel/packager-wasm/-/packager-wasm-2.13.3-dev.3418.tgz#ecd6dc970e10b805be6768ecee28a795f79130ec" + integrity sha512-XNIiDGbf8ZMyqxJYYQq2TrshMbBTDU5DdC9V7ByS4EuPpr9DjNhZWa7NXzU5rfze2b/sNyL5Y9w52wIU2JNMEA== dependencies: - "@parcel/plugin" "2.0.0-dev.1789+0b82b13d6" + "@parcel/plugin" "2.0.0-dev.1795+9f297b15c" -"@parcel/plugin@2.0.0-dev.1789+0b82b13d6": - version "2.0.0-dev.1789" - resolved "https://registry.yarnpkg.com/@parcel/plugin/-/plugin-2.0.0-dev.1789.tgz#b4678e9197352e1e6b734913580ee826f10d2f5a" - integrity sha512-zxWiQ/aXiD6qYPPs8Q0GQd+sYWJcDuOQPc6wuDFxehiBw1+cEL5EeJyKCdeh7HoYuCZHDEnKjK+f3k511zTzTw== +"@parcel/plugin@2.0.0-dev.1795+9f297b15c": + version "2.0.0-dev.1795" + resolved "https://registry.yarnpkg.com/@parcel/plugin/-/plugin-2.0.0-dev.1795.tgz#6ad492e83bc6cef4c3c360fc0dac3f6bc61d3631" + integrity sha512-2lj8IU3DHZvoBbKbuRwDr8s+ktPd8EOMIZZZ8bWuQ28mGCE5HaogbU7mHuzJqbSzcd9GnxPqSoKSA7msv1jDew== dependencies: - "@parcel/types" "2.0.0-dev.1789+0b82b13d6" + "@parcel/types" "2.0.0-dev.1795+9f297b15c" -"@parcel/profiler@2.13.3-dev.3412+0b82b13d6": - version "2.13.3-dev.3412" - resolved "https://registry.yarnpkg.com/@parcel/profiler/-/profiler-2.13.3-dev.3412.tgz#a271b4402150ae19534ea19f9c25d7f61663eaf5" - integrity sha512-a6btgthz0hRDy6ASQgGb/HQSxvrV0B5VfVYkL+QjTSdcybXklMTrP0qHHBoUkOWCpJ/xePvu3JzKhOlc76W2hw== +"@parcel/profiler@2.13.3-dev.3418+9f297b15c": + version "2.13.3-dev.3418" + resolved "https://registry.yarnpkg.com/@parcel/profiler/-/profiler-2.13.3-dev.3418.tgz#551a2b7d8181c971ea4912919094bfa5d3fc65e6" + integrity sha512-AisnQYasAaJNqepRnihs50SEtO+hBUKoX1BzJ/ha63MAnB5eGRXilHjjjVgGPcvthn89II+NtejNp0eiiXeYBQ== dependencies: - "@parcel/diagnostic" "2.0.0-dev.1789+0b82b13d6" - "@parcel/events" "2.0.0-dev.1789+0b82b13d6" - "@parcel/types-internal" "2.13.3-dev.3412+0b82b13d6" + "@parcel/diagnostic" "2.0.0-dev.1795+9f297b15c" + "@parcel/events" "2.0.0-dev.1795+9f297b15c" + "@parcel/types-internal" "2.13.3-dev.3418+9f297b15c" chrome-trace-event "^1.0.2" -"@parcel/reporter-cli@2.0.0-dev.1789+0b82b13d6": - version "2.0.0-dev.1789" - resolved "https://registry.yarnpkg.com/@parcel/reporter-cli/-/reporter-cli-2.0.0-dev.1789.tgz#d5822f505bbdb1fe9d460015acc4c44b0666c37e" - integrity sha512-M6J3P/eVfQXCt4+VcQVS05AB5IyH7SmI9iPTKH9E30gORdndciydgQLsdoU/nx1N2c33x41P/8hHfpqUKzGvHg== +"@parcel/reporter-cli@2.0.0-dev.1795+9f297b15c": + version "2.0.0-dev.1795" + resolved "https://registry.yarnpkg.com/@parcel/reporter-cli/-/reporter-cli-2.0.0-dev.1795.tgz#f449ac4fb987dd36367cd6a137e2d61ebf23d39a" + integrity sha512-KdF4fTodClz/SBCmTgn1s3hF8FRDFLMiOArNF4zzpwYR7LqVb21fD6uCzdKluqR/dC8S6bQvyjKdYDxKbEuoGQ== dependencies: - "@parcel/plugin" "2.0.0-dev.1789+0b82b13d6" - "@parcel/types" "2.0.0-dev.1789+0b82b13d6" - "@parcel/utils" "2.0.0-dev.1789+0b82b13d6" + "@parcel/plugin" "2.0.0-dev.1795+9f297b15c" + "@parcel/types" "2.0.0-dev.1795+9f297b15c" + "@parcel/utils" "2.0.0-dev.1795+9f297b15c" chalk "^4.1.2" term-size "^2.2.1" -"@parcel/reporter-dev-server@2.0.0-dev.1789+0b82b13d6": - version "2.0.0-dev.1789" - resolved "https://registry.yarnpkg.com/@parcel/reporter-dev-server/-/reporter-dev-server-2.0.0-dev.1789.tgz#86377aeffc57539732624ca56ea74ec7042ce783" - integrity sha512-8Xmh8YEd9QN7TgwicOSsL2Vdtk5JWoXu4lIvLLn96ux8pHs8ReVE/r2o29xsIwy43uhRQbwYSX8tG1FDxUnuiw== +"@parcel/reporter-dev-server@2.0.0-dev.1795+9f297b15c": + version "2.0.0-dev.1795" + resolved "https://registry.yarnpkg.com/@parcel/reporter-dev-server/-/reporter-dev-server-2.0.0-dev.1795.tgz#b5fc225cdb1f186003bbf862c61842358c002752" + integrity sha512-cot7qGAkujf/XMIH/jk2NY+agxgXO+PG8YrbCn3Tul/Gm3bYy5pc2XCuMYqyRHAIWe3wKLuHba2RHMR4WgZitg== dependencies: - "@parcel/plugin" "2.0.0-dev.1789+0b82b13d6" - "@parcel/utils" "2.0.0-dev.1789+0b82b13d6" + "@parcel/plugin" "2.0.0-dev.1795+9f297b15c" + "@parcel/utils" "2.0.0-dev.1795+9f297b15c" -"@parcel/reporter-tracer@2.13.3-dev.3412+0b82b13d6": - version "2.13.3-dev.3412" - resolved "https://registry.yarnpkg.com/@parcel/reporter-tracer/-/reporter-tracer-2.13.3-dev.3412.tgz#f953447f6132a69895fbc27f65fb3d2ca541071e" - integrity sha512-oKoact1zmzW6bcbAg0X3JVZ2zZjovFqpTc/6P2vdF2FiBAwaafq4RdTRPdAh8z1YnqJU3Kh1ONaY+wfTzxCtYQ== +"@parcel/reporter-tracer@2.13.3-dev.3418+9f297b15c": + version "2.13.3-dev.3418" + resolved "https://registry.yarnpkg.com/@parcel/reporter-tracer/-/reporter-tracer-2.13.3-dev.3418.tgz#7996aae33b5e3776bd2c1f61bebac26263bc3536" + integrity sha512-SpTrCT51wjYMPqlRPqKAx6rXpUGx3rkfYov97ID1K17z1KoWqIkZPvjDrTRD01aEz9lLHF4Ywc5beaguVwecJA== dependencies: - "@parcel/plugin" "2.0.0-dev.1789+0b82b13d6" - "@parcel/utils" "2.0.0-dev.1789+0b82b13d6" + "@parcel/plugin" "2.0.0-dev.1795+9f297b15c" + "@parcel/utils" "2.0.0-dev.1795+9f297b15c" chrome-trace-event "^1.0.3" nullthrows "^1.1.1" -"@parcel/resolver-default@2.0.0-dev.1789+0b82b13d6": - version "2.0.0-dev.1789" - resolved "https://registry.yarnpkg.com/@parcel/resolver-default/-/resolver-default-2.0.0-dev.1789.tgz#54bfa782a4dc687ce3c9ca9c2d80f37b176a28df" - integrity sha512-h6YC6BZJcNFW1Wz7x3PXFyFTjyrPfjybtmObEUWgFQVkqwGfGRiNfpQqdNDNeABeE0Fn46dX/FymQ7u4LzQPVg== +"@parcel/resolver-default@2.0.0-dev.1795+9f297b15c": + version "2.0.0-dev.1795" + resolved "https://registry.yarnpkg.com/@parcel/resolver-default/-/resolver-default-2.0.0-dev.1795.tgz#684e45e1d92847882332a3b61fde91bd58b831d4" + integrity sha512-0oXyPHEWf6fLrLe1FDN7fwhnKB/QmMw4di1wosxRHtyfTgIa/hs+r8hK0n5Jqy3JzmMlwgBr+p4gwnxOLPK/KQ== dependencies: - "@parcel/node-resolver-core" "3.4.3-dev.3412+0b82b13d6" - "@parcel/plugin" "2.0.0-dev.1789+0b82b13d6" + "@parcel/node-resolver-core" "3.4.3-dev.3418+9f297b15c" + "@parcel/plugin" "2.0.0-dev.1795+9f297b15c" -"@parcel/runtime-browser-hmr@2.0.0-dev.1789+0b82b13d6": - version "2.0.0-dev.1789" - resolved "https://registry.yarnpkg.com/@parcel/runtime-browser-hmr/-/runtime-browser-hmr-2.0.0-dev.1789.tgz#c5ba22bb7afeb3ad8acc48d47176fd57f1fb33e6" - integrity sha512-0Kqd2Wlc+piflkz46YIEz0+FglS6BBzyREW0e8rYbD2BHlnunbifl7E4e899UdfWSjRUFa1brHlUBFjidGwQgQ== +"@parcel/runtime-browser-hmr@2.0.0-dev.1795+9f297b15c": + version "2.0.0-dev.1795" + resolved "https://registry.yarnpkg.com/@parcel/runtime-browser-hmr/-/runtime-browser-hmr-2.0.0-dev.1795.tgz#c2c72a056b53c43e4e642197382e885f0f17425f" + integrity sha512-L0VpGl6D92WOsoF98eesLg/09VnGUr/14TlDzh0G7tRG+LHXcg0o/6vsGzVLKjR6yXQHuc/6SepbY2U5NsQNNg== dependencies: - "@parcel/plugin" "2.0.0-dev.1789+0b82b13d6" - "@parcel/utils" "2.0.0-dev.1789+0b82b13d6" + "@parcel/plugin" "2.0.0-dev.1795+9f297b15c" + "@parcel/utils" "2.0.0-dev.1795+9f297b15c" -"@parcel/runtime-js@2.0.0-dev.1789+0b82b13d6": - version "2.0.0-dev.1789" - resolved "https://registry.yarnpkg.com/@parcel/runtime-js/-/runtime-js-2.0.0-dev.1789.tgz#324661b5976a8ef982fd53dfde20ebc1b84c03f8" - integrity sha512-2mxG1sZgTDoMddBcrRa/vaqUaVtMUw5Sn+DonubGn68SJdQ9JWzJt5dmpy2mbleCBlFjRI5X8jjNfvbFciLkAw== +"@parcel/runtime-js@2.0.0-dev.1795+9f297b15c": + version "2.0.0-dev.1795" + resolved "https://registry.yarnpkg.com/@parcel/runtime-js/-/runtime-js-2.0.0-dev.1795.tgz#e0843dbeb6d2790bf4221c9128125eb208f46c06" + integrity sha512-np7fz79T7zxIuzZOZIgUpeLy297r0pkHQn0TfGvXs16m7Yz3KGOXsYe8gJGLfhsJk2JLOZ0PQsLvgXRfyKDoxA== dependencies: - "@parcel/diagnostic" "2.0.0-dev.1789+0b82b13d6" - "@parcel/plugin" "2.0.0-dev.1789+0b82b13d6" - "@parcel/utils" "2.0.0-dev.1789+0b82b13d6" + "@parcel/diagnostic" "2.0.0-dev.1795+9f297b15c" + "@parcel/plugin" "2.0.0-dev.1795+9f297b15c" + "@parcel/utils" "2.0.0-dev.1795+9f297b15c" nullthrows "^1.1.1" -"@parcel/runtime-react-refresh@2.0.0-dev.1789+0b82b13d6": - version "2.0.0-dev.1789" - resolved "https://registry.yarnpkg.com/@parcel/runtime-react-refresh/-/runtime-react-refresh-2.0.0-dev.1789.tgz#08d92779681acd628a7e86284c3c13338723e0b4" - integrity sha512-3TCh5PHa7e+NQ4ZHCDPTZNrOiV1BDJdl7zFkjMyl8BWOa80cJbt3F0PrF5xx+VVQ3LtMoQH3RtjsvyfoHlhn9Q== +"@parcel/runtime-react-refresh@2.0.0-dev.1795+9f297b15c": + version "2.0.0-dev.1795" + resolved "https://registry.yarnpkg.com/@parcel/runtime-react-refresh/-/runtime-react-refresh-2.0.0-dev.1795.tgz#06469b97471044fae8528dcad4e39c51b4945e8f" + integrity sha512-UQHDuBOTC0lsoFmyTD7zELmErVvt9wge5yeqLrB59yYvK570MrU/ujkyCECCOIf5IojbNn7GI4rmntO3SEp4Kg== dependencies: - "@parcel/plugin" "2.0.0-dev.1789+0b82b13d6" - "@parcel/utils" "2.0.0-dev.1789+0b82b13d6" + "@parcel/plugin" "2.0.0-dev.1795+9f297b15c" + "@parcel/utils" "2.0.0-dev.1795+9f297b15c" react-error-overlay "6.0.9" react-refresh ">=0.9 <=0.14" -"@parcel/runtime-rsc@2.13.3-dev.3412": - version "2.13.3-dev.3412" - resolved "https://registry.yarnpkg.com/@parcel/runtime-rsc/-/runtime-rsc-2.13.3-dev.3412.tgz#434cc6afeff78027b6703af5ef27fdbbddd3d6df" - integrity sha512-kz+kJWJkQhEDIcnJhiG5PbG2zX4hRkwwiS7aIbby/9rMHexaENy54KMA6+PbMLXZdSlq42HvUSPyW3nHOz7+ew== +"@parcel/runtime-rsc@2.13.3-dev.3418": + version "2.13.3-dev.3418" + resolved "https://registry.yarnpkg.com/@parcel/runtime-rsc/-/runtime-rsc-2.13.3-dev.3418.tgz#288351ebfb2b89a8b9fb6ebc880480950a97c8da" + integrity sha512-uBs80TXnx3rlauNeolYwfrrvLNjQgkM0ilxXmE2636hEhI6TRUWR86s/DoLKx/WFW+A7oAxSgXa4LUXPhiuQhA== dependencies: - "@parcel/plugin" "2.0.0-dev.1789+0b82b13d6" - "@parcel/utils" "2.0.0-dev.1789+0b82b13d6" + "@parcel/plugin" "2.0.0-dev.1795+9f297b15c" + "@parcel/rust" "2.13.3-dev.3418+9f297b15c" + "@parcel/utils" "2.0.0-dev.1795+9f297b15c" nullthrows "^1.1.1" -"@parcel/runtime-service-worker@2.13.3-dev.3412+0b82b13d6": - version "2.13.3-dev.3412" - resolved "https://registry.yarnpkg.com/@parcel/runtime-service-worker/-/runtime-service-worker-2.13.3-dev.3412.tgz#c59e6ce37e8e961d2f14b8bc03a5818a9f905cf7" - integrity sha512-okUqDtRuFAknQpg7EhmOYvqCn8rTWAW3Ldcv/qD5OzfcQPAI/kmAACDEf1UOWzv7+SCKs1ihRZZ0AfW//j0VJQ== +"@parcel/runtime-service-worker@2.13.3-dev.3418+9f297b15c": + version "2.13.3-dev.3418" + resolved "https://registry.yarnpkg.com/@parcel/runtime-service-worker/-/runtime-service-worker-2.13.3-dev.3418.tgz#5a55a63ecbe338f6950899db69b7319957a14490" + integrity sha512-ptW3S1klF5XBcMVInmuXYvYzJMFoTTB/0U56knKDq2dvFBbsLtHdTR+oMREzv0sqkT/UeT75uC5gLTYnqogvKw== dependencies: - "@parcel/plugin" "2.0.0-dev.1789+0b82b13d6" - "@parcel/utils" "2.0.0-dev.1789+0b82b13d6" + "@parcel/plugin" "2.0.0-dev.1795+9f297b15c" + "@parcel/utils" "2.0.0-dev.1795+9f297b15c" nullthrows "^1.1.1" -"@parcel/rust@2.13.3-dev.3412+0b82b13d6": - version "2.13.3-dev.3412" - resolved "https://registry.yarnpkg.com/@parcel/rust/-/rust-2.13.3-dev.3412.tgz#40016415f749f2d52b46e9bc32522b45e6f176ad" - integrity sha512-Bqf7lcrs+FDZ1bUASwPTBC1SYKLO+8fjcP9VzTHHH0xgcv9wAVslzLwn2S3763rP3sZeQHlXLabO4nDUrqfxQw== +"@parcel/rust@2.13.3-dev.3418+9f297b15c": + version "2.13.3-dev.3418" + resolved "https://registry.yarnpkg.com/@parcel/rust/-/rust-2.13.3-dev.3418.tgz#8fb1db9f7c361f4d08476380c0b823b84f9107c8" + integrity sha512-HcwkHle9XCqg3rXv+sQR5LcoiSJAoGNWrOC5Abz3ANev8frisLnHqL6foFT1nCmEWQ8QUluBYJfNgv1DZ/aVxg== "@parcel/source-map@^2.1.1": version "2.1.1" @@ -528,41 +529,41 @@ dependencies: detect-libc "^1.0.3" -"@parcel/transformer-babel@2.0.0-dev.1789+0b82b13d6": - version "2.0.0-dev.1789" - resolved "https://registry.yarnpkg.com/@parcel/transformer-babel/-/transformer-babel-2.0.0-dev.1789.tgz#559177c6236a0b65ca9a22fbfba108030cef7534" - integrity sha512-PLkE6NQGJfydZ+juCESwTbMrinfKwGYwEeYganBKmWCccnYUgSqKF4QWab2sMMXvXNnyazgE/NzPmgyBQEtW4A== +"@parcel/transformer-babel@2.0.0-dev.1795+9f297b15c": + version "2.0.0-dev.1795" + resolved "https://registry.yarnpkg.com/@parcel/transformer-babel/-/transformer-babel-2.0.0-dev.1795.tgz#f9612f6a224ddd0cb149d45e8ae4f5bf87c73c44" + integrity sha512-VpLZDimLWosfUbuqgcJTj6Y/3QETmZdJtb3JRAP1hc0EDvYsWkvUcCDe7CzYeYFsMSA8lAZxWmU3mG5Fp8h9pA== dependencies: - "@parcel/diagnostic" "2.0.0-dev.1789+0b82b13d6" - "@parcel/plugin" "2.0.0-dev.1789+0b82b13d6" + "@parcel/diagnostic" "2.0.0-dev.1795+9f297b15c" + "@parcel/plugin" "2.0.0-dev.1795+9f297b15c" "@parcel/source-map" "^2.1.1" - "@parcel/utils" "2.0.0-dev.1789+0b82b13d6" + "@parcel/utils" "2.0.0-dev.1795+9f297b15c" browserslist "^4.6.6" json5 "^2.2.0" nullthrows "^1.1.1" semver "^7.5.2" -"@parcel/transformer-css@2.0.0-dev.1789+0b82b13d6": - version "2.0.0-dev.1789" - resolved "https://registry.yarnpkg.com/@parcel/transformer-css/-/transformer-css-2.0.0-dev.1789.tgz#b9144e0bcb7bfd0e64c0768bbb36d2a3c6b985ce" - integrity sha512-TB0KorHhRHJ5C5AWdbXpbwao0g1rzQj0d/ptieoQUip3IBzEUjJn99YqpOCJI6AeOHZYMMcfUkdCEy7Aghk4Aw== +"@parcel/transformer-css@2.0.0-dev.1795+9f297b15c": + version "2.0.0-dev.1795" + resolved "https://registry.yarnpkg.com/@parcel/transformer-css/-/transformer-css-2.0.0-dev.1795.tgz#bc7a59f7de7ec4b3cba2ba174c0f5e5c0cfaf35c" + integrity sha512-nwiES/2DD/pmzZCj1enfmfloC2ehKoGZ6vr5XKEM7y+ld42Hea5UT0sjinJHoV+Xuk6pkcOa1j1Vb8d4JURgTQ== dependencies: - "@parcel/diagnostic" "2.0.0-dev.1789+0b82b13d6" - "@parcel/plugin" "2.0.0-dev.1789+0b82b13d6" + "@parcel/diagnostic" "2.0.0-dev.1795+9f297b15c" + "@parcel/plugin" "2.0.0-dev.1795+9f297b15c" "@parcel/source-map" "^2.1.1" - "@parcel/utils" "2.0.0-dev.1789+0b82b13d6" + "@parcel/utils" "2.0.0-dev.1795+9f297b15c" browserslist "^4.6.6" lightningcss "^1.22.1" nullthrows "^1.1.1" -"@parcel/transformer-html@2.0.0-dev.1789+0b82b13d6": - version "2.0.0-dev.1789" - resolved "https://registry.yarnpkg.com/@parcel/transformer-html/-/transformer-html-2.0.0-dev.1789.tgz#ac382abafdfc1c4bb4de8a7bf7c0423084f67e69" - integrity sha512-HDeTR0ah8mtHalB65BkbmUR1Xgi2BJmZNTRvUn6x3Rd6qS4YUzuAA5en6k64XruZOwP5nvhnkWR1Lwa/h9XfBQ== +"@parcel/transformer-html@2.0.0-dev.1795+9f297b15c": + version "2.0.0-dev.1795" + resolved "https://registry.yarnpkg.com/@parcel/transformer-html/-/transformer-html-2.0.0-dev.1795.tgz#cc5fa7398b51e8ddf08838677255b74cdae1118c" + integrity sha512-hlnc1KfrLNlKndDa5wXOY6EVKJjbeTQX2/d5w7eifSbDBW7yejDq1k2PByVZpitJ2dgO41NXMeDWSpvZt36G1A== dependencies: - "@parcel/diagnostic" "2.0.0-dev.1789+0b82b13d6" - "@parcel/plugin" "2.0.0-dev.1789+0b82b13d6" - "@parcel/rust" "2.13.3-dev.3412+0b82b13d6" + "@parcel/diagnostic" "2.0.0-dev.1795+9f297b15c" + "@parcel/plugin" "2.0.0-dev.1795+9f297b15c" + "@parcel/rust" "2.13.3-dev.3418+9f297b15c" nullthrows "^1.1.1" posthtml "^0.16.5" posthtml-parser "^0.12.1" @@ -570,126 +571,126 @@ semver "^7.5.2" srcset "4" -"@parcel/transformer-image@2.13.3-dev.3412+0b82b13d6": - version "2.13.3-dev.3412" - resolved "https://registry.yarnpkg.com/@parcel/transformer-image/-/transformer-image-2.13.3-dev.3412.tgz#acd31d217748aa5ac95c7aa1562da47336e2dce1" - integrity sha512-A3958b4pQYcSPYwdJn7oHPdGe7LXF7LC9kq/HA5jIcnY5iWqpmeZ0E4QxjuZcOCcMdezUTl9VOKy1Lo++g4YLQ== +"@parcel/transformer-image@2.13.3-dev.3418+9f297b15c": + version "2.13.3-dev.3418" + resolved "https://registry.yarnpkg.com/@parcel/transformer-image/-/transformer-image-2.13.3-dev.3418.tgz#a8153a7d94c9e6ddc49ff0a0bb41b645a4a5459e" + integrity sha512-lXkkCWbhuYd0UWm3zwVAIoC6B8fvyRacDkbhjhDO96gs7kmZPh369P4rlh03HeoOJlgTcpuvIH4LcnoVeccUSw== dependencies: - "@parcel/plugin" "2.0.0-dev.1789+0b82b13d6" - "@parcel/utils" "2.0.0-dev.1789+0b82b13d6" - "@parcel/workers" "2.0.0-dev.1789+0b82b13d6" + "@parcel/plugin" "2.0.0-dev.1795+9f297b15c" + "@parcel/utils" "2.0.0-dev.1795+9f297b15c" + "@parcel/workers" "2.0.0-dev.1795+9f297b15c" nullthrows "^1.1.1" -"@parcel/transformer-js@2.0.0-dev.1789+0b82b13d6": - version "2.0.0-dev.1789" - resolved "https://registry.yarnpkg.com/@parcel/transformer-js/-/transformer-js-2.0.0-dev.1789.tgz#bbd1a08f14d153a422ae2b1d8221e2af44a2a2f4" - integrity sha512-FxdroHko0GONty4gM8lJ8gmftwFw+rRX+4m2i4fMOZ+ekARLAoSiQ7uhob2PcIuNeOPvzeHxjV6eg2F9skUasw== +"@parcel/transformer-js@2.0.0-dev.1795+9f297b15c": + version "2.0.0-dev.1795" + resolved "https://registry.yarnpkg.com/@parcel/transformer-js/-/transformer-js-2.0.0-dev.1795.tgz#741ee51638cbd8787f7a0903aef670382ecad19f" + integrity sha512-7ZxmALPqcI/B1Ywg835qwicZlmE5is2bbPYkjx7Pjc3MB5q+GKTwz/iWEjTS5AbZkKw9Db3QXp/+dubFoLSI4g== dependencies: - "@parcel/diagnostic" "2.0.0-dev.1789+0b82b13d6" - "@parcel/plugin" "2.0.0-dev.1789+0b82b13d6" - "@parcel/rust" "2.13.3-dev.3412+0b82b13d6" + "@parcel/diagnostic" "2.0.0-dev.1795+9f297b15c" + "@parcel/plugin" "2.0.0-dev.1795+9f297b15c" + "@parcel/rust" "2.13.3-dev.3418+9f297b15c" "@parcel/source-map" "^2.1.1" - "@parcel/utils" "2.0.0-dev.1789+0b82b13d6" - "@parcel/workers" "2.0.0-dev.1789+0b82b13d6" + "@parcel/utils" "2.0.0-dev.1795+9f297b15c" + "@parcel/workers" "2.0.0-dev.1795+9f297b15c" "@swc/helpers" "^0.5.0" browserslist "^4.6.6" nullthrows "^1.1.1" regenerator-runtime "^0.14.1" semver "^7.5.2" -"@parcel/transformer-json@2.0.0-dev.1789+0b82b13d6": - version "2.0.0-dev.1789" - resolved "https://registry.yarnpkg.com/@parcel/transformer-json/-/transformer-json-2.0.0-dev.1789.tgz#3f712d8e1343d65e5f92898d4c47723cfc10e877" - integrity sha512-1u7UowT1KhzNviHJnhdHV8aULBM0JgOk9vOIAe+7aVXvl5SGQSO/s87oAxUcIodKePqkRIBU27dqf2i3VHBuoQ== +"@parcel/transformer-json@2.0.0-dev.1795+9f297b15c": + version "2.0.0-dev.1795" + resolved "https://registry.yarnpkg.com/@parcel/transformer-json/-/transformer-json-2.0.0-dev.1795.tgz#a5215f863cab73d58e66d9466e45da145879cddd" + integrity sha512-2oKPaX2oFmynkMN8pA/BnrssnNL5xWuh6nKW5qqX/n9R7GoGIPAJ7iZrsFlU5vPLw2nsz1EDdhv4R5ydZK3a+Q== dependencies: - "@parcel/plugin" "2.0.0-dev.1789+0b82b13d6" + "@parcel/plugin" "2.0.0-dev.1795+9f297b15c" json5 "^2.2.0" -"@parcel/transformer-postcss@2.0.0-dev.1789+0b82b13d6": - version "2.0.0-dev.1789" - resolved "https://registry.yarnpkg.com/@parcel/transformer-postcss/-/transformer-postcss-2.0.0-dev.1789.tgz#19ee839823eda37940b1edef6c81c4074894e695" - integrity sha512-zzMXmUblf/4VrKPukvqM+24vTFx+W/x0POJwUJ9vcer+e2FV9gAPcYvMMdEHbqNoO6dcs7J4o/DnmqgCDF3IrQ== +"@parcel/transformer-postcss@2.0.0-dev.1795+9f297b15c": + version "2.0.0-dev.1795" + resolved "https://registry.yarnpkg.com/@parcel/transformer-postcss/-/transformer-postcss-2.0.0-dev.1795.tgz#0413805bb09bf22227b15bfcbb817142d7a9c6ce" + integrity sha512-b2+r8gMtGczhEuUFJmYi8Joo0TEN+8VD4mMaQxtUbz3cNdJxDVWjD5fLQNJcbWFzEsAv2aU5eXJ1dGU8vofd5Q== dependencies: - "@parcel/diagnostic" "2.0.0-dev.1789+0b82b13d6" - "@parcel/plugin" "2.0.0-dev.1789+0b82b13d6" - "@parcel/rust" "2.13.3-dev.3412+0b82b13d6" - "@parcel/utils" "2.0.0-dev.1789+0b82b13d6" + "@parcel/diagnostic" "2.0.0-dev.1795+9f297b15c" + "@parcel/plugin" "2.0.0-dev.1795+9f297b15c" + "@parcel/rust" "2.13.3-dev.3418+9f297b15c" + "@parcel/utils" "2.0.0-dev.1795+9f297b15c" clone "^2.1.1" nullthrows "^1.1.1" postcss-value-parser "^4.2.0" semver "^7.5.2" -"@parcel/transformer-posthtml@2.0.0-dev.1789+0b82b13d6": - version "2.0.0-dev.1789" - resolved "https://registry.yarnpkg.com/@parcel/transformer-posthtml/-/transformer-posthtml-2.0.0-dev.1789.tgz#ac9f937485a748bf3cba1fb689488093ecb0e7db" - integrity sha512-JNx5LhRh2W0D7pACy1APzr00EH/Lxm54p5BT0B+fAVavuQvyHdYjtLS1L/B3wBKIPQNJNqkgjatebH/OnGrZOQ== +"@parcel/transformer-posthtml@2.0.0-dev.1795+9f297b15c": + version "2.0.0-dev.1795" + resolved "https://registry.yarnpkg.com/@parcel/transformer-posthtml/-/transformer-posthtml-2.0.0-dev.1795.tgz#787682376cb01bd6d8811246619881af2e7cd0a7" + integrity sha512-9lrdDY1pIQtRMkxe8r0EghSd+7hFEyU3xb2nNO68m9Eut9zHjZo3p1As6rKp6eJ5ZqZxOJYj4xhqymwc5Evi4Q== dependencies: - "@parcel/plugin" "2.0.0-dev.1789+0b82b13d6" - "@parcel/utils" "2.0.0-dev.1789+0b82b13d6" + "@parcel/plugin" "2.0.0-dev.1795+9f297b15c" + "@parcel/utils" "2.0.0-dev.1795+9f297b15c" nullthrows "^1.1.1" posthtml "^0.16.5" posthtml-parser "^0.12.1" posthtml-render "^3.0.0" semver "^7.5.2" -"@parcel/transformer-raw@2.0.0-dev.1789+0b82b13d6": - version "2.0.0-dev.1789" - resolved "https://registry.yarnpkg.com/@parcel/transformer-raw/-/transformer-raw-2.0.0-dev.1789.tgz#0e2c5f826dabe7f929e06b54e1e91f1efec4e2c9" - integrity sha512-LKq8UFFmERO4be0rjPkLC4yziXDMRbule7l6mSXMCh3t+TIlGu7K/8SWLidNQk8C8yxePt6sl5MYsend7rbYLA== +"@parcel/transformer-raw@2.0.0-dev.1795+9f297b15c": + version "2.0.0-dev.1795" + resolved "https://registry.yarnpkg.com/@parcel/transformer-raw/-/transformer-raw-2.0.0-dev.1795.tgz#97edd45f72b39286aa1d79601912ea68c127448a" + integrity sha512-pg6/3C3kFLbWghdE7v4OdMRDvNIhSLhYy4AGOI+6KbPtVCI/0We1ZCSf1ciAluYbq7/tvYJhUuwGLukrdxTfDg== dependencies: - "@parcel/plugin" "2.0.0-dev.1789+0b82b13d6" + "@parcel/plugin" "2.0.0-dev.1795+9f297b15c" -"@parcel/transformer-react-refresh-wrap@2.0.0-dev.1789+0b82b13d6": - version "2.0.0-dev.1789" - resolved "https://registry.yarnpkg.com/@parcel/transformer-react-refresh-wrap/-/transformer-react-refresh-wrap-2.0.0-dev.1789.tgz#12371b903f77de2d27aed2ddbf4cbdacbd3713e2" - integrity sha512-nk9d4/XSv0kTrvEN0Rt8KbZrMpHRlPOlMvqlpRx+gGIZabT/XL67wg6CgzoVT/fLOF50B/9hHIs/ecpQQnDHGg== +"@parcel/transformer-react-refresh-wrap@2.0.0-dev.1795+9f297b15c": + version "2.0.0-dev.1795" + resolved "https://registry.yarnpkg.com/@parcel/transformer-react-refresh-wrap/-/transformer-react-refresh-wrap-2.0.0-dev.1795.tgz#96fc5b5ead89eae1d6a23690bc9b63cfd2f7bb11" + integrity sha512-OT41I1Y5anyvy6ocnzsgfvMi71J/DUlgcu0X1d3eRl3ui4ACmXB/NkzZr4+EmCFKgE8cp0UR67y6OpoIYFP79Q== dependencies: - "@parcel/plugin" "2.0.0-dev.1789+0b82b13d6" - "@parcel/utils" "2.0.0-dev.1789+0b82b13d6" + "@parcel/plugin" "2.0.0-dev.1795+9f297b15c" + "@parcel/utils" "2.0.0-dev.1795+9f297b15c" react-refresh ">=0.9 <=0.14" -"@parcel/transformer-svg@2.13.3-dev.3412+0b82b13d6": - version "2.13.3-dev.3412" - resolved "https://registry.yarnpkg.com/@parcel/transformer-svg/-/transformer-svg-2.13.3-dev.3412.tgz#76679120fb7599f27e2dd86c7b75108c97e58f83" - integrity sha512-A9tRc+OjjZ5/UA0Ej6SHj1Rim7ue0379duV3mgk+VduvfEIZ1GQjgKgkrGpMJDZ/pPsoM23GLjLa/ZyYOFbV1A== +"@parcel/transformer-svg@2.13.3-dev.3418+9f297b15c": + version "2.13.3-dev.3418" + resolved "https://registry.yarnpkg.com/@parcel/transformer-svg/-/transformer-svg-2.13.3-dev.3418.tgz#17556f8fbb5a972dc01fd1c43aa26be1ca539460" + integrity sha512-AkrAKTdEU0L2Hoz7CPjTg81ChroffpoX71RZffmBAcFi0vkpBMlFzvITjrbV1dC+OtQ5t/+gnmZnowSpZBgU+A== dependencies: - "@parcel/diagnostic" "2.0.0-dev.1789+0b82b13d6" - "@parcel/plugin" "2.0.0-dev.1789+0b82b13d6" - "@parcel/rust" "2.13.3-dev.3412+0b82b13d6" + "@parcel/diagnostic" "2.0.0-dev.1795+9f297b15c" + "@parcel/plugin" "2.0.0-dev.1795+9f297b15c" + "@parcel/rust" "2.13.3-dev.3418+9f297b15c" nullthrows "^1.1.1" posthtml "^0.16.5" posthtml-parser "^0.12.1" posthtml-render "^3.0.0" semver "^7.5.2" -"@parcel/types-internal@2.13.3-dev.3412+0b82b13d6": - version "2.13.3-dev.3412" - resolved "https://registry.yarnpkg.com/@parcel/types-internal/-/types-internal-2.13.3-dev.3412.tgz#47bbe12a0a9b51d6c11fed394e98af61c05d95af" - integrity sha512-EKVDGRxLamtSeH7xAZUciz8eH5zQCExnhqlQNtsRB+9jFoz5hGlsGOX6U7rimSUbe6Hw1DtX9jo5SqtyVRZ+ug== +"@parcel/types-internal@2.13.3-dev.3418+9f297b15c": + version "2.13.3-dev.3418" + resolved "https://registry.yarnpkg.com/@parcel/types-internal/-/types-internal-2.13.3-dev.3418.tgz#76644a7b21bd78e654134be6679afd6a727bb4db" + integrity sha512-B0L3jAMe6tAxB22p2UZwJQGl1FRte9ikLJw4fcPJbszcv5MRLnszISNcSgJQN99hM5ISu+bRMioMksZ2FrDIYA== dependencies: - "@parcel/diagnostic" "2.0.0-dev.1789+0b82b13d6" - "@parcel/feature-flags" "2.13.3-dev.3412+0b82b13d6" + "@parcel/diagnostic" "2.0.0-dev.1795+9f297b15c" + "@parcel/feature-flags" "2.13.3-dev.3418+9f297b15c" "@parcel/source-map" "^2.1.1" utility-types "^3.10.0" -"@parcel/types@2.0.0-dev.1789+0b82b13d6": - version "2.0.0-dev.1789" - resolved "https://registry.yarnpkg.com/@parcel/types/-/types-2.0.0-dev.1789.tgz#58f406fb572bd3687025bb34fcbdb77f6cf1b47a" - integrity sha512-Jh6mHW1HPfwg/CRcNfQoZM9hr0nXvQhSXG+/AUxczcxYzc/KflReYHzyk4du4Z3dS/m9QT+3arVvbVhnZLsOeA== +"@parcel/types@2.0.0-dev.1795+9f297b15c": + version "2.0.0-dev.1795" + resolved "https://registry.yarnpkg.com/@parcel/types/-/types-2.0.0-dev.1795.tgz#1f1503870d97245d1dc82263306cbd25d4313a5b" + integrity sha512-VojyhwPI2L54XMJ45sHFnMm8T75zhQKjhB6tlARWA02sDveCfvfBONfJY/r+O1poeel42Gci+bLanri0s3GgTg== dependencies: - "@parcel/types-internal" "2.13.3-dev.3412+0b82b13d6" - "@parcel/workers" "2.0.0-dev.1789+0b82b13d6" + "@parcel/types-internal" "2.13.3-dev.3418+9f297b15c" + "@parcel/workers" "2.0.0-dev.1795+9f297b15c" -"@parcel/utils@2.0.0-dev.1789+0b82b13d6": - version "2.0.0-dev.1789" - resolved "https://registry.yarnpkg.com/@parcel/utils/-/utils-2.0.0-dev.1789.tgz#5f9f208359fbf85c84a07102be6a34dc28bbf7af" - integrity sha512-SeTQr4rWpvq/yjp5kZrACvewMgz/Q5QuekSlEViICPfsI1L9BNfnNdacAbqOTpsZGW/p/RVobEamQqjIjzhBTQ== +"@parcel/utils@2.0.0-dev.1795+9f297b15c": + version "2.0.0-dev.1795" + resolved "https://registry.yarnpkg.com/@parcel/utils/-/utils-2.0.0-dev.1795.tgz#12aca94ddd344170b5b1081c012bb524f37ebfda" + integrity sha512-JjleEfv9NJD02AOPEOjwMVr5h0SZdtTocQuQBS4S+SC6Ql9ZdiUArE5jXVbUCYmyQnfjG7FYfajzrFnfnA9Uqg== dependencies: - "@parcel/codeframe" "2.0.0-dev.1789+0b82b13d6" - "@parcel/diagnostic" "2.0.0-dev.1789+0b82b13d6" - "@parcel/logger" "2.0.0-dev.1789+0b82b13d6" - "@parcel/markdown-ansi" "2.0.0-dev.1789+0b82b13d6" - "@parcel/rust" "2.13.3-dev.3412+0b82b13d6" + "@parcel/codeframe" "2.0.0-dev.1795+9f297b15c" + "@parcel/diagnostic" "2.0.0-dev.1795+9f297b15c" + "@parcel/logger" "2.0.0-dev.1795+9f297b15c" + "@parcel/markdown-ansi" "2.0.0-dev.1795+9f297b15c" + "@parcel/rust" "2.13.3-dev.3418+9f297b15c" "@parcel/source-map" "^2.1.1" chalk "^4.1.2" nullthrows "^1.1.1" @@ -783,16 +784,16 @@ "@parcel/watcher-win32-ia32" "2.5.0" "@parcel/watcher-win32-x64" "2.5.0" -"@parcel/workers@2.0.0-dev.1789+0b82b13d6": - version "2.0.0-dev.1789" - resolved "https://registry.yarnpkg.com/@parcel/workers/-/workers-2.0.0-dev.1789.tgz#d9cbfac188ff6d87858735ff23d304be03060bbe" - integrity sha512-qtjpw3tfoK8Mr3jbF4YISwYqwRIpAeKhpnSX/r73Jz3iVbCvi+WuYv26A1CexSkc4jGWGnXJRAEQSI/4h1vYuQ== +"@parcel/workers@2.0.0-dev.1795+9f297b15c": + version "2.0.0-dev.1795" + resolved "https://registry.yarnpkg.com/@parcel/workers/-/workers-2.0.0-dev.1795.tgz#5d7840b90cc80a1f01619ed676a744f6e3524af9" + integrity sha512-MiW001+79Qox78Xy7bNBGKWB2PdurL5qdrKxLdKnIU41mZ5ki5tg4cxsgf9+H5KTp4Y8MnLqOh0yjD/8ujdB8g== dependencies: - "@parcel/diagnostic" "2.0.0-dev.1789+0b82b13d6" - "@parcel/logger" "2.0.0-dev.1789+0b82b13d6" - "@parcel/profiler" "2.13.3-dev.3412+0b82b13d6" - "@parcel/types-internal" "2.13.3-dev.3412+0b82b13d6" - "@parcel/utils" "2.0.0-dev.1789+0b82b13d6" + "@parcel/diagnostic" "2.0.0-dev.1795+9f297b15c" + "@parcel/logger" "2.0.0-dev.1795+9f297b15c" + "@parcel/profiler" "2.13.3-dev.3418+9f297b15c" + "@parcel/types-internal" "2.13.3-dev.3418+9f297b15c" + "@parcel/utils" "2.0.0-dev.1795+9f297b15c" nullthrows "^1.1.1" "@swc/core-darwin-arm64@1.10.1": @@ -1872,23 +1873,23 @@ ordered-binary@^1.4.1: resolved "https://registry.yarnpkg.com/ordered-binary/-/ordered-binary-1.5.3.tgz#8bee2aa7a82c3439caeb1e80c272fd4cf51170fb" integrity sha512-oGFr3T+pYdTGJ+YFEILMpS3es+GiIbs9h/XQrclBXUtd44ey7XwfsMzM31f64I1SQOawDoDr/D823kNCADI8TA== -parcel@2.0.0-dev.1787: - version "2.0.0-dev.1787" - resolved "https://registry.yarnpkg.com/parcel/-/parcel-2.0.0-dev.1787.tgz#036c9155b5daf942e5cabaa6738bc8ea96d2f8fc" - integrity sha512-u6uPbkrrhHaTBm7d5RLP9VMgjJkFs23QAMLkqAFcM+M2WQRfb0S6sW0jksRgheEOvpkOlxLgJxXQ6Ein2JZtLw== - dependencies: - "@parcel/config-default" "2.0.0-dev.1789+0b82b13d6" - "@parcel/core" "2.0.0-dev.1787+0b82b13d6" - "@parcel/diagnostic" "2.0.0-dev.1789+0b82b13d6" - "@parcel/events" "2.0.0-dev.1789+0b82b13d6" - "@parcel/feature-flags" "2.13.3-dev.3412+0b82b13d6" - "@parcel/fs" "2.0.0-dev.1789+0b82b13d6" - "@parcel/logger" "2.0.0-dev.1789+0b82b13d6" - "@parcel/package-manager" "2.0.0-dev.1789+0b82b13d6" - "@parcel/reporter-cli" "2.0.0-dev.1789+0b82b13d6" - "@parcel/reporter-dev-server" "2.0.0-dev.1789+0b82b13d6" - "@parcel/reporter-tracer" "2.13.3-dev.3412+0b82b13d6" - "@parcel/utils" "2.0.0-dev.1789+0b82b13d6" +parcel@2.0.0-dev.1793: + version "2.0.0-dev.1793" + resolved "https://registry.yarnpkg.com/parcel/-/parcel-2.0.0-dev.1793.tgz#f9705f9dc730f9f06b8447eeb697930d52819fce" + integrity sha512-+OyZB74SZfkVxZjNb4euYkBPXkx5st6/Ic0mvmGoKcNOLiHyb/1BW3TuoWRcqNsZs9vFMf2i24rpOZKITBeDOA== + dependencies: + "@parcel/config-default" "2.0.0-dev.1795+9f297b15c" + "@parcel/core" "2.0.0-dev.1793+9f297b15c" + "@parcel/diagnostic" "2.0.0-dev.1795+9f297b15c" + "@parcel/events" "2.0.0-dev.1795+9f297b15c" + "@parcel/feature-flags" "2.13.3-dev.3418+9f297b15c" + "@parcel/fs" "2.0.0-dev.1795+9f297b15c" + "@parcel/logger" "2.0.0-dev.1795+9f297b15c" + "@parcel/package-manager" "2.0.0-dev.1795+9f297b15c" + "@parcel/reporter-cli" "2.0.0-dev.1795+9f297b15c" + "@parcel/reporter-dev-server" "2.0.0-dev.1795+9f297b15c" + "@parcel/reporter-tracer" "2.13.3-dev.3418+9f297b15c" + "@parcel/utils" "2.0.0-dev.1795+9f297b15c" chalk "^4.1.2" commander "^12.1.0" get-port "^4.2.0" diff --git a/packages/react-client/src/forks/ReactFlightClientConfig.dom-browser-parcel.js b/packages/react-client/src/forks/ReactFlightClientConfig.dom-browser-parcel.js index 55be32796754d..89b834784f0e8 100644 --- a/packages/react-client/src/forks/ReactFlightClientConfig.dom-browser-parcel.js +++ b/packages/react-client/src/forks/ReactFlightClientConfig.dom-browser-parcel.js @@ -13,5 +13,6 @@ export const rendererPackageName = 'react-server-dom-parcel'; export * from 'react-client/src/ReactFlightClientStreamConfigWeb'; export * from 'react-client/src/ReactClientConsoleConfigBrowser'; export * from 'react-server-dom-parcel/src/client/ReactFlightClientConfigBundlerParcel'; +export * from 'react-server-dom-parcel/src/client/ReactFlightClientConfigTargetParcelBrowser'; export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM'; export const usedWithSSR = false; diff --git a/packages/react-client/src/forks/ReactFlightClientConfig.dom-edge-parcel.js b/packages/react-client/src/forks/ReactFlightClientConfig.dom-edge-parcel.js index 3c86f9ab07f85..626f26903ed73 100644 --- a/packages/react-client/src/forks/ReactFlightClientConfig.dom-edge-parcel.js +++ b/packages/react-client/src/forks/ReactFlightClientConfig.dom-edge-parcel.js @@ -13,5 +13,6 @@ export const rendererPackageName = 'react-server-dom-parcel'; export * from 'react-client/src/ReactFlightClientStreamConfigWeb'; export * from 'react-client/src/ReactClientConsoleConfigServer'; export * from 'react-server-dom-parcel/src/client/ReactFlightClientConfigBundlerParcel'; +export * from 'react-server-dom-parcel/src/client/ReactFlightClientConfigTargetParcelServer'; export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM'; export const usedWithSSR = true; diff --git a/packages/react-client/src/forks/ReactFlightClientConfig.dom-node-parcel.js b/packages/react-client/src/forks/ReactFlightClientConfig.dom-node-parcel.js index 1ed0f6825f6c6..466d21164cc5a 100644 --- a/packages/react-client/src/forks/ReactFlightClientConfig.dom-node-parcel.js +++ b/packages/react-client/src/forks/ReactFlightClientConfig.dom-node-parcel.js @@ -13,5 +13,6 @@ export const rendererPackageName = 'react-server-dom-parcel'; export * from 'react-client/src/ReactFlightClientStreamConfigNode'; export * from 'react-client/src/ReactClientConsoleConfigServer'; export * from 'react-server-dom-parcel/src/client/ReactFlightClientConfigBundlerParcel'; +export * from 'react-server-dom-parcel/src/client/ReactFlightClientConfigTargetParcelServer'; export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM'; export const usedWithSSR = true; diff --git a/packages/react-server-dom-parcel/src/client/ReactFlightClientConfigBundlerParcel.js b/packages/react-server-dom-parcel/src/client/ReactFlightClientConfigBundlerParcel.js index f99eb85acbbc8..128ef9f5e15ed 100644 --- a/packages/react-server-dom-parcel/src/client/ReactFlightClientConfigBundlerParcel.js +++ b/packages/react-server-dom-parcel/src/client/ReactFlightClientConfigBundlerParcel.js @@ -12,6 +12,7 @@ import type {Thenable} from 'shared/ReactTypes'; import type {ImportMetadata} from '../shared/ReactFlightImportMetadata'; import {ID, NAME, BUNDLES} from '../shared/ReactFlightImportMetadata'; +import {prepareDestinationWithChunks} from 'react-client/src/ReactFlightClientConfig'; export type ServerManifest = { [string]: Array, @@ -24,21 +25,14 @@ export type ServerReferenceId = string; export opaque type ClientReferenceMetadata = ImportMetadata; // eslint-disable-next-line no-unused-vars -export opaque type ClientReference = { - // Module id. - id: string, - // Export name. - name: string, - // List of bundle URLs, relative to the distDir. - bundles: Array, -}; +export opaque type ClientReference = ImportMetadata; export function prepareDestinationForModule( moduleLoading: ModuleLoading, nonce: ?string, metadata: ClientReferenceMetadata, ) { - return; + prepareDestinationWithChunks(moduleLoading, metadata[BUNDLES], nonce); } export function resolveClientReference( @@ -46,11 +40,7 @@ export function resolveClientReference( metadata: ClientReferenceMetadata, ): ClientReference { // Reference is already resolved during the build. - return { - id: metadata[ID], - name: metadata[NAME], - bundles: metadata[BUNDLES], - }; + return metadata; } export function resolveServerReference( @@ -64,20 +54,19 @@ export function resolveServerReference( if (!bundles) { throw new Error('Invalid server action: ' + ref); } - return { - id, - name, - bundles, - }; + return [id, name, bundles]; } export function preloadModule( metadata: ClientReference, ): null | Thenable { - return Promise.all(metadata.bundles.map(url => parcelRequire.load(url))); + if (metadata[BUNDLES].length === 0) { + return null; + } + return Promise.all(metadata[BUNDLES].map(url => parcelRequire.load(url))); } export function requireModule(metadata: ClientReference): T { - const moduleExports = parcelRequire(metadata.id); - return moduleExports[metadata.name]; + const moduleExports = parcelRequire(metadata[ID]); + return moduleExports[metadata[NAME]]; } diff --git a/packages/react-server-dom-parcel/src/client/ReactFlightClientConfigTargetParcelBrowser.js b/packages/react-server-dom-parcel/src/client/ReactFlightClientConfigTargetParcelBrowser.js new file mode 100644 index 0000000000000..31346b2255483 --- /dev/null +++ b/packages/react-server-dom-parcel/src/client/ReactFlightClientConfigTargetParcelBrowser.js @@ -0,0 +1,18 @@ +/** + * 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 {ModuleLoading} from './ReactFlightClientConfigBundlerParcel'; + +export function prepareDestinationWithChunks( + moduleLoading: ModuleLoading, + bundles: Array, + nonce: ?string, +) { + // In the browser we don't need to prepare our destination since the browser is the Destination +} diff --git a/packages/react-server-dom-parcel/src/client/ReactFlightClientConfigTargetParcelServer.js b/packages/react-server-dom-parcel/src/client/ReactFlightClientConfigTargetParcelServer.js new file mode 100644 index 0000000000000..c4ebcde9412e2 --- /dev/null +++ b/packages/react-server-dom-parcel/src/client/ReactFlightClientConfigTargetParcelServer.js @@ -0,0 +1,21 @@ +/** + * 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 {ModuleLoading} from './ReactFlightClientConfigBundlerParcel'; +import {preinitModuleForSSR} from 'react-client/src/ReactFlightClientConfig'; + +export function prepareDestinationWithChunks( + moduleLoading: ModuleLoading, + bundles: Array, + nonce: ?string, +) { + for (let i = 0; i < bundles.length; i++) { + preinitModuleForSSR(parcelRequire.meta.publicUrl + bundles[i], nonce); + } +} diff --git a/scripts/flow/environment.js b/scripts/flow/environment.js index 46cb46cadc31a..1d4e6dbd9c7f1 100644 --- a/scripts/flow/environment.js +++ b/scripts/flow/environment.js @@ -106,6 +106,9 @@ declare const __turbopack_require__: ((id: string) => any) & { declare var parcelRequire: { (id: string): any, load: (url: string) => Promise, + meta: { + publicUrl: string, + }, }; declare module 'fs/promises' { From 62208bee5ad7e447d42459ace8c0edcb7c4f9197 Mon Sep 17 00:00:00 2001 From: Ruslan Lesiutin Date: Thu, 2 Jan 2025 14:07:21 +0000 Subject: [PATCH 0020/1160] DevTools: fork FastRefresh test for <18 versions of React (#31893) We currently have a failing test for React DevTools against React 17. This started failing in https://github.com/facebook/react/pull/30899, where we changed logic for error tracking and started relying on `onPostCommitFiberRoot` hook. Looking at https://github.com/facebook/react/pull/21183, `onPostCommitFiberRoot` was shipped in 18, which means that any console errors / warnings emitted in passive effects won't be recorded by React DevTools for React < 18. --- .../FastRefreshDevToolsIntegration-test.js | 77 ++++++++++++++++++- packages/react-devtools-shared/src/hook.js | 1 + 2 files changed, 77 insertions(+), 1 deletion(-) diff --git a/packages/react-devtools-shared/src/__tests__/FastRefreshDevToolsIntegration-test.js b/packages/react-devtools-shared/src/__tests__/FastRefreshDevToolsIntegration-test.js index ff07f0ef16f8a..a80a11fde12d4 100644 --- a/packages/react-devtools-shared/src/__tests__/FastRefreshDevToolsIntegration-test.js +++ b/packages/react-devtools-shared/src/__tests__/FastRefreshDevToolsIntegration-test.js @@ -186,8 +186,83 @@ describe('Fast Refresh', () => { expect(getContainer().firstChild).not.toBe(element); }); + // @reactVersion < 18.0 // @reactVersion >= 16.9 - it('should not break when there are warnings in between patching', () => { + it('should not break when there are warnings in between patching (before post commit hook)', () => { + withErrorsOrWarningsIgnored(['Expected:'], () => { + render(` + const {useState} = React; + + export default function Component() { + const [state, setState] = useState(1); + console.warn("Expected: warning during render"); + return null; + } + `); + }); + expect(store).toMatchInlineSnapshot(` + ✕ 0, ⚠ 1 + [root] + ⚠ + `); + + withErrorsOrWarningsIgnored(['Expected:'], () => { + patch(` + const {useEffect, useState} = React; + + export default function Component() { + const [state, setState] = useState(1); + console.warn("Expected: warning during render"); + return null; + } + `); + }); + expect(store).toMatchInlineSnapshot(` + ✕ 0, ⚠ 2 + [root] + ⚠ + `); + + withErrorsOrWarningsIgnored(['Expected:'], () => { + patch(` + const {useEffect, useState} = React; + + export default function Component() { + const [state, setState] = useState(1); + useEffect(() => { + console.error("Expected: error during effect"); + }); + console.warn("Expected: warning during render"); + return null; + } + `); + }); + expect(store).toMatchInlineSnapshot(` + ✕ 0, ⚠ 1 + [root] + ⚠ + `); + + withErrorsOrWarningsIgnored(['Expected:'], () => { + patch(` + const {useEffect, useState} = React; + + export default function Component() { + const [state, setState] = useState(1); + console.warn("Expected: warning during render"); + return null; + } + `); + }); + expect(store).toMatchInlineSnapshot(` + ✕ 0, ⚠ 1 + [root] + ⚠ + `); + }); + + // @reactVersion >= 18.0 + it('should not break when there are warnings in between patching (with post commit hook)', () => { withErrorsOrWarningsIgnored(['Expected:'], () => { render(` const {useState} = React; diff --git a/packages/react-devtools-shared/src/hook.js b/packages/react-devtools-shared/src/hook.js index d754140f96541..699d23094571b 100644 --- a/packages/react-devtools-shared/src/hook.js +++ b/packages/react-devtools-shared/src/hook.js @@ -648,6 +648,7 @@ export function installHook( checkDCE, onCommitFiberUnmount, onCommitFiberRoot, + // React v18.0+ onPostCommitFiberRoot, setStrictMode, From c8c89fab5beaa481d132318437b2651ec89440c3 Mon Sep 17 00:00:00 2001 From: lauren Date: Thu, 2 Jan 2025 11:24:26 -0500 Subject: [PATCH 0021/1160] [compiler] Update rollup plugins (#31919) Update our various compiler rollup plugins. --- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/facebook/react/pull/31919). * #31927 * #31918 * #31917 * #31916 * __->__ #31919 --- compiler/package.json | 6 +- .../rollup.config.js | 1 + .../rollup.config.js | 1 + .../rollup.config.js | 1 + .../react-compiler-runtime/rollup.config.js | 1 + compiler/yarn.lock | 75 ++++++++----------- 6 files changed, 38 insertions(+), 47 deletions(-) diff --git a/compiler/package.json b/compiler/package.json index c05e0e70d3e35..b25031b9967d8 100644 --- a/compiler/package.json +++ b/compiler/package.json @@ -26,11 +26,11 @@ "react-is": "0.0.0-experimental-4beb1fd8-20241118" }, "devDependencies": { - "@rollup/plugin-commonjs": "^25.0.7", + "@rollup/plugin-commonjs": "^28.0.2", "@rollup/plugin-json": "^6.1.0", - "@rollup/plugin-node-resolve": "^15.2.3", + "@rollup/plugin-node-resolve": "^16.0.0", "@rollup/plugin-terser": "^0.4.4", - "@rollup/plugin-typescript": "^11.1.6", + "@rollup/plugin-typescript": "^12.1.2", "@tsconfig/strictest": "^2.0.5", "concurrently": "^7.4.0", "folder-hash": "^4.0.4", diff --git a/compiler/packages/babel-plugin-react-compiler/rollup.config.js b/compiler/packages/babel-plugin-react-compiler/rollup.config.js index 77e785c4641f2..b95cc89b39b13 100644 --- a/compiler/packages/babel-plugin-react-compiler/rollup.config.js +++ b/compiler/packages/babel-plugin-react-compiler/rollup.config.js @@ -24,6 +24,7 @@ const DEV_ROLLUP_CONFIG = { format: 'cjs', sourcemap: false, exports: 'named', + inlineDynamicImports: true, }, plugins: [ typescript({ diff --git a/compiler/packages/eslint-plugin-react-compiler/rollup.config.js b/compiler/packages/eslint-plugin-react-compiler/rollup.config.js index 4d813564098a4..743e4cc844102 100644 --- a/compiler/packages/eslint-plugin-react-compiler/rollup.config.js +++ b/compiler/packages/eslint-plugin-react-compiler/rollup.config.js @@ -29,6 +29,7 @@ const DEV_ROLLUP_CONFIG = { file: 'dist/index.js', format: 'cjs', sourcemap: false, + inlineDynamicImports: true, }, treeshake: { moduleSideEffects: false, diff --git a/compiler/packages/react-compiler-healthcheck/rollup.config.js b/compiler/packages/react-compiler-healthcheck/rollup.config.js index 117974ad6b776..0c2492d14069d 100644 --- a/compiler/packages/react-compiler-healthcheck/rollup.config.js +++ b/compiler/packages/react-compiler-healthcheck/rollup.config.js @@ -33,6 +33,7 @@ const DEV_ROLLUP_CONFIG = { format: 'cjs', sourcemap: false, exports: 'named', + inlineDynamicImports: true, }, plugins: [ typescript({ diff --git a/compiler/packages/react-compiler-runtime/rollup.config.js b/compiler/packages/react-compiler-runtime/rollup.config.js index 260359dd0cd81..2399f2160c223 100644 --- a/compiler/packages/react-compiler-runtime/rollup.config.js +++ b/compiler/packages/react-compiler-runtime/rollup.config.js @@ -21,6 +21,7 @@ const PROD_ROLLUP_CONFIG = { file: 'dist/index.js', format: 'cjs', sourcemap: true, + inlineDynamicImports: true, }, plugins: [ typescript({ diff --git a/compiler/yarn.lock b/compiler/yarn.lock index b4c72ff3c5ede..21f771c93fd68 100644 --- a/compiler/yarn.lock +++ b/compiler/yarn.lock @@ -2512,17 +2512,18 @@ resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg== -"@rollup/plugin-commonjs@^25.0.7": - version "25.0.7" - resolved "https://registry.yarnpkg.com/@rollup/plugin-commonjs/-/plugin-commonjs-25.0.7.tgz#145cec7589ad952171aeb6a585bbeabd0fd3b4cf" - integrity sha512-nEvcR+LRjEjsaSsc4x3XZfCCvZIaSMenZu/OiwOKGN2UhQpAYI7ru7czFvyWbErlpoGjnSX3D5Ch5FcMA3kRWQ== +"@rollup/plugin-commonjs@^28.0.2": + version "28.0.2" + resolved "https://registry.yarnpkg.com/@rollup/plugin-commonjs/-/plugin-commonjs-28.0.2.tgz#193d7a86470f112b56927c1d821ee45951a819ea" + integrity sha512-BEFI2EDqzl+vA1rl97IDRZ61AIwGH093d9nz8+dThxJNH8oSoB7MjWvPCX3dkaK1/RCJ/1v/R1XB15FuSs0fQw== dependencies: "@rollup/pluginutils" "^5.0.1" commondir "^1.0.1" estree-walker "^2.0.2" - glob "^8.0.3" + fdir "^6.2.0" is-reference "1.2.1" magic-string "^0.30.3" + picomatch "^4.0.2" "@rollup/plugin-json@^6.1.0": version "6.1.0" @@ -2531,15 +2532,14 @@ dependencies: "@rollup/pluginutils" "^5.1.0" -"@rollup/plugin-node-resolve@^15.2.3": - version "15.2.3" - resolved "https://registry.yarnpkg.com/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.2.3.tgz#e5e0b059bd85ca57489492f295ce88c2d4b0daf9" - integrity sha512-j/lym8nf5E21LwBT4Df1VD6hRO2L2iwUeUmP7litikRsVp1H6NWx20NEp0Y7su+7XGc476GnXXc4kFeZNGmaSQ== +"@rollup/plugin-node-resolve@^16.0.0": + version "16.0.0" + resolved "https://registry.yarnpkg.com/@rollup/plugin-node-resolve/-/plugin-node-resolve-16.0.0.tgz#b1a0594661f40d7b061d82136e847354ff85f211" + integrity sha512-0FPvAeVUT/zdWoO0jnb/V5BlBsUSNfkIOtFHzMO4H9MOklrmQFY6FduVHKucNb/aTFxvnGhj4MNj/T1oNdDfNg== dependencies: "@rollup/pluginutils" "^5.0.1" "@types/resolve" "1.20.2" deepmerge "^4.2.2" - is-builtin-module "^3.2.1" is-module "^1.0.0" resolve "^1.22.1" @@ -2552,10 +2552,10 @@ smob "^1.0.0" terser "^5.17.4" -"@rollup/plugin-typescript@^11.1.6": - version "11.1.6" - resolved "https://registry.yarnpkg.com/@rollup/plugin-typescript/-/plugin-typescript-11.1.6.tgz#724237d5ec12609ec01429f619d2a3e7d4d1b22b" - integrity sha512-R92yOmIACgYdJ7dJ97p4K69I8gg6IEHt8M7dUBxN3W6nrO8uUxX5ixl0yU/N3aZTi8WhPuICvOHXQvF6FaykAA== +"@rollup/plugin-typescript@^12.1.2": + version "12.1.2" + resolved "https://registry.yarnpkg.com/@rollup/plugin-typescript/-/plugin-typescript-12.1.2.tgz#ebaeec2e7376faa889030ccd7cb485a649e63118" + integrity sha512-cdtSp154H5sv637uMr1a8OTWB0L1SWDSm1rDGiyfcGcvQ6cuTs4MDk2BVEBGysUWago4OJN4EQZqOTl/QY3Jgg== dependencies: "@rollup/pluginutils" "^5.1.0" resolve "^1.22.1" @@ -3621,11 +3621,6 @@ buffer@^5.5.0: base64-js "^1.3.1" ieee754 "^1.1.13" -builtin-modules@^3.3.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-3.3.0.tgz#cae62812b89801e9656336e46223e030386be7b6" - integrity sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw== - call-bind@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c" @@ -4408,6 +4403,11 @@ fbt@^1.0.2: dependencies: invariant "^2.2.4" +fdir@^6.2.0: + version "6.4.2" + resolved "https://registry.yarnpkg.com/fdir/-/fdir-6.4.2.tgz#ddaa7ce1831b161bc3657bb99cb36e1622702689" + integrity sha512-KnhMXsKSPZlAhp7+IjUkRZKPb4fUyccpDrdFXbi4QL1qkmFh9kVY09Yox+n4MaOb3lHZ1Tv829C3oaaXoMYPDQ== + file-entry-cache@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027" @@ -4592,17 +4592,6 @@ glob@^7.1.3, glob@^7.1.4, glob@^7.1.6: once "^1.3.0" path-is-absolute "^1.0.0" -glob@^8.0.3: - version "8.1.0" - resolved "https://registry.yarnpkg.com/glob/-/glob-8.1.0.tgz#d388f656593ef708ee3e34640fdfb99a9fd1c33e" - integrity sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ== - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^5.0.1" - once "^1.3.0" - globals@^11.1.0: version "11.12.0" resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" @@ -4804,13 +4793,6 @@ is-arrayish@^0.2.1: resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" integrity sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg== -is-builtin-module@^3.2.1: - version "3.2.1" - resolved "https://registry.yarnpkg.com/is-builtin-module/-/is-builtin-module-3.2.1.tgz#f03271717d8654cfcaf07ab0463faa3571581169" - integrity sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A== - dependencies: - builtin-modules "^3.3.0" - is-core-module@^2.11.0: version "2.12.1" resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.12.1.tgz#0c0b6885b6f80011c71541ce15c8d66cf5a4f9fd" @@ -6429,13 +6411,6 @@ minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: dependencies: brace-expansion "^1.1.7" -minimatch@^5.0.1, minimatch@~5.1.2: - version "5.1.6" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.6.tgz#1cfcb8cf5522ea69952cd2af95ae09477f122a96" - integrity sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g== - dependencies: - brace-expansion "^2.0.1" - minimatch@^9.0.4: version "9.0.5" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.5.tgz#d74f9dd6b57d83d8e98cfb82133b03978bc929e5" @@ -6443,6 +6418,13 @@ minimatch@^9.0.4: dependencies: brace-expansion "^2.0.1" +minimatch@~5.1.2: + version "5.1.6" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.6.tgz#1cfcb8cf5522ea69952cd2af95ae09477f122a96" + integrity sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g== + dependencies: + brace-expansion "^2.0.1" + minimist@^1.2.8: version "1.2.8" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" @@ -6724,6 +6706,11 @@ picomatch@^2.0.4, picomatch@^2.2.3, picomatch@^2.3.1: resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== +picomatch@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.2.tgz#77c742931e8f3b8820946c76cd0c1f13730d1dab" + integrity sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg== + pify@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/pify/-/pify-4.0.1.tgz#4b2cd25c50d598735c50292224fd8c6df41e3231" From fe21c947c82b173ae538aa1d215559ec3dccd103 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Thu, 2 Jan 2025 13:02:22 -0500 Subject: [PATCH 0022/1160] [Fiber] Yield every other frame for Transition/Retry work (#31828) This flag first moves the `shouldYield()` logic into React itself. We need this for `postTask` compatibility anyway since this logic is no longer a concern of the scheduler. This means that there can also be no global `requestPaint()` that asks for painting earlier. So this is best rolled out with `enableAlwaysYieldScheduler` (and ideally `enableYieldingBeforePassive`) instead of `enableRequestPaint`. Once in React we can change the yield timing heuristics. This uses the previous 5ms for Idle work to keep everything responsive while doing background work. However, for Transitions and Retries we have seen that same thread animations (like loading states animating, or constant animations like cool Three.js stuff) can take CPU time away from the Transition that causes moving into new content to slow down. Therefore we only yield every 25ms. The purpose of this yield is not to avoid the overhead of yielding, which is very low, but rather to intentionally block any frequently occurring other main thread work like animations from starving our work. If we could we could just tell everyone else to throttle their stuff for ideal scheduling but that's not quite realistic. In other words, the purpose of this is to reduce the frame rate of animations to 30 fps and we achieve this by not yielding. We still do yield to allow the animations to not just stall. This seems like a good balance. The 5ms of Idle is because we don't really need to yield less often since the overhead is low. We keep it low to allow 120 fps animations to run if necessary and our work may not be the only work within a frame so we need to yield early enough to leave enough time left. Similarly we choose 25ms rather than say 35ms to ensure that we push long enough to guarantee to half the frame rate but low enough that there's plenty of time left for a rAF to power each animation every other frame. It's also low enough that if something else interrupts the work like a new interaction, we can still be responsive to that within 50ms or so. We also need to yield in case there's I/O work that needs to get bounced through the main thread. This flag is currently off everywhere since we have so many other scheduling flags but that means there's some urgency to roll those out fully so we can test this one. There's also some tests to update since this doesn't go through the Mock scheduler anymore for yields. --- .../src/ReactFiberWorkLoop.js | 26 ++++++++++++++++--- packages/shared/ReactFeatureFlags.js | 3 +++ .../forks/ReactFeatureFlags.native-fb.js | 1 + .../forks/ReactFeatureFlags.native-oss.js | 2 ++ .../forks/ReactFeatureFlags.test-renderer.js | 2 ++ ...actFeatureFlags.test-renderer.native-fb.js | 1 + .../ReactFeatureFlags.test-renderer.www.js | 2 ++ .../shared/forks/ReactFeatureFlags.www.js | 2 ++ 8 files changed, 36 insertions(+), 3 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.js b/packages/react-reconciler/src/ReactFiberWorkLoop.js index 2e8b443d22caa..8e75135ff6e08 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.js @@ -41,6 +41,7 @@ import { enableSiblingPrerendering, enableComponentPerformanceTrack, enableYieldingBeforePassive, + enableThrottledScheduling, } from 'shared/ReactFeatureFlags'; import ReactSharedInternals from 'shared/ReactSharedInternals'; import is from 'shared/objectIs'; @@ -2610,8 +2611,10 @@ function renderRootConcurrent(root: FiberRoot, lanes: Lanes) { // can't trust the result of `shouldYield`, because the host I/O is // likely mocked. workLoopSync(); + } else if (enableThrottledScheduling) { + workLoopConcurrent(includesNonIdleWork(lanes)); } else { - workLoopConcurrent(); + workLoopConcurrentByScheduler(); } break; } catch (thrownValue) { @@ -2650,10 +2653,27 @@ function renderRootConcurrent(root: FiberRoot, lanes: Lanes) { } /** @noinline */ -function workLoopConcurrent() { +function workLoopConcurrent(nonIdle: boolean) { + // We yield every other "frame" when rendering Transition or Retries. Those are blocking + // revealing new content. The purpose of this yield is not to avoid the overhead of yielding, + // which is very low, but rather to intentionally block any frequently occuring other main + // thread work like animations from starving our work. In other words, the purpose of this + // is to reduce the framerate of animations to 30 frames per second. + // For Idle work we yield every 5ms to keep animations going smooth. + if (workInProgress !== null) { + const yieldAfter = now() + (nonIdle ? 25 : 5); + do { + // $FlowFixMe[incompatible-call] flow doesn't know that now() is side-effect free + performUnitOfWork(workInProgress); + } while (workInProgress !== null && now() < yieldAfter); + } +} + +/** @noinline */ +function workLoopConcurrentByScheduler() { // Perform work until Scheduler asks us to yield while (workInProgress !== null && !shouldYield()) { - // $FlowFixMe[incompatible-call] found when upgrading Flow + // $FlowFixMe[incompatible-call] flow doesn't know that shouldYield() is side-effect free performUnitOfWork(workInProgress); } } diff --git a/packages/shared/ReactFeatureFlags.js b/packages/shared/ReactFeatureFlags.js index 0720ab2a88b78..f1332a2bfd1ec 100644 --- a/packages/shared/ReactFeatureFlags.js +++ b/packages/shared/ReactFeatureFlags.js @@ -81,6 +81,9 @@ export const enableLegacyFBSupport = false; // Fix gated tests that fail with this flag enabled before turning it back on. export const enableYieldingBeforePassive = false; +// Experiment to intentionally yield less to block high framerate animations. +export const enableThrottledScheduling = false; + export const enableLegacyCache = __EXPERIMENTAL__; export const enableAsyncIterableChildren = __EXPERIMENTAL__; diff --git a/packages/shared/forks/ReactFeatureFlags.native-fb.js b/packages/shared/forks/ReactFeatureFlags.native-fb.js index 4ea9499e6ca80..ed06661eedc39 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-fb.js +++ b/packages/shared/forks/ReactFeatureFlags.native-fb.js @@ -81,6 +81,7 @@ export const syncLaneExpirationMs = 250; export const transitionLaneExpirationMs = 5000; export const enableHydrationLaneScheduling = true; export const enableYieldingBeforePassive = false; +export const enableThrottledScheduling = false; // Flow magic to verify the exports of this file match the original version. ((((null: any): ExportsType): FeatureFlagsType): ExportsType); diff --git a/packages/shared/forks/ReactFeatureFlags.native-oss.js b/packages/shared/forks/ReactFeatureFlags.native-oss.js index dddd80aeea82a..2220891b7b426 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-oss.js +++ b/packages/shared/forks/ReactFeatureFlags.native-oss.js @@ -73,6 +73,8 @@ export const enableHydrationLaneScheduling = true; export const enableYieldingBeforePassive = false; +export const enableThrottledScheduling = false; + // Profiling Only export const enableProfilerTimer = __PROFILE__; export const enableProfilerCommitHooks = __PROFILE__; diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.js index 95826007dc9cd..2c5bc8f297ba5 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.js @@ -72,6 +72,8 @@ export const enableUseResourceEffectHook = false; export const enableYieldingBeforePassive = true; +export const enableThrottledScheduling = false; + // TODO: This must be in sync with the main ReactFeatureFlags file because // the Test Renderer's value must be the same as the one used by the // react package. diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js index 81060cfafb1b1..1c2fbb4847960 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js @@ -69,6 +69,7 @@ export const enableSiblingPrerendering = true; export const enableUseResourceEffectHook = true; export const enableHydrationLaneScheduling = true; export const enableYieldingBeforePassive = false; +export const enableThrottledScheduling = false; // Flow magic to verify the exports of this file match the original version. ((((null: any): ExportsType): FeatureFlagsType): ExportsType); diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js index e0e9906d52b8f..8e96038f44246 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js @@ -84,5 +84,7 @@ export const enableHydrationLaneScheduling = true; export const enableYieldingBeforePassive = false; +export const enableThrottledScheduling = false; + // Flow magic to verify the exports of this file match the original version. ((((null: any): ExportsType): FeatureFlagsType): ExportsType); diff --git a/packages/shared/forks/ReactFeatureFlags.www.js b/packages/shared/forks/ReactFeatureFlags.www.js index cf514f93d43ac..658443aea13e9 100644 --- a/packages/shared/forks/ReactFeatureFlags.www.js +++ b/packages/shared/forks/ReactFeatureFlags.www.js @@ -58,6 +58,8 @@ export const enableLegacyFBSupport = true; export const enableYieldingBeforePassive = false; +export const enableThrottledScheduling = false; + export const enableHydrationLaneScheduling = true; export const enableComponentPerformanceTrack = false; From 1e9eb95db5b3a2064ecc26915a4e640b3a9bdaf5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Thu, 2 Jan 2025 13:04:09 -0500 Subject: [PATCH 0023/1160] [Fiber] Mark cascading updates (#31866) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A common source of performance problems is due to cascading renders from calling `setState` in `useLayoutEffect` or `useEffect`. This marks the entry from the update to when we start the render as red and `"Cascade"` to highlight this. Screenshot 2024-12-19 at 10 54 59 PM In addition to this case, there's another case where you call `setState` multiple times in the same event causing multiple renders. This might be due to multiple `flushSync`, or spawned a microtasks from a `useLayoutEffect`. In theory it could also be from a microtask scheduled after the first `setState`. This one we can only detect if it's from an event that has a `window.event` since otherwise it's hard to know if we're still in the same event. Screenshot 2024-12-19 at 11 38 44 PM I decided against making a ping in a microtask considered a cascade. Because that should ideally be using the Suspense Optimization and so wouldn't be considered multi-pass. Screenshot 2024-12-19 at 11 07 30 PM We might consider making the whole render phase and maybe commit phase red but that should maybe reserved for actual errors. The "Blocked" phase really represents the `setState` and so will have the stack trace of the first update. --- .../src/ReactFiberPerformanceTrack.js | 19 +++++++++----- .../src/ReactFiberRootScheduler.js | 26 +++++++++++++------ .../src/ReactFiberWorkLoop.js | 9 +++---- .../src/ReactProfilerTimer.js | 12 +++++++++ 4 files changed, 47 insertions(+), 19 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberPerformanceTrack.js b/packages/react-reconciler/src/ReactFiberPerformanceTrack.js index 61bfd5cf7f844..6e0ebc4e980af 100644 --- a/packages/react-reconciler/src/ReactFiberPerformanceTrack.js +++ b/packages/react-reconciler/src/ReactFiberPerformanceTrack.js @@ -276,11 +276,15 @@ export function logBlockingStart( eventTime: number, eventType: null | string, eventIsRepeat: boolean, + isSpawnedUpdate: boolean, renderStartTime: number, lanes: Lanes, ): void { if (supportsUserTiming) { reusableLaneDevToolDetails.track = 'Blocking'; + // If a blocking update was spawned within render or an effect, that's considered a cascading render. + // If you have a second blocking update within the same event, that suggests multiple flushSync or + // setState in a microtask which is also considered a cascade. if (eventTime > 0 && eventType !== null) { // Log the time from the event timeStamp until we called setState. reusableLaneDevToolDetails.color = eventIsRepeat @@ -295,14 +299,17 @@ export function logBlockingStart( } if (updateTime > 0) { // Log the time from when we called setState until we started rendering. - reusableLaneDevToolDetails.color = includesOnlyHydrationOrOffscreenLanes( - lanes, - ) - ? 'tertiary-light' - : 'primary-light'; + reusableLaneDevToolDetails.color = isSpawnedUpdate + ? 'error' + : includesOnlyHydrationOrOffscreenLanes(lanes) + ? 'tertiary-light' + : 'primary-light'; reusableLaneOptions.start = updateTime; reusableLaneOptions.end = renderStartTime; - performance.measure('Blocked', reusableLaneOptions); + performance.measure( + isSpawnedUpdate ? 'Cascade' : 'Blocked', + reusableLaneOptions, + ); } } } diff --git a/packages/react-reconciler/src/ReactFiberRootScheduler.js b/packages/react-reconciler/src/ReactFiberRootScheduler.js index dcaadc5a6ef18..e3791f4c8dcff 100644 --- a/packages/react-reconciler/src/ReactFiberRootScheduler.js +++ b/packages/react-reconciler/src/ReactFiberRootScheduler.js @@ -128,12 +128,12 @@ export function ensureRootIsScheduled(root: FiberRoot): void { // We're inside an `act` scope. if (!didScheduleMicrotask_act) { didScheduleMicrotask_act = true; - scheduleImmediateTask(processRootScheduleInMicrotask); + scheduleImmediateRootScheduleTask(); } } else { if (!didScheduleMicrotask) { didScheduleMicrotask = true; - scheduleImmediateTask(processRootScheduleInMicrotask); + scheduleImmediateRootScheduleTask(); } } @@ -229,13 +229,17 @@ function flushSyncWorkAcrossRoots_impl( isFlushingWork = false; } -function processRootScheduleInMicrotask() { +function processRootScheduleInImmediateTask() { if (enableProfilerTimer && enableComponentPerformanceTrack) { // Track the currently executing event if there is one so we can ignore this // event when logging events. trackSchedulerEvent(); } + processRootScheduleInMicrotask(); +} + +function processRootScheduleInMicrotask() { // This function is always called inside a microtask. It should never be // called synchronously. didScheduleMicrotask = false; @@ -558,7 +562,7 @@ function cancelCallback(callbackNode: mixed) { } } -function scheduleImmediateTask(cb: () => mixed) { +function scheduleImmediateRootScheduleTask() { if (__DEV__ && ReactSharedInternals.actQueue !== null) { // Special case: Inside an `act` scope, we push microtasks to the fake `act` // callback queue. This is because we currently support calling `act` @@ -566,7 +570,7 @@ function scheduleImmediateTask(cb: () => mixed) { // that you always await the result so that the microtasks have a chance to // run. But it hasn't happened yet. ReactSharedInternals.actQueue.push(() => { - cb(); + processRootScheduleInMicrotask(); return null; }); } @@ -588,14 +592,20 @@ function scheduleImmediateTask(cb: () => mixed) { // wrong semantically but it prevents an infinite loop. The bug is // Safari's, not ours, so we just do our best to not crash even though // the behavior isn't completely correct. - Scheduler_scheduleCallback(ImmediateSchedulerPriority, cb); + Scheduler_scheduleCallback( + ImmediateSchedulerPriority, + processRootScheduleInImmediateTask, + ); return; } - cb(); + processRootScheduleInMicrotask(); }); } else { // If microtasks are not supported, use Scheduler. - Scheduler_scheduleCallback(ImmediateSchedulerPriority, cb); + Scheduler_scheduleCallback( + ImmediateSchedulerPriority, + processRootScheduleInImmediateTask, + ); } } diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.js b/packages/react-reconciler/src/ReactFiberWorkLoop.js index 8e75135ff6e08..133c91f518dfc 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.js @@ -236,6 +236,7 @@ import { blockingEventTime, blockingEventType, blockingEventIsRepeat, + blockingSpawnedUpdate, blockingSuspendedTime, transitionClampTime, transitionStartTime, @@ -1664,11 +1665,8 @@ export function flushSyncWork(): boolean { export function isAlreadyRendering(): boolean { // Used by the renderer to print a warning if certain APIs are called from - // the wrong context. - return ( - __DEV__ && - (executionContext & (RenderContext | CommitContext)) !== NoContext - ); + // the wrong context, and for profiling warnings. + return (executionContext & (RenderContext | CommitContext)) !== NoContext; } export function isInvalidExecutionContextForEventFunction(): boolean { @@ -1797,6 +1795,7 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber { clampedEventTime, blockingEventType, blockingEventIsRepeat, + blockingSpawnedUpdate, renderStartTime, lanes, ); diff --git a/packages/react-reconciler/src/ReactProfilerTimer.js b/packages/react-reconciler/src/ReactProfilerTimer.js index d408ba0ff7bd0..d3bdf6f6a9c7f 100644 --- a/packages/react-reconciler/src/ReactProfilerTimer.js +++ b/packages/react-reconciler/src/ReactProfilerTimer.js @@ -30,6 +30,8 @@ import { enableComponentPerformanceTrack, } from 'shared/ReactFeatureFlags'; +import {isAlreadyRendering} from './ReactFiberWorkLoop'; + // Intentionally not named imports because Rollup would use dynamic dispatch for // CommonJS interop named imports. import * as Scheduler from 'scheduler'; @@ -50,6 +52,7 @@ export let blockingUpdateTime: number = -1.1; // First sync setState scheduled. export let blockingEventTime: number = -1.1; // Event timeStamp of the first setState. export let blockingEventType: null | string = null; // Event type of the first setState. export let blockingEventIsRepeat: boolean = false; +export let blockingSpawnedUpdate: boolean = false; export let blockingSuspendedTime: number = -1.1; // TODO: This should really be one per Transition lane. export let transitionClampTime: number = -0; @@ -78,6 +81,9 @@ export function startUpdateTimerByLane(lane: Lane): void { if (isSyncLane(lane) || isBlockingLane(lane)) { if (blockingUpdateTime < 0) { blockingUpdateTime = now(); + if (isAlreadyRendering()) { + blockingSpawnedUpdate = true; + } const newEventTime = resolveEventTimeStamp(); const newEventType = resolveEventType(); if ( @@ -85,6 +91,11 @@ export function startUpdateTimerByLane(lane: Lane): void { newEventType !== blockingEventType ) { blockingEventIsRepeat = false; + } else if (newEventType !== null) { + // If this is a second update in the same event, we treat it as a spawned update. + // This might be a microtask spawned from useEffect, multiple flushSync or + // a setState in a microtask spawned after the first setState. Regardless it's bad. + blockingSpawnedUpdate = true; } blockingEventTime = newEventTime; blockingEventType = newEventType; @@ -141,6 +152,7 @@ export function clearBlockingTimers(): void { blockingUpdateTime = -1.1; blockingSuspendedTime = -1.1; blockingEventIsRepeat = true; + blockingSpawnedUpdate = false; } export function startAsyncTransitionTimer(): void { From 0de1233fd180969f7ffdfc98151922f2466ceb1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Thu, 2 Jan 2025 13:28:24 -0500 Subject: [PATCH 0024/1160] [Fiber] Mark error boundaries and commit phases when an error is thrown (#31876) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This tracks commit phase errors and marks the component that errored as red. These also get the errors attached to the entry. Screenshot 2024-12-20 at 2 40 14 PM In the render phase I just mark the Error Boundary that caught the error. We don't have access to the actual error since it's locked behind closures in the update queue. We could probably expose that someway. Screenshot 2024-12-20 at 1 49 05 PM Follow ups: Since the Error Boundary doesn't commit its attempted render, we don't log those. If we did then maybe we should just mark the errored component like I do for the commit phase. We could potentially walk the list of errors and log the captured fibers and just log their entries as children. We could also potentially walk the uncommitted Fiber tree by stashing it somewhere or even getting it from the alternate. This could be done on Suspense boundaries too to track failed hydrations. --------- Co-authored-by: Ricky --- .../src/ReactFiberCommitWork.js | 117 ++++++++++++----- .../src/ReactFiberPerformanceTrack.js | 118 +++++++++++++++++- .../src/ReactFiberWorkLoop.js | 18 ++- .../src/ReactProfilerTimer.js | 43 +++++++ 4 files changed, 262 insertions(+), 34 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.js b/packages/react-reconciler/src/ReactFiberCommitWork.js index 43b726e981007..a244c65e3e886 100644 --- a/packages/react-reconciler/src/ReactFiberCommitWork.js +++ b/packages/react-reconciler/src/ReactFiberCommitWork.js @@ -98,6 +98,7 @@ import { Cloned, PerformedWork, ForceClientRender, + DidCapture, } from './ReactFiberFlags'; import { commitStartTime, @@ -107,14 +108,17 @@ import { resetComponentEffectTimers, pushComponentEffectStart, popComponentEffectStart, + pushComponentEffectErrors, + popComponentEffectErrors, componentEffectStartTime, componentEffectEndTime, componentEffectDuration, + componentEffectErrors, } from './ReactProfilerTimer'; import { logComponentRender, + logComponentErrored, logComponentEffect, - logSuspenseBoundaryClientRendered, } from './ReactFiberPerformanceTrack'; import {ConcurrentMode, NoMode, ProfileMode} from './ReactTypeOfMode'; import {deferHiddenCallbacks} from './ReactFiberClassUpdateQueue'; @@ -395,7 +399,7 @@ function commitLayoutEffectOnFiber( committedLanes: Lanes, ): void { const prevEffectStart = pushComponentEffectStart(); - + const prevEffectErrors = pushComponentEffectErrors(); // When updating this function, also update reappearLayoutEffects, which does // most of the same things when an offscreen tree goes from hidden -> visible. const flags = finishedWork.flags; @@ -631,10 +635,12 @@ function commitLayoutEffectOnFiber( componentEffectStartTime, componentEffectEndTime, componentEffectDuration, + componentEffectErrors, ); } popComponentEffectStart(prevEffectStart); + popComponentEffectErrors(prevEffectErrors); } function abortRootTransitions( @@ -1627,7 +1633,7 @@ function commitMutationEffectsOnFiber( lanes: Lanes, ) { const prevEffectStart = pushComponentEffectStart(); - + const prevEffectErrors = pushComponentEffectErrors(); const current = finishedWork.alternate; const flags = finishedWork.flags; @@ -2136,10 +2142,12 @@ function commitMutationEffectsOnFiber( componentEffectStartTime, componentEffectEndTime, componentEffectDuration, + componentEffectErrors, ); } popComponentEffectStart(prevEffectStart); + popComponentEffectErrors(prevEffectErrors); } function commitReconciliationEffects(finishedWork: Fiber) { @@ -2212,7 +2220,7 @@ function recursivelyTraverseLayoutEffects( export function disappearLayoutEffects(finishedWork: Fiber) { const prevEffectStart = pushComponentEffectStart(); - + const prevEffectErrors = pushComponentEffectErrors(); switch (finishedWork.tag) { case FunctionComponent: case ForwardRef: @@ -2285,10 +2293,12 @@ export function disappearLayoutEffects(finishedWork: Fiber) { componentEffectStartTime, componentEffectEndTime, componentEffectDuration, + componentEffectErrors, ); } popComponentEffectStart(prevEffectStart); + popComponentEffectErrors(prevEffectErrors); } function recursivelyTraverseDisappearLayoutEffects(parentFiber: Fiber) { @@ -2310,7 +2320,7 @@ export function reappearLayoutEffects( includeWorkInProgressEffects: boolean, ) { const prevEffectStart = pushComponentEffectStart(); - + const prevEffectErrors = pushComponentEffectErrors(); // Turn on layout effects in a tree that previously disappeared. const flags = finishedWork.flags; switch (finishedWork.tag) { @@ -2461,10 +2471,12 @@ export function reappearLayoutEffects( componentEffectStartTime, componentEffectEndTime, componentEffectDuration, + componentEffectErrors, ); } popComponentEffectStart(prevEffectStart); + popComponentEffectErrors(prevEffectErrors); } function recursivelyTraverseReappearLayoutEffects( @@ -2701,26 +2713,7 @@ function commitPassiveMountOnFiber( endTime: number, // Profiling-only. The start time of the next Fiber or root completion. ): void { const prevEffectStart = pushComponentEffectStart(); - - // If this component rendered in Profiling mode (DEV or in Profiler component) then log its - // render time. We do this after the fact in the passive effect to avoid the overhead of this - // getting in the way of the render characteristics and avoid the overhead of unwinding - // uncommitted renders. - if ( - enableProfilerTimer && - enableComponentPerformanceTrack && - (finishedWork.mode & ProfileMode) !== NoMode && - ((finishedWork.actualStartTime: any): number) > 0 && - (finishedWork.flags & PerformedWork) !== NoFlags - ) { - logComponentRender( - finishedWork, - ((finishedWork.actualStartTime: any): number), - endTime, - inHydratedSubtree, - ); - } - + const prevEffectErrors = pushComponentEffectErrors(); // When updating this function, also update reconnectPassiveEffects, which does // most of the same things when an offscreen tree goes from hidden -> visible, // or when toggling effects inside a hidden tree. @@ -2729,6 +2722,25 @@ function commitPassiveMountOnFiber( case FunctionComponent: case ForwardRef: case SimpleMemoComponent: { + // If this component rendered in Profiling mode (DEV or in Profiler component) then log its + // render time. We do this after the fact in the passive effect to avoid the overhead of this + // getting in the way of the render characteristics and avoid the overhead of unwinding + // uncommitted renders. + if ( + enableProfilerTimer && + enableComponentPerformanceTrack && + (finishedWork.mode & ProfileMode) !== NoMode && + ((finishedWork.actualStartTime: any): number) > 0 && + (finishedWork.flags & PerformedWork) !== NoFlags + ) { + logComponentRender( + finishedWork, + ((finishedWork.actualStartTime: any): number), + endTime, + inHydratedSubtree, + ); + } + recursivelyTraversePassiveMountEffects( finishedRoot, finishedWork, @@ -2744,6 +2756,45 @@ function commitPassiveMountOnFiber( } break; } + case ClassComponent: { + // If this component rendered in Profiling mode (DEV or in Profiler component) then log its + // render time. We do this after the fact in the passive effect to avoid the overhead of this + // getting in the way of the render characteristics and avoid the overhead of unwinding + // uncommitted renders. + if ( + enableProfilerTimer && + enableComponentPerformanceTrack && + (finishedWork.mode & ProfileMode) !== NoMode && + ((finishedWork.actualStartTime: any): number) > 0 + ) { + if ((finishedWork.flags & DidCapture) !== NoFlags) { + logComponentErrored( + finishedWork, + ((finishedWork.actualStartTime: any): number), + endTime, + // TODO: The captured values are all hidden inside the updater/callback closures so + // we can't get to the errors but they're there so we should be able to log them. + [], + ); + } else if ((finishedWork.flags & PerformedWork) !== NoFlags) { + logComponentRender( + finishedWork, + ((finishedWork.actualStartTime: any): number), + endTime, + inHydratedSubtree, + ); + } + } + + recursivelyTraversePassiveMountEffects( + finishedRoot, + finishedWork, + committedLanes, + committedTransitions, + endTime, + ); + break; + } case HostRoot: { const prevEffectDuration = pushNestedEffectDurations(); @@ -2891,7 +2942,7 @@ function commitPassiveMountOnFiber( // rendered boundary. Such as postpone. if (hydrationErrors !== null) { const startTime: number = (finishedWork.actualStartTime: any); - logSuspenseBoundaryClientRendered( + logComponentErrored( finishedWork, startTime, endTime, @@ -3074,10 +3125,12 @@ function commitPassiveMountOnFiber( componentEffectStartTime, componentEffectEndTime, componentEffectDuration, + componentEffectErrors, ); } popComponentEffectStart(prevEffectStart); + popComponentEffectErrors(prevEffectErrors); } function recursivelyTraverseReconnectPassiveEffects( @@ -3137,7 +3190,7 @@ export function reconnectPassiveEffects( endTime: number, // Profiling-only. The start time of the next Fiber or root completion. ) { const prevEffectStart = pushComponentEffectStart(); - + const prevEffectErrors = pushComponentEffectErrors(); // If this component rendered in Profiling mode (DEV or in Profiler component) then log its // render time. We do this after the fact in the passive effect to avoid the overhead of this // getting in the way of the render characteristics and avoid the overhead of unwinding @@ -3331,10 +3384,12 @@ export function reconnectPassiveEffects( componentEffectStartTime, componentEffectEndTime, componentEffectDuration, + componentEffectErrors, ); } popComponentEffectStart(prevEffectStart); + popComponentEffectErrors(prevEffectErrors); } function recursivelyTraverseAtomicPassiveEffects( @@ -3611,7 +3666,7 @@ function recursivelyTraversePassiveUnmountEffects(parentFiber: Fiber): void { function commitPassiveUnmountOnFiber(finishedWork: Fiber): void { const prevEffectStart = pushComponentEffectStart(); - + const prevEffectErrors = pushComponentEffectErrors(); switch (finishedWork.tag) { case FunctionComponent: case ForwardRef: @@ -3696,10 +3751,12 @@ function commitPassiveUnmountOnFiber(finishedWork: Fiber): void { componentEffectStartTime, componentEffectEndTime, componentEffectDuration, + componentEffectErrors, ); } popComponentEffectStart(prevEffectStart); + popComponentEffectErrors(prevEffectErrors); } function recursivelyTraverseDisconnectPassiveEffects(parentFiber: Fiber): void { @@ -3819,7 +3876,7 @@ function commitPassiveUnmountInsideDeletedTreeOnFiber( nearestMountedAncestor: Fiber | null, ): void { const prevEffectStart = pushComponentEffectStart(); - + const prevEffectErrors = pushComponentEffectErrors(); switch (current.tag) { case FunctionComponent: case ForwardRef: @@ -3946,10 +4003,12 @@ function commitPassiveUnmountInsideDeletedTreeOnFiber( componentEffectStartTime, componentEffectEndTime, componentEffectDuration, + componentEffectErrors, ); } popComponentEffectStart(prevEffectStart); + popComponentEffectErrors(prevEffectErrors); } export function invokeLayoutEffectMountInDEV(fiber: Fiber): void { diff --git a/packages/react-reconciler/src/ReactFiberPerformanceTrack.js b/packages/react-reconciler/src/ReactFiberPerformanceTrack.js index 6e0ebc4e980af..ae48ed383413a 100644 --- a/packages/react-reconciler/src/ReactFiberPerformanceTrack.js +++ b/packages/react-reconciler/src/ReactFiberPerformanceTrack.js @@ -13,6 +13,8 @@ import type {Lanes} from './ReactFiberLane'; import type {CapturedValue} from './ReactCapturedValue'; +import {SuspenseComponent} from './ReactWorkTags'; + import getComponentNameFromFiber from './getComponentNameFromFiber'; import { @@ -159,13 +161,64 @@ export function logComponentRender( } } -export function logSuspenseBoundaryClientRendered( +export function logComponentErrored( + fiber: Fiber, + startTime: number, + endTime: number, + errors: Array>, +): void { + if (supportsUserTiming) { + const name = getComponentNameFromFiber(fiber); + if (name === null) { + // Skip + return; + } + const properties = []; + if (__DEV__) { + for (let i = 0; i < errors.length; i++) { + const capturedValue = errors[i]; + const error = capturedValue.value; + const message = + typeof error === 'object' && + error !== null && + typeof error.message === 'string' + ? // eslint-disable-next-line react-internal/safe-string-coercion + String(error.message) + : // eslint-disable-next-line react-internal/safe-string-coercion + String(error); + properties.push(['Error', message]); + } + } + performance.measure(name, { + start: startTime, + end: endTime, + detail: { + devtools: { + color: 'error', + track: COMPONENTS_TRACK, + tooltipText: + fiber.tag === SuspenseComponent + ? 'Hydration failed' + : 'Error boundary caught an error', + properties, + }, + }, + }); + } +} + +function logComponentEffectErrored( fiber: Fiber, startTime: number, endTime: number, errors: Array>, ): void { if (supportsUserTiming) { + const name = getComponentNameFromFiber(fiber); + if (name === null) { + // Skip + return; + } const properties = []; if (__DEV__) { for (let i = 0; i < errors.length; i++) { @@ -182,14 +235,14 @@ export function logSuspenseBoundaryClientRendered( properties.push(['Error', message]); } } - performance.measure('Suspense', { + performance.measure(name, { start: startTime, end: endTime, detail: { devtools: { color: 'error', track: COMPONENTS_TRACK, - tooltipText: 'Hydration failed', + tooltipText: 'A lifecycle or effect errored', properties, }, }, @@ -202,7 +255,12 @@ export function logComponentEffect( startTime: number, endTime: number, selfTime: number, + errors: null | Array>, ): void { + if (errors !== null) { + logComponentEffectErrored(fiber, startTime, endTime, errors); + return; + } const name = getComponentNameFromFiber(fiber); if (name === null) { // Skip @@ -534,7 +592,54 @@ export function logSuspendedCommitPhase( } } -export function logCommitPhase(startTime: number, endTime: number): void { +export function logCommitErrored( + startTime: number, + endTime: number, + errors: Array>, + passive: boolean, +): void { + if (supportsUserTiming) { + const properties = []; + if (__DEV__) { + for (let i = 0; i < errors.length; i++) { + const capturedValue = errors[i]; + const error = capturedValue.value; + const message = + typeof error === 'object' && + error !== null && + typeof error.message === 'string' + ? // eslint-disable-next-line react-internal/safe-string-coercion + String(error.message) + : // eslint-disable-next-line react-internal/safe-string-coercion + String(error); + properties.push(['Error', message]); + } + } + performance.measure('Errored', { + start: startTime, + end: endTime, + detail: { + devtools: { + color: 'error', + track: reusableLaneDevToolDetails.track, + trackGroup: LANES_TRACK_GROUP, + tooltipText: passive ? 'Remaining Effects Errored' : 'Commit Errored', + properties, + }, + }, + }); + } +} + +export function logCommitPhase( + startTime: number, + endTime: number, + errors: null | Array>, +): void { + if (errors !== null) { + logCommitErrored(startTime, endTime, errors, false); + return; + } if (supportsUserTiming) { reusableLaneDevToolDetails.color = 'secondary-dark'; reusableLaneOptions.start = startTime; @@ -562,7 +667,12 @@ export function logPaintYieldPhase( export function logPassiveCommitPhase( startTime: number, endTime: number, + errors: null | Array>, ): void { + if (errors !== null) { + logCommitErrored(startTime, endTime, errors, true); + return; + } if (supportsUserTiming) { reusableLaneDevToolDetails.color = 'secondary-dark'; reusableLaneOptions.start = startTime; diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.js b/packages/react-reconciler/src/ReactFiberWorkLoop.js index 133c91f518dfc..ded26ed6e2bfb 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.js @@ -253,6 +253,7 @@ import { renderStartTime, commitStartTime, commitEndTime, + commitErrors, recordRenderTime, recordCommitTime, recordCommitEndTime, @@ -264,6 +265,8 @@ import { yieldStartTime, yieldReason, startPingTimerByLanes, + recordEffectError, + resetCommitErrors, } from './ReactProfilerTimer'; // DEV stuff @@ -3340,6 +3343,7 @@ function commitRootImpl( if (enableProfilerTimer) { // Mark the current commit time to be shared by all Profilers in this // batch. This enables them to be grouped later. + resetCommitErrors(); recordCommitTime(); if (enableComponentPerformanceTrack) { if (suspendedCommitReason === SUSPENDED_COMMIT) { @@ -3433,6 +3437,7 @@ function commitRootImpl( ? completedRenderEndTime : commitStartTime, commitEndTime, + commitErrors, ); } @@ -3722,6 +3727,7 @@ function flushPassiveEffectsImpl(wasDelayedCommit: void | boolean) { let passiveEffectStartTime = 0; if (enableProfilerTimer && enableComponentPerformanceTrack) { + resetCommitErrors(); passiveEffectStartTime = now(); logPaintYieldPhase( commitEndTime, @@ -3758,7 +3764,11 @@ function flushPassiveEffectsImpl(wasDelayedCommit: void | boolean) { if (enableProfilerTimer && enableComponentPerformanceTrack) { const passiveEffectsEndTime = now(); - logPassiveCommitPhase(passiveEffectStartTime, passiveEffectsEndTime); + logPassiveCommitPhase( + passiveEffectStartTime, + passiveEffectsEndTime, + commitErrors, + ); finalizeRender(lanes, passiveEffectsEndTime); } @@ -3842,6 +3852,9 @@ function captureCommitPhaseErrorOnRoot( error: mixed, ) { const errorInfo = createCapturedValueAtFiber(error, sourceFiber); + if (enableProfilerTimer && enableComponentPerformanceTrack) { + recordEffectError(errorInfo); + } const update = createRootErrorUpdate( rootFiber.stateNode, errorInfo, @@ -3883,6 +3896,9 @@ export function captureCommitPhaseError( !isAlreadyFailedLegacyErrorBoundary(instance)) ) { const errorInfo = createCapturedValueAtFiber(error, sourceFiber); + if (enableProfilerTimer && enableComponentPerformanceTrack) { + recordEffectError(errorInfo); + } const update = createClassErrorUpdate((SyncLane: Lane)); const root = enqueueUpdate(fiber, update, (SyncLane: Lane)); if (root !== null) { diff --git a/packages/react-reconciler/src/ReactProfilerTimer.js b/packages/react-reconciler/src/ReactProfilerTimer.js index d3bdf6f6a9c7f..bc289bcab6321 100644 --- a/packages/react-reconciler/src/ReactProfilerTimer.js +++ b/packages/react-reconciler/src/ReactProfilerTimer.js @@ -12,6 +12,9 @@ import type {Fiber} from './ReactInternalTypes'; import type {SuspendedReason} from './ReactFiberWorkLoop'; import type {Lane, Lanes} from './ReactFiberLane'; + +import type {CapturedValue} from './ReactCapturedValue'; + import { isTransitionLane, isBlockingLane, @@ -41,11 +44,13 @@ const {unstable_now: now} = Scheduler; export let renderStartTime: number = -0; export let commitStartTime: number = -0; export let commitEndTime: number = -0; +export let commitErrors: null | Array> = null; export let profilerStartTime: number = -1.1; export let profilerEffectDuration: number = -0; export let componentEffectDuration: number = -0; export let componentEffectStartTime: number = -1.1; export let componentEffectEndTime: number = -1.1; +export let componentEffectErrors: null | Array> = null; export let blockingClampTime: number = -0; export let blockingUpdateTime: number = -1.1; // First sync setState scheduled. @@ -282,6 +287,26 @@ export function popComponentEffectStart(prevEffectStart: number): void { } } +export function pushComponentEffectErrors(): null | Array< + CapturedValue, +> { + if (!enableProfilerTimer || !enableProfilerCommitHooks) { + return null; + } + const prevErrors = componentEffectErrors; + componentEffectErrors = null; + return prevErrors; +} + +export function popComponentEffectErrors( + prevErrors: null | Array>, +): void { + if (!enableProfilerTimer || !enableProfilerCommitHooks) { + return; + } + componentEffectErrors = prevErrors; +} + /** * Tracks whether the current update was a nested/cascading update (scheduled from a layout effect). * @@ -416,6 +441,24 @@ export function recordEffectDuration(fiber: Fiber): void { } } +export function recordEffectError(errorInfo: CapturedValue): void { + if (!enableProfilerTimer || !enableProfilerCommitHooks) { + return; + } + if (componentEffectErrors === null) { + componentEffectErrors = []; + } + componentEffectErrors.push(errorInfo); + if (commitErrors === null) { + commitErrors = []; + } + commitErrors.push(errorInfo); +} + +export function resetCommitErrors(): void { + commitErrors = null; +} + export function startEffectTimer(): void { if (!enableProfilerTimer || !enableProfilerCommitHooks) { return; From 6ca7fbe884d17ef6c18d143421cc3e232bbba516 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Thu, 2 Jan 2025 14:34:10 -0500 Subject: [PATCH 0025/1160] [Fiber] Gate Update flag on BeforeMutationMask on flags (#31921) We're currently visiting the snapshot phase for every `Update` flag even though we rarely have to do anything in the Snapshot phase. The only flags that seem to use these wider visits is `enableCreateEventHandleAPI` and `enableUseEffectEventHook` but really neither of those should do that neither. They should schedule explicit Snapshot phases if needed. --- .../react-reconciler/src/ReactFiberFlags.js | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberFlags.js b/packages/react-reconciler/src/ReactFiberFlags.js index 66e9249f15026..0fef260ab23fd 100644 --- a/packages/react-reconciler/src/ReactFiberFlags.js +++ b/packages/react-reconciler/src/ReactFiberFlags.js @@ -7,7 +7,10 @@ * @flow */ -import {enableCreateEventHandleAPI} from 'shared/ReactFeatureFlags'; +import { + enableCreateEventHandleAPI, + enableUseEffectEventHook, +} from 'shared/ReactFeatureFlags'; export type Flags = number; @@ -77,17 +80,19 @@ export const MountPassiveDev = /* */ 0b1000000000000000000000000000 // don't contain effects, by checking subtreeFlags. export const BeforeMutationMask: number = - // TODO: Remove Update flag from before mutation phase by re-landing Visibility - // flag logic (see #20043) - Update | Snapshot | (enableCreateEventHandleAPI ? // createEventHandle needs to visit deleted and hidden trees to // fire beforeblur // TODO: Only need to visit Deletions during BeforeMutation phase if an // element is focused. - ChildDeletion | Visibility - : 0); + Update | ChildDeletion | Visibility + : enableUseEffectEventHook + ? // TODO: The useEffectEvent hook uses the snapshot phase for clean up but it + // really should use the mutation phase for this or at least schedule an + // explicit Snapshot phase flag for this. + Update + : 0); export const MutationMask = Placement | From d8b903f49edebdd9ed081ff0514c28fe130cd510 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Thu, 2 Jan 2025 14:34:26 -0500 Subject: [PATCH 0026/1160] [Fiber] Avoid return value from commitBeforeMutationEffects (#31922) This is behind an unusual flag (enableCreateEventHandleAPI) that doesn't serve a special return value. I'll be collecting other flags from this phase too. We can just use the global flag and reset it before the next mutation phase. Unlike focusedInstanceHandle this doesn't leak any memory in the meantime. --- packages/react-reconciler/src/ReactFiberCommitWork.js | 9 +++------ packages/react-reconciler/src/ReactFiberWorkLoop.js | 6 ++---- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.js b/packages/react-reconciler/src/ReactFiberCommitWork.js index a244c65e3e886..80a27c450253b 100644 --- a/packages/react-reconciler/src/ReactFiberCommitWork.js +++ b/packages/react-reconciler/src/ReactFiberCommitWork.js @@ -236,23 +236,20 @@ let inProgressLanes: Lanes | null = null; let inProgressRoot: FiberRoot | null = null; let focusedInstanceHandle: null | Fiber = null; -let shouldFireAfterActiveInstanceBlur: boolean = false; +export let shouldFireAfterActiveInstanceBlur: boolean = false; export function commitBeforeMutationEffects( root: FiberRoot, firstChild: Fiber, -): boolean { +): void { focusedInstanceHandle = prepareForCommit(root.containerInfo); + shouldFireAfterActiveInstanceBlur = false; nextEffect = firstChild; commitBeforeMutationEffects_begin(); // We no longer need to track the active instance fiber - const shouldFire = shouldFireAfterActiveInstanceBlur; - shouldFireAfterActiveInstanceBlur = false; focusedInstanceHandle = null; - - return shouldFire; } function commitBeforeMutationEffects_begin() { diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.js b/packages/react-reconciler/src/ReactFiberWorkLoop.js index ded26ed6e2bfb..26544788f1508 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.js @@ -198,6 +198,7 @@ import { } from './ReactFiberThrow'; import { commitBeforeMutationEffects, + shouldFireAfterActiveInstanceBlur, commitLayoutEffects, commitMutationEffects, commitPassiveMountEffects, @@ -3384,10 +3385,7 @@ function commitRootImpl( // The first phase a "before mutation" phase. We use this phase to read the // state of the host tree right before we mutate it. This is where // getSnapshotBeforeUpdate is called. - const shouldFireAfterActiveInstanceBlur = commitBeforeMutationEffects( - root, - finishedWork, - ); + commitBeforeMutationEffects(root, finishedWork); // The next phase is the mutation phase, where we mutate the host tree. commitMutationEffects(root, finishedWork, lanes); From c81312e3a78dcbf71ed98c8893abe6dbfeaef3f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Thu, 2 Jan 2025 14:55:34 -0500 Subject: [PATCH 0027/1160] [Fiber] Refactor Commit Phase into Separate Functions for Before Mutation/Mutation/Layout (#31930) This is doing some general clean up to be able to split the commit root three phases into three separate async steps. --- .../react-reconciler/src/ReactFiberLane.js | 16 +- .../react-reconciler/src/ReactFiberRoot.js | 2 - .../src/ReactFiberRootScheduler.js | 11 + .../src/ReactFiberWorkLoop.js | 294 ++++++++++-------- .../src/ReactInternalTypes.js | 4 - .../src/__tests__/ReactDeferredValue-test.js | 1 + 6 files changed, 174 insertions(+), 154 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberLane.js b/packages/react-reconciler/src/ReactFiberLane.js index 4c96207bd6696..71a1e973c3af8 100644 --- a/packages/react-reconciler/src/ReactFiberLane.js +++ b/packages/react-reconciler/src/ReactFiberLane.js @@ -221,7 +221,11 @@ function getHighestPriorityLanes(lanes: Lanes | Lane): Lanes { } } -export function getNextLanes(root: FiberRoot, wipLanes: Lanes): Lanes { +export function getNextLanes( + root: FiberRoot, + wipLanes: Lanes, + rootHasPendingCommit: boolean, +): Lanes { // Early bailout if there's no pending work left. const pendingLanes = root.pendingLanes; if (pendingLanes === NoLanes) { @@ -246,16 +250,6 @@ export function getNextLanes(root: FiberRoot, wipLanes: Lanes): Lanes { // a brief amount of time (i.e. below the "Just Noticeable Difference" // threshold). // - // TODO: finishedLanes is also set when a Suspensey resource, like CSS or - // images, suspends during the commit phase. (We could detect that here by - // checking for root.cancelPendingCommit.) These are also expected to resolve - // quickly, because of preloading, but theoretically they could block forever - // like in a normal "suspend indefinitely" scenario. In the future, we should - // consider only blocking for up to some time limit before discarding the - // commit in favor of prerendering. If we do discard a pending commit, then - // the commit phase callback should act as a ping to try the original - // render again. - const rootHasPendingCommit = root.finishedLanes !== NoLanes; // Do not work on any idle work until all the non-idle work has finished, // even if the work is suspended. diff --git a/packages/react-reconciler/src/ReactFiberRoot.js b/packages/react-reconciler/src/ReactFiberRoot.js index 29bc7923d893e..4971bb4c2be34 100644 --- a/packages/react-reconciler/src/ReactFiberRoot.js +++ b/packages/react-reconciler/src/ReactFiberRoot.js @@ -61,7 +61,6 @@ function FiberRootNode( this.pendingChildren = null; this.current = null; this.pingCache = null; - this.finishedWork = null; this.timeoutHandle = noTimeout; this.cancelPendingCommit = null; this.context = null; @@ -76,7 +75,6 @@ function FiberRootNode( this.pingedLanes = NoLanes; this.warmLanes = NoLanes; this.expiredLanes = NoLanes; - this.finishedLanes = NoLanes; this.errorRecoveryDisabledLanes = NoLanes; this.shellSuspendCounter = 0; diff --git a/packages/react-reconciler/src/ReactFiberRootScheduler.js b/packages/react-reconciler/src/ReactFiberRootScheduler.js index e3791f4c8dcff..d57458fdbf41c 100644 --- a/packages/react-reconciler/src/ReactFiberRootScheduler.js +++ b/packages/react-reconciler/src/ReactFiberRootScheduler.js @@ -69,6 +69,7 @@ import { scheduleMicrotask, shouldAttemptEagerTransition, trackSchedulerEvent, + noTimeout, } from './ReactFiberConfig'; import ReactSharedInternals from 'shared/ReactSharedInternals'; @@ -207,11 +208,15 @@ function flushSyncWorkAcrossRoots_impl( const workInProgressRoot = getWorkInProgressRoot(); const workInProgressRootRenderLanes = getWorkInProgressRootRenderLanes(); + const rootHasPendingCommit = + root.cancelPendingCommit !== null || + root.timeoutHandle !== noTimeout; const nextLanes = getNextLanes( root, root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes, + rootHasPendingCommit, ); if ( includesSyncLane(nextLanes) && @@ -335,6 +340,8 @@ function scheduleTaskForRootDuringMicrotask( const pendingPassiveEffectsLanes = getPendingPassiveEffectsLanes(); const workInProgressRoot = getWorkInProgressRoot(); const workInProgressRootRenderLanes = getWorkInProgressRootRenderLanes(); + const rootHasPendingCommit = + root.cancelPendingCommit !== null || root.timeoutHandle !== noTimeout; const nextLanes = enableYieldingBeforePassive && root === rootWithPendingPassiveEffects ? // This will schedule the callback at the priority of the lane but we used to @@ -345,6 +352,7 @@ function scheduleTaskForRootDuringMicrotask( : getNextLanes( root, root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes, + rootHasPendingCommit, ); const existingCallbackNode = root.callbackNode; @@ -488,9 +496,12 @@ function performWorkOnRootViaSchedulerTask( // it's available). const workInProgressRoot = getWorkInProgressRoot(); const workInProgressRootRenderLanes = getWorkInProgressRootRenderLanes(); + const rootHasPendingCommit = + root.cancelPendingCommit !== null || root.timeoutHandle !== noTimeout; const lanes = getNextLanes( root, root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes, + rootHasPendingCommit, ); if (lanes === NoLanes) { // No more work on this root. diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.js b/packages/react-reconciler/src/ReactFiberWorkLoop.js index 26544788f1508..ce253f5436cea 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.js @@ -14,7 +14,6 @@ import type {Fiber, FiberRoot} from './ReactInternalTypes'; import type {Lanes, Lane} from './ReactFiberLane'; import type {SuspenseState} from './ReactFiberSuspenseComponent'; import type {FunctionComponentUpdateQueue} from './ReactFiberHooks'; -import type {EventPriority} from './ReactEventPriorities'; import type { PendingTransitionCallbacks, PendingBoundaries, @@ -1240,16 +1239,12 @@ function finishConcurrentRender( } } - // Only set these if we have a complete tree that is ready to be committed. - // We use these fields to determine later whether or not the work should be - // discarded for a fresh render attempt. - root.finishedWork = finishedWork; - root.finishedLanes = lanes; - if (shouldForceFlushFallbacksInDEV()) { // We're inside an `act` scope. Commit immediately. commitRoot( root, + finishedWork, + lanes, workInProgressRootRecoverableErrors, workInProgressTransitions, workInProgressRootDidIncludeRecursiveRenderUpdate, @@ -1282,7 +1277,7 @@ function finishConcurrentRender( didAttemptEntireTree, ); - const nextLanes = getNextLanes(root, NoLanes); + const nextLanes = getNextLanes(root, NoLanes, true); if (nextLanes !== NoLanes) { // There's additional work we can do on this root. We might as well // attempt to work on that while we're suspended. @@ -1352,6 +1347,8 @@ function commitRootWhenReady( completedRenderStartTime: number, // Profiling-only completedRenderEndTime: number, // Profiling-only ) { + root.timeoutHandle = noTimeout; + // TODO: Combine retry throttling with Suspensey commits. Right now they run // one after the other. const BothVisibilityAndMaySuspendCommit = Visibility | MaySuspendCommit; @@ -1385,6 +1382,8 @@ function commitRootWhenReady( commitRoot.bind( null, root, + finishedWork, + lanes, recoverableErrors, transitions, didIncludeRenderPhaseUpdate, @@ -1406,6 +1405,8 @@ function commitRootWhenReady( // Otherwise, commit immediately.; commitRoot( root, + finishedWork, + lanes, recoverableErrors, transitions, didIncludeRenderPhaseUpdate, @@ -1843,9 +1844,6 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber { } } - root.finishedWork = null; - root.finishedLanes = NoLanes; - const timeoutHandle = root.timeoutHandle; if (timeoutHandle !== noTimeout) { // The root previous suspended and scheduled a timeout to commit a fallback @@ -3129,6 +3127,8 @@ const THROTTLED_COMMIT = 2; function commitRoot( root: FiberRoot, + finishedWork: null | Fiber, + lanes: Lanes, recoverableErrors: null | Array>, transitions: Array | null, didIncludeRenderPhaseUpdate: boolean, @@ -3139,48 +3139,9 @@ function commitRoot( suspendedCommitReason: SuspendedCommitReason, // Profiling-only completedRenderStartTime: number, // Profiling-only completedRenderEndTime: number, // Profiling-only -) { - // TODO: This no longer makes any sense. We already wrap the mutation and - // layout phases. Should be able to remove. - const prevTransition = ReactSharedInternals.T; - const previousUpdateLanePriority = getCurrentUpdatePriority(); - try { - setCurrentUpdatePriority(DiscreteEventPriority); - ReactSharedInternals.T = null; - commitRootImpl( - root, - recoverableErrors, - transitions, - didIncludeRenderPhaseUpdate, - previousUpdateLanePriority, - spawnedLane, - updatedLanes, - suspendedRetryLanes, - exitStatus, - suspendedCommitReason, - completedRenderStartTime, - completedRenderEndTime, - ); - } finally { - ReactSharedInternals.T = prevTransition; - setCurrentUpdatePriority(previousUpdateLanePriority); - } -} +): void { + root.cancelPendingCommit = null; -function commitRootImpl( - root: FiberRoot, - recoverableErrors: null | Array>, - transitions: Array | null, - didIncludeRenderPhaseUpdate: boolean, - renderPriorityLevel: EventPriority, - spawnedLane: Lane, - updatedLanes: Lanes, - suspendedRetryLanes: Lanes, - exitStatus: RootExitStatus, // Profiling-only - suspendedCommitReason: SuspendedCommitReason, // Profiling-only - completedRenderStartTime: number, // Profiling-only - completedRenderEndTime: number, // Profiling-only -) { do { // `flushPassiveEffects` will call `flushSyncUpdateQueue` at the end, which // means `flushPassiveEffects` will sometimes result in additional @@ -3196,9 +3157,6 @@ function commitRootImpl( throw new Error('Should not already be working.'); } - const finishedWork = root.finishedWork; - const lanes = root.finishedLanes; - if (enableProfilerTimer && enableComponentPerformanceTrack) { // Log the previous render phase once we commit. I.e. we weren't interrupted. setCurrentTrackFromLanes(lanes); @@ -3234,19 +3192,17 @@ function commitRootImpl( if (enableSchedulingProfiler) { markCommitStopped(); } - return null; + return; } else { if (__DEV__) { if (lanes === NoLanes) { console.error( - 'root.finishedLanes should not be empty during a commit. This is a ' + + 'finishedLanes should not be empty during a commit. This is a ' + 'bug in React.', ); } } } - root.finishedWork = null; - root.finishedLanes = NoLanes; if (finishedWork === root.current) { throw new Error( @@ -3292,7 +3248,6 @@ function commitRootImpl( // might get scheduled in the commit phase. (See #16714.) // TODO: Delete all other places that schedule the passive effect callback // They're redundant. - let rootDoesHavePassiveEffects: boolean = false; if ( // If this subtree rendered with profiling this commit, we need to visit it to log it. (enableProfilerTimer && @@ -3301,7 +3256,6 @@ function commitRootImpl( (finishedWork.subtreeFlags & PassiveMask) !== NoFlags || (finishedWork.flags & PassiveMask) !== NoFlags ) { - rootDoesHavePassiveEffects = true; pendingPassiveEffectsRemainingLanes = remainingLanes; pendingPassiveEffectsRenderEndTime = completedRenderEndTime; // workInProgressTransitions might be overwritten, so we want @@ -3319,7 +3273,6 @@ function commitRootImpl( // So we can clear these now to allow a new callback to be scheduled. root.callbackNode = null; root.callbackPriority = NoLane; - root.cancelPendingCommit = null; scheduleCallback(NormalSchedulerPriority, () => { if (enableProfilerTimer && enableComponentPerformanceTrack) { // Track the currently executing event if there is one so we can ignore this @@ -3338,7 +3291,6 @@ function commitRootImpl( // so we can clear the callback now. root.callbackNode = null; root.callbackPriority = NoLane; - root.cancelPendingCommit = null; } if (enableProfilerTimer) { @@ -3355,79 +3307,136 @@ function commitRootImpl( } } + // The commit phase is broken into several sub-phases. We do a separate pass + // of the effect list for each phase: all mutation effects come before all + // layout effects, and so on. + // Check if there are any effects in the whole tree. // TODO: This is left over from the effect list implementation, where we had // to check for the existence of `firstEffect` to satisfy Flow. I think the // only other reason this optimization exists is because it affects profiling. // Reconsider whether this is necessary. - const subtreeHasEffects = - (finishedWork.subtreeFlags & - (BeforeMutationMask | MutationMask | LayoutMask | PassiveMask)) !== - NoFlags; - const rootHasEffect = - (finishedWork.flags & - (BeforeMutationMask | MutationMask | LayoutMask | PassiveMask)) !== + const subtreeHasBeforeMutationEffects = + (finishedWork.subtreeFlags & (BeforeMutationMask | MutationMask)) !== NoFlags; + const rootHasBeforeMutationEffect = + (finishedWork.flags & (BeforeMutationMask | MutationMask)) !== NoFlags; - if (subtreeHasEffects || rootHasEffect) { + if (subtreeHasBeforeMutationEffects || rootHasBeforeMutationEffect) { const prevTransition = ReactSharedInternals.T; ReactSharedInternals.T = null; const previousPriority = getCurrentUpdatePriority(); setCurrentUpdatePriority(DiscreteEventPriority); - const prevExecutionContext = executionContext; executionContext |= CommitContext; + try { + // The first phase a "before mutation" phase. We use this phase to read the + // state of the host tree right before we mutate it. This is where + // getSnapshotBeforeUpdate is called. + commitBeforeMutationEffects(root, finishedWork); + } finally { + // Reset the priority to the previous non-sync value. + executionContext = prevExecutionContext; + setCurrentUpdatePriority(previousPriority); + ReactSharedInternals.T = prevTransition; + } + } + flushMutationEffects(root, finishedWork, lanes); + flushLayoutEffects( + root, + finishedWork, + lanes, + recoverableErrors, + didIncludeRenderPhaseUpdate, + suspendedCommitReason, + completedRenderEndTime, + ); +} - // The commit phase is broken into several sub-phases. We do a separate pass - // of the effect list for each phase: all mutation effects come before all - // layout effects, and so on. - - // The first phase a "before mutation" phase. We use this phase to read the - // state of the host tree right before we mutate it. This is where - // getSnapshotBeforeUpdate is called. - commitBeforeMutationEffects(root, finishedWork); +function flushMutationEffects( + root: FiberRoot, + finishedWork: Fiber, + lanes: Lanes, +): void { + const subtreeMutationHasEffects = + (finishedWork.subtreeFlags & MutationMask) !== NoFlags; + const rootMutationHasEffect = (finishedWork.flags & MutationMask) !== NoFlags; - // The next phase is the mutation phase, where we mutate the host tree. - commitMutationEffects(root, finishedWork, lanes); + if (subtreeMutationHasEffects || rootMutationHasEffect) { + const prevTransition = ReactSharedInternals.T; + ReactSharedInternals.T = null; + const previousPriority = getCurrentUpdatePriority(); + setCurrentUpdatePriority(DiscreteEventPriority); + const prevExecutionContext = executionContext; + executionContext |= CommitContext; + try { + // The next phase is the mutation phase, where we mutate the host tree. + commitMutationEffects(root, finishedWork, lanes); - if (enableCreateEventHandleAPI) { - if (shouldFireAfterActiveInstanceBlur) { - afterActiveInstanceBlur(); + if (enableCreateEventHandleAPI) { + if (shouldFireAfterActiveInstanceBlur) { + afterActiveInstanceBlur(); + } } + resetAfterCommit(root.containerInfo); + } finally { + // Reset the priority to the previous non-sync value. + executionContext = prevExecutionContext; + setCurrentUpdatePriority(previousPriority); + ReactSharedInternals.T = prevTransition; } - resetAfterCommit(root.containerInfo); - - // The work-in-progress tree is now the current tree. This must come after - // the mutation phase, so that the previous tree is still current during - // componentWillUnmount, but before the layout phase, so that the finished - // work is current during componentDidMount/Update. - root.current = finishedWork; - - // The next phase is the layout phase, where we call effects that read - // the host tree after it's been mutated. The idiomatic use case for this is - // layout, but class component lifecycles also fire here for legacy reasons. - if (enableSchedulingProfiler) { - markLayoutEffectsStarted(lanes); - } - commitLayoutEffects(finishedWork, root, lanes); - if (enableSchedulingProfiler) { - markLayoutEffectsStopped(); - } + } - // Tell Scheduler to yield at the end of the frame, so the browser has an - // opportunity to paint. - requestPaint(); + // The work-in-progress tree is now the current tree. This must come after + // the mutation phase, so that the previous tree is still current during + // componentWillUnmount, but before the layout phase, so that the finished + // work is current during componentDidMount/Update. + root.current = finishedWork; +} - executionContext = prevExecutionContext; +function flushLayoutEffects( + root: FiberRoot, + finishedWork: Fiber, + lanes: Lanes, + recoverableErrors: null | Array>, + didIncludeRenderPhaseUpdate: boolean, + suspendedCommitReason: SuspendedCommitReason, // Profiling-only + completedRenderEndTime: number, // Profiling-only +): void { + const subtreeHasLayoutEffects = + (finishedWork.subtreeFlags & LayoutMask) !== NoFlags; + const rootHasLayoutEffect = (finishedWork.flags & LayoutMask) !== NoFlags; - // Reset the priority to the previous non-sync value. - setCurrentUpdatePriority(previousPriority); - ReactSharedInternals.T = prevTransition; - } else { - // No effects. - root.current = finishedWork; + if (subtreeHasLayoutEffects || rootHasLayoutEffect) { + const prevTransition = ReactSharedInternals.T; + ReactSharedInternals.T = null; + const previousPriority = getCurrentUpdatePriority(); + setCurrentUpdatePriority(DiscreteEventPriority); + const prevExecutionContext = executionContext; + executionContext |= CommitContext; + try { + // The next phase is the layout phase, where we call effects that read + // the host tree after it's been mutated. The idiomatic use case for this is + // layout, but class component lifecycles also fire here for legacy reasons. + if (enableSchedulingProfiler) { + markLayoutEffectsStarted(lanes); + } + commitLayoutEffects(finishedWork, root, lanes); + if (enableSchedulingProfiler) { + markLayoutEffectsStopped(); + } + } finally { + // Reset the priority to the previous non-sync value. + executionContext = prevExecutionContext; + setCurrentUpdatePriority(previousPriority); + ReactSharedInternals.T = prevTransition; + } } + // Tell Scheduler to yield at the end of the frame, so the browser has an + // opportunity to paint. + requestPaint(); + if (enableProfilerTimer && enableComponentPerformanceTrack) { recordCommitEndTime(); logCommitPhase( @@ -3439,18 +3448,22 @@ function commitRootImpl( ); } - const rootDidHavePassiveEffects = rootDoesHavePassiveEffects; + const rootDidHavePassiveEffects = // If this subtree rendered with profiling this commit, we need to visit it to log it. + (enableProfilerTimer && + enableComponentPerformanceTrack && + finishedWork.actualDuration !== 0) || + (finishedWork.subtreeFlags & PassiveMask) !== NoFlags || + (finishedWork.flags & PassiveMask) !== NoFlags; - if (rootDoesHavePassiveEffects) { + if (rootDidHavePassiveEffects) { // This commit has passive effects. Stash a reference to them. But don't // schedule a callback until after flushing layout work. - rootDoesHavePassiveEffects = false; rootWithPendingPassiveEffects = root; pendingPassiveEffectsLanes = lanes; } else { // There were no passive effects, so we can immediately release the cache // pool for this render. - releaseRootPooledCache(root, remainingLanes); + releaseRootPooledCache(root, root.pendingLanes); if (__DEV__) { nestedPassiveUpdateCount = 0; rootWithPassiveNestedUpdates = null; @@ -3458,7 +3471,7 @@ function commitRootImpl( } // Read this again, since an effect might have updated it - remainingLanes = root.pendingLanes; + let remainingLanes = root.pendingLanes; // Check if there's remaining work on this root // TODO: This is part of the `componentDidCatch` implementation. Its purpose @@ -3482,7 +3495,8 @@ function commitRootImpl( } } - onCommitRootDevTools(finishedWork.stateNode, renderPriorityLevel); + const renderPriority = lanesToEventPriority(lanes); + onCommitRootDevTools(finishedWork.stateNode, renderPriority); if (enableUpdaterTracking) { if (isDevToolsPresent) { @@ -3495,22 +3509,31 @@ function commitRootImpl( } if (recoverableErrors !== null) { - // There were errors during this render, but recovered from them without - // needing to surface it to the UI. We log them here. - const onRecoverableError = root.onRecoverableError; - for (let i = 0; i < recoverableErrors.length; i++) { - const recoverableError = recoverableErrors[i]; - const errorInfo = makeErrorInfo(recoverableError.stack); - if (__DEV__) { - runWithFiberInDEV( - recoverableError.source, - onRecoverableError, - recoverableError.value, - errorInfo, - ); - } else { - onRecoverableError(recoverableError.value, errorInfo); + const prevTransition = ReactSharedInternals.T; + const previousUpdateLanePriority = getCurrentUpdatePriority(); + setCurrentUpdatePriority(DiscreteEventPriority); + ReactSharedInternals.T = null; + try { + // There were errors during this render, but recovered from them without + // needing to surface it to the UI. We log them here. + const onRecoverableError = root.onRecoverableError; + for (let i = 0; i < recoverableErrors.length; i++) { + const recoverableError = recoverableErrors[i]; + const errorInfo = makeErrorInfo(recoverableError.stack); + if (__DEV__) { + runWithFiberInDEV( + recoverableError.source, + onRecoverableError, + recoverableError.value, + errorInfo, + ); + } else { + onRecoverableError(recoverableError.value, errorInfo); + } } + } finally { + ReactSharedInternals.T = prevTransition; + setCurrentUpdatePriority(previousUpdateLanePriority); } } @@ -3610,8 +3633,6 @@ function commitRootImpl( }); } } - - return null; } function makeErrorInfo(componentStack: ?string) { @@ -3705,7 +3726,6 @@ function flushPassiveEffectsImpl(wasDelayedCommit: void | boolean) { // We've finished our work for this render pass. root.callbackNode = null; root.callbackPriority = NoLane; - root.cancelPendingCommit = null; } if ((executionContext & (RenderContext | CommitContext)) !== NoContext) { diff --git a/packages/react-reconciler/src/ReactInternalTypes.js b/packages/react-reconciler/src/ReactInternalTypes.js index afe853202f141..859da37cadf32 100644 --- a/packages/react-reconciler/src/ReactInternalTypes.js +++ b/packages/react-reconciler/src/ReactInternalTypes.js @@ -220,8 +220,6 @@ type BaseFiberRootProperties = { pingCache: WeakMap> | Map> | null, - // A finished work-in-progress HostRoot that's ready to be committed. - finishedWork: Fiber | null, // Timeout handle returned by setTimeout. Used to cancel a pending timeout, if // it's superseded by a new one. timeoutHandle: TimeoutHandle | NoTimeout, @@ -252,8 +250,6 @@ type BaseFiberRootProperties = { errorRecoveryDisabledLanes: Lanes, shellSuspendCounter: number, - finishedLanes: Lanes, - entangledLanes: Lanes, entanglements: LaneMap, diff --git a/packages/react-reconciler/src/__tests__/ReactDeferredValue-test.js b/packages/react-reconciler/src/__tests__/ReactDeferredValue-test.js index bbd01cd551e86..f57fc0ac0239a 100644 --- a/packages/react-reconciler/src/__tests__/ReactDeferredValue-test.js +++ b/packages/react-reconciler/src/__tests__/ReactDeferredValue-test.js @@ -744,6 +744,7 @@ describe('ReactDeferredValue', () => { , ); // We should switch to pre-rendering the new preview. + await waitForPaint([]); await waitForPaint(['Preview [B]']); expect(root).toMatchRenderedOutput(); From a7c898d83a991c48f3981fcc65d969f1d90d80a1 Mon Sep 17 00:00:00 2001 From: Ricky Date: Thu, 2 Jan 2025 15:28:06 -0500 Subject: [PATCH 0028/1160] [assert helpers] react-dom (pt 1) (#31897) Converts ~half of react-dom tests --- .../__tests__/CSSPropertyOperations-test.js | 62 +- .../__tests__/DOMPropertyOperations-test.js | 21 +- .../__tests__/InvalidEventListeners-test.js | 17 +- .../__tests__/ReactChildReconciler-test.js | 90 +-- .../src/__tests__/ReactComponent-test.js | 83 ++- .../__tests__/ReactComponentLifeCycle-test.js | 605 +++++++++++------- .../__tests__/ReactCompositeComponent-test.js | 214 ++++--- .../ReactCompositeComponentState-test.js | 48 +- .../react-dom/src/__tests__/ReactDOM-test.js | 115 ++-- .../src/__tests__/ReactDOMAttribute-test.js | 69 +- .../ReactDeprecationWarnings-test.js | 18 +- .../src/__tests__/findDOMNodeFB-test.js | 10 +- .../ReactDOMServerIntegrationTestUtils.js | 2 +- 13 files changed, 781 insertions(+), 573 deletions(-) diff --git a/packages/react-dom/src/__tests__/CSSPropertyOperations-test.js b/packages/react-dom/src/__tests__/CSSPropertyOperations-test.js index bda95b8434d46..b5671cf759cef 100644 --- a/packages/react-dom/src/__tests__/CSSPropertyOperations-test.js +++ b/packages/react-dom/src/__tests__/CSSPropertyOperations-test.js @@ -13,6 +13,8 @@ const React = require('react'); const ReactDOMClient = require('react-dom/client'); const ReactDOMServer = require('react-dom/server'); const act = require('internal-test-utils').act; +const assertConsoleErrorDev = + require('internal-test-utils').assertConsoleErrorDev; describe('CSSPropertyOperations', () => { it('should automatically append `px` to relevant styles', () => { @@ -103,15 +105,14 @@ describe('CSSPropertyOperations', () => { const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); - await expect(async () => { - await act(() => { - root.render(); - }); - }).toErrorDev( + await act(() => { + root.render(); + }); + assertConsoleErrorDev([ 'Unsupported style property background-color. Did you mean backgroundColor?' + '\n in div (at **)' + '\n in Comp (at **)', - ); + ]); }); it('should warn when updating hyphenated style names', async () => { @@ -132,11 +133,10 @@ describe('CSSPropertyOperations', () => { await act(() => { root.render(); }); - await expect(async () => { - await act(() => { - root.render(); - }); - }).toErrorDev([ + await act(() => { + root.render(); + }); + assertConsoleErrorDev([ 'Unsupported style property -ms-transform. Did you mean msTransform?' + '\n in div (at **)' + '\n in Comp (at **)', @@ -165,11 +165,10 @@ describe('CSSPropertyOperations', () => { const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); - await expect(async () => { - await act(() => { - root.render(); - }); - }).toErrorDev([ + await act(() => { + root.render(); + }); + assertConsoleErrorDev([ // msTransform is correct already and shouldn't warn 'Unsupported vendor-prefixed style property oTransform. ' + 'Did you mean OTransform?' + @@ -202,11 +201,10 @@ describe('CSSPropertyOperations', () => { const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); - await expect(async () => { - await act(() => { - root.render(); - }); - }).toErrorDev([ + await act(() => { + root.render(); + }); + assertConsoleErrorDev([ "Style property values shouldn't contain a semicolon. " + 'Try "backgroundColor: blue" instead.' + '\n in div (at **)' + @@ -229,15 +227,14 @@ describe('CSSPropertyOperations', () => { const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); - await expect(async () => { - await act(() => { - root.render(); - }); - }).toErrorDev( + await act(() => { + root.render(); + }); + assertConsoleErrorDev([ '`NaN` is an invalid value for the `fontSize` css style property.' + '\n in div (at **)' + '\n in Comp (at **)', - ); + ]); }); it('should not warn when setting CSS custom properties', async () => { @@ -265,15 +262,14 @@ describe('CSSPropertyOperations', () => { const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); - await expect(async () => { - await act(() => { - root.render(); - }); - }).toErrorDev( + await act(() => { + root.render(); + }); + assertConsoleErrorDev([ '`Infinity` is an invalid value for the `fontSize` css style property.' + '\n in div (at **)' + '\n in Comp (at **)', - ); + ]); }); it('should not add units to CSS custom properties', async () => { diff --git a/packages/react-dom/src/__tests__/DOMPropertyOperations-test.js b/packages/react-dom/src/__tests__/DOMPropertyOperations-test.js index cecc71b45a6c0..ef09e49bf36c1 100644 --- a/packages/react-dom/src/__tests__/DOMPropertyOperations-test.js +++ b/packages/react-dom/src/__tests__/DOMPropertyOperations-test.js @@ -1333,7 +1333,8 @@ describe('DOMPropertyOperations', () => { }); assertConsoleErrorDev([ - 'The `popoverTarget` prop expects the ID of an Element as a string. Received HTMLDivElement {} instead.', + 'The `popoverTarget` prop expects the ID of an Element as a string. Received HTMLDivElement {} instead.\n' + + ' in button (at **)', ]); // Dedupe warning @@ -1375,13 +1376,17 @@ describe('DOMPropertyOperations', () => { expect(container.firstChild.getAttribute('value')).toBe('foo'); } expect(container.firstChild.value).toBe('foo'); - await expect(async () => { - await act(() => { - root.render(); - }); - }).toErrorDev( - 'A component is changing a controlled input to be uncontrolled', - ); + await act(() => { + root.render(); + }); + assertConsoleErrorDev([ + 'A component is changing a controlled input to be uncontrolled. ' + + 'This is likely caused by the value changing from a defined to undefined, ' + + 'which should not happen. Decide between using a controlled or uncontrolled ' + + 'input element for the lifetime of the component. ' + + 'More info: https://react.dev/link/controlled-components\n' + + ' in input (at **)', + ]); if (disableInputAttributeSyncing) { expect(container.firstChild.hasAttribute('value')).toBe(false); } else { diff --git a/packages/react-dom/src/__tests__/InvalidEventListeners-test.js b/packages/react-dom/src/__tests__/InvalidEventListeners-test.js index d7045d2756ad4..8bff125c82c32 100644 --- a/packages/react-dom/src/__tests__/InvalidEventListeners-test.js +++ b/packages/react-dom/src/__tests__/InvalidEventListeners-test.js @@ -15,13 +15,14 @@ describe('InvalidEventListeners', () => { let React; let ReactDOMClient; let act; + let assertConsoleErrorDev; let container; beforeEach(() => { jest.resetModules(); React = require('react'); ReactDOMClient = require('react-dom/client'); - act = require('internal-test-utils').act; + ({act, assertConsoleErrorDev} = require('internal-test-utils')); container = document.createElement('div'); document.body.appendChild(container); @@ -34,13 +35,13 @@ describe('InvalidEventListeners', () => { it('should prevent non-function listeners, at dispatch', async () => { const root = ReactDOMClient.createRoot(container); - await expect(async () => { - await act(() => { - root.render(
); - }); - }).toErrorDev( - 'Expected `onClick` listener to be a function, instead got a value of `string` type.', - ); + await act(() => { + root.render(
); + }); + assertConsoleErrorDev([ + 'Expected `onClick` listener to be a function, instead got a value of `string` type.\n' + + ' in div (at **)', + ]); const node = container.firstChild; console.error = jest.fn(); diff --git a/packages/react-dom/src/__tests__/ReactChildReconciler-test.js b/packages/react-dom/src/__tests__/ReactChildReconciler-test.js index 1c1475534f3e0..ba0c028fa863a 100644 --- a/packages/react-dom/src/__tests__/ReactChildReconciler-test.js +++ b/packages/react-dom/src/__tests__/ReactChildReconciler-test.js @@ -15,6 +15,7 @@ let React; let ReactDOMClient; let act; +let assertConsoleErrorDev; describe('ReactChildReconciler', () => { beforeEach(() => { @@ -22,7 +23,7 @@ describe('ReactChildReconciler', () => { React = require('react'); ReactDOMClient = require('react-dom/client'); - act = require('internal-test-utils').act; + ({act, assertConsoleErrorDev} = require('internal-test-utils')); }); function createIterable(array) { @@ -62,15 +63,21 @@ describe('ReactChildReconciler', () => { const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); - await expect(async () => { - await act(() => { - root.render( -
-

{iterableFunction}

-
, - ); - }); - }).toErrorDev('Functions are not valid as a React child'); + await act(() => { + root.render( +
+

{iterableFunction}

+
, + ); + }); + assertConsoleErrorDev([ + 'Functions are not valid as a React child. ' + + 'This may happen if you return fn instead of from render. ' + + 'Or maybe you meant to call this function rather than return it.\n' + + '

{fn}

\n' + + ' in h1 (at **)' + + (gate('enableOwnerStacks') ? '' : '\n in div (at **)'), + ]); const node = container.firstChild; expect(node.innerHTML).toContain(''); // h1 @@ -85,16 +92,18 @@ describe('ReactChildReconciler', () => { const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); - await expect(async () => { - await act(() => { - root.render(); - }); - }).toErrorDev( - 'Keys should be unique so that components maintain their identity ' + - 'across updates. Non-unique keys may cause children to be ' + - 'duplicated and/or omitted — the behavior is unsupported and ' + - 'could change in a future version.', - ); + await act(() => { + root.render(); + }); + assertConsoleErrorDev([ + 'Encountered two children with the same key, `1`. ' + + 'Keys should be unique so that components maintain their identity across updates. ' + + 'Non-unique keys may cause children to be duplicated and/or omitted — ' + + 'the behavior is unsupported and could change in a future version.\n' + + (gate('enableOwnerStacks') ? '' : ' in div (at **)\n') + + ' in div (at **)\n' + + ' in Component (at **)', + ]); }); it('warns for duplicated array keys with component stack info', async () => { @@ -118,11 +127,10 @@ describe('ReactChildReconciler', () => { const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); - await expect(async () => { - await act(() => { - root.render(); - }); - }).toErrorDev( + await act(() => { + root.render(); + }); + assertConsoleErrorDev([ 'Encountered two children with the same key, `1`. ' + 'Keys should be unique so that components maintain their identity ' + 'across updates. Non-unique keys may cause children to be ' + @@ -135,7 +143,7 @@ describe('ReactChildReconciler', () => { ? '' : ' in Parent (at **)\n') + ' in GrandParent (at **)', - ); + ]); }); it('warns for duplicated iterable keys', async () => { @@ -147,16 +155,19 @@ describe('ReactChildReconciler', () => { const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); - await expect(async () => { - await act(() => { - root.render(); - }); - }).toErrorDev( - 'Keys should be unique so that components maintain their identity ' + + await act(() => { + root.render(); + }); + assertConsoleErrorDev([ + 'Encountered two children with the same key, `1`. ' + + 'Keys should be unique so that components maintain their identity ' + 'across updates. Non-unique keys may cause children to be ' + 'duplicated and/or omitted — the behavior is unsupported and ' + - 'could change in a future version.', - ); + 'could change in a future version.\n' + + ' in div (at **)\n' + + (gate(flags => flags.enableOwnerStacks) ? '' : ' in div (at **)\n') + + ' in Component (at **)', + ]); }); it('warns for duplicated iterable keys with component stack info', async () => { @@ -180,11 +191,10 @@ describe('ReactChildReconciler', () => { const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); - await expect(async () => { - await act(() => { - root.render(); - }); - }).toErrorDev( + await act(() => { + root.render(); + }); + assertConsoleErrorDev([ 'Encountered two children with the same key, `1`. ' + 'Keys should be unique so that components maintain their identity ' + 'across updates. Non-unique keys may cause children to be ' + @@ -197,6 +207,6 @@ describe('ReactChildReconciler', () => { ? '' : ' in Parent (at **)\n') + ' in GrandParent (at **)', - ); + ]); }); }); diff --git a/packages/react-dom/src/__tests__/ReactComponent-test.js b/packages/react-dom/src/__tests__/ReactComponent-test.js index 6459114697540..c59ba61e01c44 100644 --- a/packages/react-dom/src/__tests__/ReactComponent-test.js +++ b/packages/react-dom/src/__tests__/ReactComponent-test.js @@ -411,7 +411,9 @@ describe('ReactComponent', () => { assertConsoleErrorDev( [ 'React.jsx: type is invalid -- expected a string (for built-in components) ' + - 'or a class/function (for composite components) but got: undefined.', + '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}, ); @@ -491,16 +493,11 @@ describe('ReactComponent', () => { const container = document.createElement('div'); const 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( + await expect(async () => { + await act(() => { + root.render(); + }); + }).rejects.toThrowError( 'Element type is invalid: expected a string (for built-in components) ' + 'or a class/function (for composite components) but got: undefined.' + (__DEV__ @@ -509,6 +506,26 @@ describe('ReactComponent', () => { '\n\nCheck the render method of `Bar`.' : ''), ); + if (!gate('enableOwnerStacks')) { + assertConsoleErrorDev([ + 'React.jsx: type is invalid -- expected a string (for built-in components) ' + + 'or a class/function (for composite components) but got: undefined.' + + (__DEV__ + ? " You likely forgot to export your component from the file it's defined in, " + + 'or you might have mixed up default and named imports.\n' + + ' in Bar (at **)\n' + + ' in Foo (at **)' + : ''), + 'React.jsx: type is invalid -- expected a string (for built-in components) ' + + 'or a class/function (for composite components) but got: undefined.' + + (__DEV__ + ? " You likely forgot to export your component from the file it's defined in, " + + 'or you might have mixed up default and named imports.\n' + + ' in Bar (at **)\n' + + ' in Foo (at **)' + : ''), + ]); + } }); it('throws if a plain object is used as a child', async () => { @@ -624,17 +641,16 @@ describe('ReactComponent', () => { } const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); - await expect(async () => { - await act(() => { - root.render(); - }); - }).toErrorDev( + await act(() => { + root.render(); + }); + assertConsoleErrorDev([ 'Functions are not valid as a React child. This may happen if ' + 'you return Foo instead of from render. ' + 'Or maybe you meant to call this function rather than return it.\n' + ' {Foo}\n' + ' in Foo (at **)', - ); + ]); }); it('warns on function as a return value from a class', async () => { @@ -644,19 +660,18 @@ describe('ReactComponent', () => { } } const container = document.createElement('div'); - await expect(async () => { - const root = ReactDOMClient.createRoot(container); + const root = ReactDOMClient.createRoot(container); - await act(() => { - root.render(); - }); - }).toErrorDev( + await act(() => { + root.render(); + }); + assertConsoleErrorDev([ 'Functions are not valid as a React child. This may happen if ' + 'you return Foo instead of from render. ' + 'Or maybe you meant to call this function rather than return it.\n' + ' {Foo}\n' + ' in Foo (at **)', - ); + ]); }); it('warns on function as a child to host component', async () => { @@ -669,11 +684,10 @@ describe('ReactComponent', () => { } const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); - await expect(async () => { - await act(() => { - root.render(); - }); - }).toErrorDev( + await act(() => { + root.render(); + }); + assertConsoleErrorDev([ 'Functions are not valid as a React child. This may happen if ' + 'you return Foo instead of from render. ' + 'Or maybe you meant to call this function rather than return it.\n' + @@ -683,7 +697,7 @@ describe('ReactComponent', () => { ? '' : ' in div (at **)\n') + ' in Foo (at **)', - ); + ]); }); it('does not warn for function-as-a-child that gets resolved', async () => { @@ -724,11 +738,10 @@ describe('ReactComponent', () => { const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); let component; - await expect(async () => { - await act(() => { - root.render( (component = current)} />); - }); - }).toErrorDev([ + await act(() => { + root.render( (component = current)} />); + }); + assertConsoleErrorDev([ 'Functions are not valid as a React child. This may happen if ' + 'you return Foo instead of from render. ' + 'Or maybe you meant to call this function rather than return it.\n' + diff --git a/packages/react-dom/src/__tests__/ReactComponentLifeCycle-test.js b/packages/react-dom/src/__tests__/ReactComponentLifeCycle-test.js index 428dac1f1ea9e..75e4cebe80610 100644 --- a/packages/react-dom/src/__tests__/ReactComponentLifeCycle-test.js +++ b/packages/react-dom/src/__tests__/ReactComponentLifeCycle-test.js @@ -14,6 +14,8 @@ let act; let React; let ReactDOM; let ReactDOMClient; +let assertConsoleErrorDev; +let assertConsoleWarnDev; const clone = function (o) { return JSON.parse(JSON.stringify(o)); @@ -90,7 +92,11 @@ describe('ReactComponentLifeCycle', () => { beforeEach(() => { jest.resetModules(); - act = require('internal-test-utils').act; + ({ + act, + assertConsoleErrorDev, + assertConsoleWarnDev, + } = require('internal-test-utils')); React = require('react'); ReactDOM = require('react-dom'); @@ -239,15 +245,15 @@ describe('ReactComponentLifeCycle', () => { const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); - await expect(async () => { - await act(() => { - root.render(); - }); - }).toErrorDev( + await act(() => { + root.render(); + }); + assertConsoleErrorDev([ 'StatefulComponent: It is not recommended to assign props directly to state ' + "because updates to props won't be reflected in state. " + - 'In most cases, it is better to use props directly.', - ); + 'In most cases, it is better to use props directly.\n' + + ' in StatefulComponent (at **)', + ]); }); it('should not allow update state inside of getInitialState', async () => { @@ -266,16 +272,16 @@ describe('ReactComponentLifeCycle', () => { let container = document.createElement('div'); let root = ReactDOMClient.createRoot(container); - await expect(async () => { - await act(() => { - root.render(); - }); - }).toErrorDev( + await act(() => { + root.render(); + }); + assertConsoleErrorDev([ "Can't call setState on a component that is not yet mounted. " + 'This is a no-op, but it might indicate a bug in your application. ' + 'Instead, assign to `this.state` directly or define a `state = {};` ' + - 'class property with the desired state in the StatefulComponent component.', - ); + 'class property with the desired state in the StatefulComponent component.\n' + + ' in StatefulComponent (at **)', + ]); container = document.createElement('div'); root = ReactDOMClient.createRoot(container); @@ -308,11 +314,17 @@ describe('ReactComponentLifeCycle', () => { const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); - await expect(async () => { - await act(() => { - root.render(element); - }); - }).toErrorDev('Component is accessing isMounted inside its render()'); + await act(() => { + root.render(element); + }); + assertConsoleErrorDev([ + 'Component is accessing isMounted inside its render() function. ' + + 'render() should be a pure function of props and state. ' + + 'It should never access something that requires stale data ' + + 'from the previous render, such as refs. ' + + 'Move this logic to componentDidMount and componentDidUpdate instead.\n' + + ' in Component (at **)', + ]); expect(instance._isMounted()).toBeTruthy(); }); @@ -340,11 +352,17 @@ describe('ReactComponentLifeCycle', () => { const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); - await expect(async () => { - await act(() => { - root.render(element); - }); - }).toErrorDev('Component is accessing isMounted inside its render()'); + await act(() => { + root.render(element); + }); + assertConsoleErrorDev([ + 'Component is accessing isMounted inside its render() function. ' + + 'render() should be a pure function of props and state. ' + + 'It should never access something that requires stale data ' + + 'from the previous render, such as refs. ' + + 'Move this logic to componentDidMount and componentDidUpdate instead.\n' + + ' in Component (at **)', + ]); expect(instance._isMounted()).toBeTruthy(); }); @@ -390,11 +408,17 @@ describe('ReactComponentLifeCycle', () => { const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); - await expect(async () => { - await act(() => { - root.render(); - }); - }).toErrorDev('Component is accessing findDOMNode inside its render()'); + await act(() => { + root.render(); + }); + assertConsoleErrorDev([ + 'Component is accessing findDOMNode inside its render(). ' + + 'render() should be a pure function of props and state. ' + + 'It should never access something that requires stale data ' + + 'from the previous render, such as refs. ' + + 'Move this logic to componentDidMount and componentDidUpdate instead.\n' + + ' in Component (at **)', + ]); }); it('should carry through each of the phases of setup', async () => { @@ -453,13 +477,17 @@ describe('ReactComponentLifeCycle', () => { const root = ReactDOMClient.createRoot(document.createElement('div')); const instanceRef = React.createRef(); - await expect(async () => { - await act(() => { - root.render(); - }); - }).toErrorDev( - 'LifeCycleComponent is accessing isMounted inside its render() function', - ); + await act(() => { + root.render(); + }); + assertConsoleErrorDev([ + 'LifeCycleComponent is accessing isMounted inside its render() function. ' + + 'render() should be a pure function of props and state. ' + + 'It should never access something that requires stale data ' + + 'from the previous render, such as refs. ' + + 'Move this logic to componentDidMount and componentDidUpdate instead.\n' + + ' in LifeCycleComponent (at **)', + ]); const instance = instanceRef.current; // getInitialState @@ -781,19 +809,45 @@ describe('ReactComponentLifeCycle', () => { } const root = ReactDOMClient.createRoot(document.createElement('div')); - await expect(async () => { - await expect(async () => { - await act(() => { - root.render(); - }); - }).toErrorDev( - 'Unsafe legacy lifecycles will not be called for components using new component APIs.', - ); - }).toWarnDev( + await act(() => { + root.render(); + }); + assertConsoleErrorDev([ + 'Unsafe legacy lifecycles will not be called for components using new component APIs.\n\n' + + 'Component uses getDerivedStateFromProps() but also contains the following legacy lifecycles:\n' + + ' componentWillMount\n' + + ' componentWillReceiveProps\n' + + ' componentWillUpdate\n\n' + + 'The above lifecycles should be removed. Learn more about this warning here:\n' + + 'https://react.dev/link/unsafe-component-lifecycles\n' + + ' in Component (at **)', + ]); + assertConsoleWarnDev( [ - 'componentWillMount has been renamed', - 'componentWillReceiveProps has been renamed', - 'componentWillUpdate has been renamed', + 'componentWillMount has been renamed, and is not recommended for use. ' + + 'See https://react.dev/link/unsafe-component-lifecycles for details.\n\n' + + '* Move code with side effects to componentDidMount, and set initial state in the constructor.\n' + + '* Rename componentWillMount to UNSAFE_componentWillMount to suppress this warning in non-strict mode. ' + + 'In React 18.x, only the UNSAFE_ name will work. To rename all deprecated lifecycles to their new names, ' + + 'you can run `npx react-codemod rename-unsafe-lifecycles` in your project source folder.\n\n' + + 'Please update the following components: Component', + 'componentWillReceiveProps has been renamed, and is not recommended for use. ' + + 'See https://react.dev/link/unsafe-component-lifecycles for details.\n\n' + + '* Move data fetching code or side effects to componentDidUpdate.\n' + + "* If you're updating state whenever props change, refactor your code to use " + + 'memoization techniques or move it to static getDerivedStateFromProps. ' + + 'Learn more at: https://react.dev/link/derived-state\n' + + '* Rename componentWillReceiveProps to UNSAFE_componentWillReceiveProps to suppress this warning in non-strict mode. ' + + 'In React 18.x, only the UNSAFE_ name will work. To rename all deprecated lifecycles to their new names, ' + + 'you can run `npx react-codemod rename-unsafe-lifecycles` in your project source folder.\n\n' + + 'Please update the following components: Component', + 'componentWillUpdate has been renamed, and is not recommended for use. ' + + 'See https://react.dev/link/unsafe-component-lifecycles for details.\n\n' + + '* Move data fetching code or side effects to componentDidUpdate.\n' + + '* Rename componentWillUpdate to UNSAFE_componentWillUpdate to suppress this warning in non-strict mode. ' + + 'In React 18.x, only the UNSAFE_ name will work. To rename all deprecated lifecycles to their new names, ' + + 'you can run `npx react-codemod rename-unsafe-lifecycles` in your project source folder.\n\n' + + 'Please update the following components: Component', ], {withoutStack: true}, ); @@ -821,20 +875,45 @@ describe('ReactComponentLifeCycle', () => { } const root = ReactDOMClient.createRoot(document.createElement('div')); - await expect(async () => { - await expect( - async () => - await act(() => { - root.render(); - }), - ).toErrorDev( - 'Unsafe legacy lifecycles will not be called for components using new component APIs.', - ); - }).toWarnDev( + await act(() => { + root.render(); + }); + assertConsoleErrorDev([ + 'Unsafe legacy lifecycles will not be called for components using new component APIs.\n\n' + + 'Component uses getSnapshotBeforeUpdate() but also contains the following legacy lifecycles:\n' + + ' componentWillMount\n' + + ' componentWillReceiveProps\n' + + ' componentWillUpdate\n\n' + + 'The above lifecycles should be removed. Learn more about this warning here:\n' + + 'https://react.dev/link/unsafe-component-lifecycles\n' + + ' in Component (at **)', + ]); + assertConsoleWarnDev( [ - 'componentWillMount has been renamed', - 'componentWillReceiveProps has been renamed', - 'componentWillUpdate has been renamed', + 'componentWillMount has been renamed, and is not recommended for use. ' + + 'See https://react.dev/link/unsafe-component-lifecycles for details.\n\n' + + '* Move code with side effects to componentDidMount, and set initial state in the constructor.\n' + + '* Rename componentWillMount to UNSAFE_componentWillMount to suppress this warning in non-strict mode. ' + + 'In React 18.x, only the UNSAFE_ name will work. To rename all deprecated lifecycles to their new names, ' + + 'you can run `npx react-codemod rename-unsafe-lifecycles` in your project source folder.\n\n' + + 'Please update the following components: Component', + 'componentWillReceiveProps has been renamed, and is not recommended for use. ' + + 'See https://react.dev/link/unsafe-component-lifecycles for details.\n\n' + + '* Move data fetching code or side effects to componentDidUpdate.\n' + + "* If you're updating state whenever props change, refactor your code to use " + + 'memoization techniques or move it to static getDerivedStateFromProps. ' + + 'Learn more at: https://react.dev/link/derived-state\n' + + '* Rename componentWillReceiveProps to UNSAFE_componentWillReceiveProps to suppress this warning in non-strict mode. ' + + 'In React 18.x, only the UNSAFE_ name will work. To rename all deprecated lifecycles to their new names, ' + + 'you can run `npx react-codemod rename-unsafe-lifecycles` in your project source folder.\n\n' + + 'Please update the following components: Component', + 'componentWillUpdate has been renamed, and is not recommended for use. ' + + 'See https://react.dev/link/unsafe-component-lifecycles for details.\n\n' + + '* Move data fetching code or side effects to componentDidUpdate.\n' + + '* Rename componentWillUpdate to UNSAFE_componentWillUpdate to suppress this warning in non-strict mode. ' + + 'In React 18.x, only the UNSAFE_ name will work. To rename all deprecated lifecycles to their new names, ' + + 'you can run `npx react-codemod rename-unsafe-lifecycles` in your project source folder.\n\n' + + 'Please update the following components: Component', ], {withoutStack: true}, ); @@ -865,14 +944,19 @@ describe('ReactComponentLifeCycle', () => { } const root = ReactDOMClient.createRoot(document.createElement('div')); - await expect( - async () => - await act(() => { - root.render(); - }), - ).toErrorDev( - 'Unsafe legacy lifecycles will not be called for components using new component APIs.', - ); + await act(() => { + root.render(); + }); + assertConsoleErrorDev([ + 'Unsafe legacy lifecycles will not be called for components using new component APIs.\n\n' + + 'Component uses getDerivedStateFromProps() but also contains the following legacy lifecycles:\n' + + ' UNSAFE_componentWillMount\n' + + ' UNSAFE_componentWillReceiveProps\n' + + ' UNSAFE_componentWillUpdate\n\n' + + 'The above lifecycles should be removed. Learn more about this warning here:\n' + + 'https://react.dev/link/unsafe-component-lifecycles\n' + + ' in Component (at **)', + ]); await act(() => { root.render(); }); @@ -893,24 +977,35 @@ describe('ReactComponentLifeCycle', () => { } const root = ReactDOMClient.createRoot(document.createElement('div')); - await expect(async () => { - await expect(async () => { - await act(() => { - root.render(); - }); - }).toErrorDev( - 'Unsafe legacy lifecycles will not be called for components using new component APIs.\n\n' + - 'AllLegacyLifecycles uses getDerivedStateFromProps() but also contains the following legacy lifecycles:\n' + - ' componentWillMount\n' + - ' UNSAFE_componentWillReceiveProps\n' + - ' componentWillUpdate\n\n' + - 'The above lifecycles should be removed. Learn more about this warning here:\n' + - 'https://react.dev/link/unsafe-component-lifecycles', - ); - }).toWarnDev( + await act(() => { + root.render(); + }); + assertConsoleErrorDev([ + 'Unsafe legacy lifecycles will not be called for components using new component APIs.\n\n' + + 'AllLegacyLifecycles uses getDerivedStateFromProps() but also contains the following legacy lifecycles:\n' + + ' componentWillMount\n' + + ' UNSAFE_componentWillReceiveProps\n' + + ' componentWillUpdate\n\n' + + 'The above lifecycles should be removed. Learn more about this warning here:\n' + + 'https://react.dev/link/unsafe-component-lifecycles\n' + + ' in AllLegacyLifecycles (at **)', + ]); + assertConsoleWarnDev( [ - 'componentWillMount has been renamed', - 'componentWillUpdate has been renamed', + 'componentWillMount has been renamed, and is not recommended for use. ' + + 'See https://react.dev/link/unsafe-component-lifecycles for details.\n\n' + + '* Move code with side effects to componentDidMount, and set initial state in the constructor.\n' + + '* Rename componentWillMount to UNSAFE_componentWillMount to suppress this warning in non-strict mode. ' + + 'In React 18.x, only the UNSAFE_ name will work. To rename all deprecated lifecycles to their new names, ' + + 'you can run `npx react-codemod rename-unsafe-lifecycles` in your project source folder.\n\n' + + 'Please update the following components: AllLegacyLifecycles', + 'componentWillUpdate has been renamed, and is not recommended for use. ' + + 'See https://react.dev/link/unsafe-component-lifecycles for details.\n\n' + + '* Move data fetching code or side effects to componentDidUpdate.\n' + + '* Rename componentWillUpdate to UNSAFE_componentWillUpdate to suppress this warning in non-strict mode. ' + + 'In React 18.x, only the UNSAFE_ name will work. To rename all deprecated lifecycles to their new names, ' + + 'you can run `npx react-codemod rename-unsafe-lifecycles` in your project source folder.\n\n' + + 'Please update the following components: AllLegacyLifecycles', ], {withoutStack: true}, ); @@ -926,17 +1021,17 @@ describe('ReactComponentLifeCycle', () => { } } - await expect(async () => { - await act(() => { - root.render(); - }); - }).toErrorDev( + await act(() => { + root.render(); + }); + assertConsoleErrorDev([ 'Unsafe legacy lifecycles will not be called for components using new component APIs.\n\n' + 'WillMount uses getDerivedStateFromProps() but also contains the following legacy lifecycles:\n' + ' UNSAFE_componentWillMount\n\n' + 'The above lifecycles should be removed. Learn more about this warning here:\n' + - 'https://react.dev/link/unsafe-component-lifecycles', - ); + 'https://react.dev/link/unsafe-component-lifecycles\n' + + ' in WillMount (at **)', + ]); class WillMountAndUpdate extends React.Component { state = {}; @@ -950,23 +1045,30 @@ describe('ReactComponentLifeCycle', () => { } } - await expect(async () => { - await expect( - async () => - await act(() => { - root.render(); - }), - ).toErrorDev( - 'Unsafe legacy lifecycles will not be called for components using new component APIs.\n\n' + - 'WillMountAndUpdate uses getDerivedStateFromProps() but also contains the following legacy lifecycles:\n' + - ' componentWillMount\n' + - ' UNSAFE_componentWillUpdate\n\n' + - 'The above lifecycles should be removed. Learn more about this warning here:\n' + - 'https://react.dev/link/unsafe-component-lifecycles', - ); - }).toWarnDev(['componentWillMount has been renamed'], { - withoutStack: true, + await act(() => { + root.render(); }); + assertConsoleErrorDev([ + 'Unsafe legacy lifecycles will not be called for components using new component APIs.\n\n' + + 'WillMountAndUpdate uses getDerivedStateFromProps() but also contains the following legacy lifecycles:\n' + + ' componentWillMount\n' + + ' UNSAFE_componentWillUpdate\n\n' + + 'The above lifecycles should be removed. Learn more about this warning here:\n' + + 'https://react.dev/link/unsafe-component-lifecycles\n' + + ' in WillMountAndUpdate (at **)', + ]); + assertConsoleWarnDev( + [ + 'componentWillMount has been renamed, and is not recommended for use. ' + + 'See https://react.dev/link/unsafe-component-lifecycles for details.\n\n' + + '* Move code with side effects to componentDidMount, and set initial state in the constructor.\n' + + '* Rename componentWillMount to UNSAFE_componentWillMount to suppress this warning in non-strict mode. ' + + 'In React 18.x, only the UNSAFE_ name will work. To rename all deprecated lifecycles to their new names, ' + + 'you can run `npx react-codemod rename-unsafe-lifecycles` in your project source folder.\n\n' + + 'Please update the following components: WillMountAndUpdate', + ], + {withoutStack: true}, + ); class WillReceiveProps extends React.Component { state = {}; @@ -979,21 +1081,34 @@ describe('ReactComponentLifeCycle', () => { } } - await expect(async () => { - await expect(async () => { - await act(() => { - root.render(); - }); - }).toErrorDev( - 'Unsafe legacy lifecycles will not be called for components using new component APIs.\n\n' + - 'WillReceiveProps uses getDerivedStateFromProps() but also contains the following legacy lifecycles:\n' + - ' componentWillReceiveProps\n\n' + - 'The above lifecycles should be removed. Learn more about this warning here:\n' + - 'https://react.dev/link/unsafe-component-lifecycles', - ); - }).toWarnDev(['componentWillReceiveProps has been renamed'], { - withoutStack: true, + await act(() => { + root.render(); }); + assertConsoleErrorDev([ + 'Unsafe legacy lifecycles will not be called for components using new component APIs.\n\n' + + 'WillReceiveProps uses getDerivedStateFromProps() but also contains the following legacy lifecycles:\n' + + ' componentWillReceiveProps\n\n' + + 'The above lifecycles should be removed. Learn more about this warning here:\n' + + 'https://react.dev/link/unsafe-component-lifecycles\n' + + ' in WillReceiveProps (at **)', + ]); + assertConsoleWarnDev( + [ + 'componentWillReceiveProps has been renamed, and is not recommended for use. ' + + 'See https://react.dev/link/unsafe-component-lifecycles for details.\n\n' + + '* Move data fetching code or side effects to componentDidUpdate.\n' + + "* If you're updating state whenever props change, refactor your code to use " + + 'memoization techniques or move it to static getDerivedStateFromProps. ' + + 'Learn more at: https://react.dev/link/derived-state\n' + + '* Rename componentWillReceiveProps to UNSAFE_componentWillReceiveProps to suppress this warning in non-strict mode. ' + + 'In React 18.x, only the UNSAFE_ name will work. To rename all deprecated lifecycles to their new names, ' + + 'you can run `npx react-codemod rename-unsafe-lifecycles` in your project source folder.\n\n' + + 'Please update the following components: WillReceiveProps', + ], + { + withoutStack: true, + }, + ); }); it('should warn about deprecated lifecycles (cWM/cWRP/cWU) if new getSnapshotBeforeUpdate is present', async () => { @@ -1010,24 +1125,35 @@ describe('ReactComponentLifeCycle', () => { } const root = ReactDOMClient.createRoot(document.createElement('div')); - await expect(async () => { - await expect(async () => { - await act(() => { - root.render(); - }); - }).toErrorDev( - 'Unsafe legacy lifecycles will not be called for components using new component APIs.\n\n' + - 'AllLegacyLifecycles uses getSnapshotBeforeUpdate() but also contains the following legacy lifecycles:\n' + - ' componentWillMount\n' + - ' UNSAFE_componentWillReceiveProps\n' + - ' componentWillUpdate\n\n' + - 'The above lifecycles should be removed. Learn more about this warning here:\n' + - 'https://react.dev/link/unsafe-component-lifecycles', - ); - }).toWarnDev( + await act(() => { + root.render(); + }); + assertConsoleErrorDev([ + 'Unsafe legacy lifecycles will not be called for components using new component APIs.\n\n' + + 'AllLegacyLifecycles uses getSnapshotBeforeUpdate() but also contains the following legacy lifecycles:\n' + + ' componentWillMount\n' + + ' UNSAFE_componentWillReceiveProps\n' + + ' componentWillUpdate\n\n' + + 'The above lifecycles should be removed. Learn more about this warning here:\n' + + 'https://react.dev/link/unsafe-component-lifecycles\n' + + ' in AllLegacyLifecycles (at **)', + ]); + assertConsoleWarnDev( [ - 'componentWillMount has been renamed', - 'componentWillUpdate has been renamed', + 'componentWillMount has been renamed, and is not recommended for use. ' + + 'See https://react.dev/link/unsafe-component-lifecycles for details.\n\n' + + '* Move code with side effects to componentDidMount, and set initial state in the constructor.\n' + + '* Rename componentWillMount to UNSAFE_componentWillMount to suppress this warning in non-strict mode. ' + + 'In React 18.x, only the UNSAFE_ name will work. To rename all deprecated lifecycles to their new names, ' + + 'you can run `npx react-codemod rename-unsafe-lifecycles` in your project source folder.\n\n' + + 'Please update the following components: AllLegacyLifecycles', + 'componentWillUpdate has been renamed, and is not recommended for use. ' + + 'See https://react.dev/link/unsafe-component-lifecycles for details.\n\n' + + '* Move data fetching code or side effects to componentDidUpdate.\n' + + '* Rename componentWillUpdate to UNSAFE_componentWillUpdate to suppress this warning in non-strict mode. ' + + 'In React 18.x, only the UNSAFE_ name will work. To rename all deprecated lifecycles to their new names, ' + + 'you can run `npx react-codemod rename-unsafe-lifecycles` in your project source folder.\n\n' + + 'Please update the following components: AllLegacyLifecycles', ], {withoutStack: true}, ); @@ -1042,17 +1168,17 @@ describe('ReactComponentLifeCycle', () => { } } - await expect(async () => { - await act(() => { - root.render(); - }); - }).toErrorDev( + await act(() => { + root.render(); + }); + assertConsoleErrorDev([ 'Unsafe legacy lifecycles will not be called for components using new component APIs.\n\n' + 'WillMount uses getSnapshotBeforeUpdate() but also contains the following legacy lifecycles:\n' + ' UNSAFE_componentWillMount\n\n' + 'The above lifecycles should be removed. Learn more about this warning here:\n' + - 'https://react.dev/link/unsafe-component-lifecycles', - ); + 'https://react.dev/link/unsafe-component-lifecycles\n' + + ' in WillMount (at **)', + ]); class WillMountAndUpdate extends React.Component { state = {}; @@ -1065,22 +1191,32 @@ describe('ReactComponentLifeCycle', () => { } } - await expect(async () => { - await expect(async () => { - await act(() => { - root.render(); - }); - }).toErrorDev( - 'Unsafe legacy lifecycles will not be called for components using new component APIs.\n\n' + - 'WillMountAndUpdate uses getSnapshotBeforeUpdate() but also contains the following legacy lifecycles:\n' + - ' componentWillMount\n' + - ' UNSAFE_componentWillUpdate\n\n' + - 'The above lifecycles should be removed. Learn more about this warning here:\n' + - 'https://react.dev/link/unsafe-component-lifecycles', - ); - }).toWarnDev(['componentWillMount has been renamed'], { - withoutStack: true, + await act(() => { + root.render(); }); + assertConsoleErrorDev([ + 'Unsafe legacy lifecycles will not be called for components using new component APIs.\n\n' + + 'WillMountAndUpdate uses getSnapshotBeforeUpdate() but also contains the following legacy lifecycles:\n' + + ' componentWillMount\n' + + ' UNSAFE_componentWillUpdate\n\n' + + 'The above lifecycles should be removed. Learn more about this warning here:\n' + + 'https://react.dev/link/unsafe-component-lifecycles\n' + + ' in WillMountAndUpdate (at **)', + ]); + assertConsoleWarnDev( + [ + 'componentWillMount has been renamed, and is not recommended for use. ' + + 'See https://react.dev/link/unsafe-component-lifecycles for details.\n\n' + + '* Move code with side effects to componentDidMount, and set initial state in the constructor.\n' + + '* Rename componentWillMount to UNSAFE_componentWillMount to suppress this warning in non-strict mode. ' + + 'In React 18.x, only the UNSAFE_ name will work. To rename all deprecated lifecycles to their new names, ' + + 'you can run `npx react-codemod rename-unsafe-lifecycles` in your project source folder.\n\n' + + 'Please update the following components: WillMountAndUpdate', + ], + { + withoutStack: true, + }, + ); class WillReceiveProps extends React.Component { state = {}; @@ -1092,22 +1228,34 @@ describe('ReactComponentLifeCycle', () => { } } - await expect(async () => { - await expect( - async () => - await act(() => { - root.render(); - }), - ).toErrorDev( - 'Unsafe legacy lifecycles will not be called for components using new component APIs.\n\n' + - 'WillReceiveProps uses getSnapshotBeforeUpdate() but also contains the following legacy lifecycles:\n' + - ' componentWillReceiveProps\n\n' + - 'The above lifecycles should be removed. Learn more about this warning here:\n' + - 'https://react.dev/link/unsafe-component-lifecycles', - ); - }).toWarnDev(['componentWillReceiveProps has been renamed'], { - withoutStack: true, + await act(() => { + root.render(); }); + assertConsoleErrorDev([ + 'Unsafe legacy lifecycles will not be called for components using new component APIs.\n\n' + + 'WillReceiveProps uses getSnapshotBeforeUpdate() but also contains the following legacy lifecycles:\n' + + ' componentWillReceiveProps\n\n' + + 'The above lifecycles should be removed. Learn more about this warning here:\n' + + 'https://react.dev/link/unsafe-component-lifecycles\n' + + ' in WillReceiveProps (at **)', + ]); + assertConsoleWarnDev( + [ + 'componentWillReceiveProps has been renamed, and is not recommended for use. ' + + 'See https://react.dev/link/unsafe-component-lifecycles for details.\n\n' + + '* Move data fetching code or side effects to componentDidUpdate.\n' + + "* If you're updating state whenever props change, refactor your code to use " + + 'memoization techniques or move it to static getDerivedStateFromProps. ' + + 'Learn more at: https://react.dev/link/derived-state\n' + + '* Rename componentWillReceiveProps to UNSAFE_componentWillReceiveProps to suppress this warning in non-strict mode. ' + + 'In React 18.x, only the UNSAFE_ name will work. To rename all deprecated lifecycles to their new names, ' + + 'you can run `npx react-codemod rename-unsafe-lifecycles` in your project source folder.\n\n' + + 'Please update the following components: WillReceiveProps', + ], + { + withoutStack: true, + }, + ); }); it('should warn if getDerivedStateFromProps returns undefined', async () => { @@ -1120,14 +1268,14 @@ describe('ReactComponentLifeCycle', () => { } const root = ReactDOMClient.createRoot(document.createElement('div')); - await expect(async () => { - await act(() => { - root.render(); - }); - }).toErrorDev( + await act(() => { + root.render(); + }); + assertConsoleErrorDev([ 'MyComponent.getDerivedStateFromProps(): A valid state object (or null) must ' + - 'be returned. You have returned undefined.', - ); + 'be returned. You have returned undefined.\n' + + ' in MyComponent (at **)', + ]); // De-duped await act(() => { @@ -1146,16 +1294,16 @@ describe('ReactComponentLifeCycle', () => { } const root = ReactDOMClient.createRoot(document.createElement('div')); - await expect(async () => { - await act(() => { - root.render(); - }); - }).toErrorDev( + await act(() => { + root.render(); + }); + assertConsoleErrorDev([ '`MyComponent` uses `getDerivedStateFromProps` but its initial state is ' + 'undefined. This is not recommended. Instead, define the initial state by ' + 'assigning an object to `this.state` in the constructor of `MyComponent`. ' + - 'This ensures that `getDerivedStateFromProps` arguments have a consistent shape.', - ); + 'This ensures that `getDerivedStateFromProps` arguments have a consistent shape.\n' + + ' in MyComponent (at **)', + ]); // De-duped await act(() => { @@ -1191,15 +1339,35 @@ describe('ReactComponentLifeCycle', () => { } const root = ReactDOMClient.createRoot(document.createElement('div')); - await expect(async () => { - await act(() => { - root.render(); - }); - }).toWarnDev( + await act(() => { + root.render(); + }); + assertConsoleWarnDev( [ - 'componentWillMount has been renamed', - 'componentWillReceiveProps has been renamed', - 'componentWillUpdate has been renamed', + 'componentWillMount has been renamed, and is not recommended for use. ' + + 'See https://react.dev/link/unsafe-component-lifecycles for details.\n\n' + + '* Move code with side effects to componentDidMount, and set initial state in the constructor.\n' + + '* Rename componentWillMount to UNSAFE_componentWillMount to suppress this warning in non-strict mode. ' + + 'In React 18.x, only the UNSAFE_ name will work. To rename all deprecated lifecycles to their new names, ' + + 'you can run `npx react-codemod rename-unsafe-lifecycles` in your project source folder.\n\n' + + 'Please update the following components: MyComponent', + 'componentWillReceiveProps has been renamed, and is not recommended for use. ' + + 'See https://react.dev/link/unsafe-component-lifecycles for details.\n\n' + + '* Move data fetching code or side effects to componentDidUpdate.\n' + + "* If you're updating state whenever props change, refactor your code to use " + + 'memoization techniques or move it to static getDerivedStateFromProps. ' + + 'Learn more at: https://react.dev/link/derived-state\n' + + '* Rename componentWillReceiveProps to UNSAFE_componentWillReceiveProps to suppress this warning in non-strict mode. ' + + 'In React 18.x, only the UNSAFE_ name will work. To rename all deprecated lifecycles to their new names, ' + + 'you can run `npx react-codemod rename-unsafe-lifecycles` in your project source folder.\n\n' + + 'Please update the following components: MyComponent', + 'componentWillUpdate has been renamed, and is not recommended for use. ' + + 'See https://react.dev/link/unsafe-component-lifecycles for details.\n\n' + + '* Move data fetching code or side effects to componentDidUpdate.\n' + + '* Rename componentWillUpdate to UNSAFE_componentWillUpdate to suppress this warning in non-strict mode. ' + + 'In React 18.x, only the UNSAFE_ name will work. To rename all deprecated lifecycles to their new names, ' + + 'you can run `npx react-codemod rename-unsafe-lifecycles` in your project source folder.\n\n' + + 'Please update the following components: MyComponent', ], {withoutStack: true}, ); @@ -1444,14 +1612,14 @@ describe('ReactComponentLifeCycle', () => { root.render(); }); - await expect(async () => { - await act(() => { - root.render(); - }); - }).toErrorDev( + await act(() => { + root.render(); + }); + assertConsoleErrorDev([ 'MyComponent.getSnapshotBeforeUpdate(): A snapshot value (or null) must ' + - 'be returned. You have returned undefined.', - ); + 'be returned. You have returned undefined.\n' + + ' in MyComponent (at **)', + ]); // De-duped await act(() => { @@ -1470,14 +1638,14 @@ describe('ReactComponentLifeCycle', () => { } const root = ReactDOMClient.createRoot(document.createElement('div')); - await expect(async () => { - await act(() => { - root.render(); - }); - }).toErrorDev( + await act(() => { + root.render(); + }); + assertConsoleErrorDev([ 'MyComponent: getSnapshotBeforeUpdate() should be used with componentDidUpdate(). ' + - 'This component defines getSnapshotBeforeUpdate() only.', - ); + 'This component defines getSnapshotBeforeUpdate() only.\n' + + ' in MyComponent (at **)', + ]); // De-duped await act(() => { @@ -1497,11 +1665,10 @@ describe('ReactComponentLifeCycle', () => { const root = ReactDOMClient.createRoot(document.createElement('div')); - await expect(async () => { - await act(() => { - root.render(); - }); - }).toWarnDev( + await act(() => { + root.render(); + }); + assertConsoleWarnDev( [ `componentWillMount has been renamed, and is not recommended for use. See https://react.dev/link/unsafe-component-lifecycles for details. diff --git a/packages/react-dom/src/__tests__/ReactCompositeComponent-test.js b/packages/react-dom/src/__tests__/ReactCompositeComponent-test.js index 30b5ced5cf2e3..3d5f61c1ed520 100644 --- a/packages/react-dom/src/__tests__/ReactCompositeComponent-test.js +++ b/packages/react-dom/src/__tests__/ReactCompositeComponent-test.js @@ -18,6 +18,7 @@ let ReactSharedInternals; let Scheduler; let assertLog; let act; +let assertConsoleErrorDev; describe('ReactCompositeComponent', () => { const hasOwnProperty = Object.prototype.hasOwnProperty; @@ -71,7 +72,7 @@ describe('ReactCompositeComponent', () => { require('react').__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE; Scheduler = require('scheduler'); assertLog = require('internal-test-utils').assertLog; - act = require('internal-test-utils').act; + ({act, assertConsoleErrorDev} = require('internal-test-utils')); }); describe('MorphingComponent', () => { @@ -308,16 +309,16 @@ describe('ReactCompositeComponent', () => { const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); - expect(() => { - ReactDOM.flushSync(() => { - root.render(); - }); - }).toErrorDev( + ReactDOM.flushSync(() => { + root.render(); + }); + assertConsoleErrorDev([ "Can't call forceUpdate on a component that is not yet mounted. " + 'This is a no-op, but it might indicate a bug in your application. ' + 'Instead, assign to `this.state` directly or define a `state = {};` ' + - 'class property with the desired state in the MyComponent component.', - ); + 'class property with the desired state in the MyComponent component.\n' + + ' in MyComponent (at **)', + ]); // No additional warning should be recorded const container2 = document.createElement('div'); @@ -342,16 +343,16 @@ describe('ReactCompositeComponent', () => { const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); - expect(() => { - ReactDOM.flushSync(() => { - root.render(); - }); - }).toErrorDev( + ReactDOM.flushSync(() => { + root.render(); + }); + assertConsoleErrorDev([ "Can't call setState on a component that is not yet mounted. " + 'This is a no-op, but it might indicate a bug in your application. ' + 'Instead, assign to `this.state` directly or define a `state = {};` ' + - 'class property with the desired state in the MyComponent component.', - ); + 'class property with the desired state in the MyComponent component.\n' + + ' in MyComponent (at **)', + ]); // No additional warning should be recorded const container2 = document.createElement('div'); @@ -478,16 +479,16 @@ describe('ReactCompositeComponent', () => { } const root = ReactDOMClient.createRoot(container); await expect(async () => { - await expect(async () => { - await act(() => { - root.render(); - }); - }).rejects.toThrow(TypeError); - }).toErrorDev( + await act(() => { + root.render(); + }); + }).rejects.toThrow(TypeError); + assertConsoleErrorDev([ 'The component appears to have a render method, ' + "but doesn't extend React.Component. This is likely to cause errors. " + - 'Change ClassWithRenderNotExtended to extend React.Component instead.', - ); + 'Change ClassWithRenderNotExtended to extend React.Component instead.\n' + + ' in ClassWithRenderNotExtended (at **)', + ]); // Test deduplication await expect(async () => { @@ -514,14 +515,14 @@ describe('ReactCompositeComponent', () => { let instance; const root = ReactDOMClient.createRoot(container); - expect(() => { - ReactDOM.flushSync(() => { - root.render( (instance = ref)} />); - }); - }).toErrorDev( + ReactDOM.flushSync(() => { + root.render( (instance = ref)} />); + }); + assertConsoleErrorDev([ 'Cannot update during an existing state transition (such as within ' + - '`render`). Render methods should be a pure function of props and state.', - ); + '`render`). Render methods should be a pure function of props and state.\n' + + ' in Component (at **)', + ]); // The setState call is queued and then executed as a second pass. This // behavior is undefined though so we're free to change it to suit the @@ -618,14 +619,14 @@ describe('ReactCompositeComponent', () => { root.render( (instance = ref)} />); }); - expect(() => { - ReactDOM.flushSync(() => { - instance.setState({bogus: true}); - }); - }).toErrorDev( + ReactDOM.flushSync(() => { + instance.setState({bogus: true}); + }); + assertConsoleErrorDev([ 'ClassComponent.shouldComponentUpdate(): Returned undefined instead of a ' + - 'boolean value. Make sure to return true or false.', - ); + 'boolean value. Make sure to return true or false.\n' + + ' in ClassComponent (at **)', + ]); }); it('should warn when componentDidUnmount method is defined', async () => { @@ -638,15 +639,15 @@ describe('ReactCompositeComponent', () => { } const root = ReactDOMClient.createRoot(document.createElement('div')); - expect(() => { - ReactDOM.flushSync(() => { - root.render(); - }); - }).toErrorDev( + ReactDOM.flushSync(() => { + root.render(); + }); + assertConsoleErrorDev([ 'Component has a method called ' + 'componentDidUnmount(). But there is no such lifecycle method. ' + - 'Did you mean componentWillUnmount()?', - ); + 'Did you mean componentWillUnmount()?\n' + + ' in Component (at **)', + ]); }); it('should warn when componentDidReceiveProps method is defined', () => { @@ -660,17 +661,17 @@ describe('ReactCompositeComponent', () => { const root = ReactDOMClient.createRoot(document.createElement('div')); - expect(() => { - ReactDOM.flushSync(() => { - root.render(); - }); - }).toErrorDev( + ReactDOM.flushSync(() => { + root.render(); + }); + assertConsoleErrorDev([ 'Component has a method called ' + 'componentDidReceiveProps(). But there is no such lifecycle method. ' + 'If you meant to update the state in response to changing props, ' + 'use componentWillReceiveProps(). If you meant to fetch data or ' + - 'run side-effects or mutations after React has updated the UI, use componentDidUpdate().', - ); + 'run side-effects or mutations after React has updated the UI, use componentDidUpdate().\n' + + ' in Component (at **)', + ]); }); it('should warn when defaultProps was defined as an instance property', () => { @@ -686,14 +687,14 @@ describe('ReactCompositeComponent', () => { } const root = ReactDOMClient.createRoot(document.createElement('div')); - expect(() => { - ReactDOM.flushSync(() => { - root.render(); - }); - }).toErrorDev( + ReactDOM.flushSync(() => { + root.render(); + }); + assertConsoleErrorDev([ 'Setting defaultProps as an instance property on Component is not supported ' + - 'and will be ignored. Instead, define defaultProps as a static property on Component.', - ); + 'and will be ignored. Instead, define defaultProps as a static property on Component.\n' + + ' in Component (at **)', + ]); }); it('should skip update when rerendering element in container', async () => { @@ -739,16 +740,16 @@ describe('ReactCompositeComponent', () => { } } - expect(() => { - ReactDOM.flushSync(() => { - root.render(); - }); - }).toErrorDev( + ReactDOM.flushSync(() => { + root.render(); + }); + assertConsoleErrorDev([ 'Render methods should be a pure function of props and state; ' + 'triggering nested component updates from render is not allowed. If ' + - 'necessary, trigger nested updates in componentDidUpdate.\n\nCheck the ' + - 'render method of Outer.', - ); + 'necessary, trigger nested updates in componentDidUpdate.\n\n' + + 'Check the render method of Outer.\n' + + ' in Outer (at **)', + ]); }); it('only renders once if updated in componentWillReceiveProps', async () => { @@ -836,14 +837,14 @@ describe('ReactCompositeComponent', () => { } const root = ReactDOMClient.createRoot(container); - expect(() => { - ReactDOM.flushSync(() => { - root.render(); - }); - }).toErrorDev( + ReactDOM.flushSync(() => { + root.render(); + }); + assertConsoleErrorDev([ 'When calling super() in `Foo`, make sure to pass ' + - "up the same props that your component's constructor was passed.", - ); + "up the same props that your component's constructor was passed.\n" + + ' in Foo (at **)', + ]); }); it('should only call componentWillUnmount once', async () => { @@ -1185,16 +1186,17 @@ describe('ReactCompositeComponent', () => { const root = ReactDOMClient.createRoot(document.createElement('div')); await expect(async () => { - await expect(async () => { - await act(() => { - root.render(); - }); - }).rejects.toThrow(); - }).toErrorDev([ + await act(() => { + root.render(); + }); + }).rejects.toThrow(); + assertConsoleErrorDev([ 'No `render` method found on the RenderTextInvalidConstructor instance: ' + - 'did you accidentally return an object from the constructor?', + 'did you accidentally return an object from the constructor?\n' + + ' in RenderTextInvalidConstructor (at **)', 'No `render` method found on the RenderTextInvalidConstructor instance: ' + - 'did you accidentally return an object from the constructor?', + 'did you accidentally return an object from the constructor?\n' + + ' in RenderTextInvalidConstructor (at **)', ]); }); @@ -1210,14 +1212,14 @@ describe('ReactCompositeComponent', () => { const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); - expect(() => { - ReactDOM.flushSync(() => { - root.render(); - }); - }).toErrorDev( + ReactDOM.flushSync(() => { + root.render(); + }); + assertConsoleErrorDev([ 'It looks like Bad is reassigning its own `this.props` while rendering. ' + - 'This is not supported and can lead to confusing bugs.', - ); + 'This is not supported and can lead to confusing bugs.\n' + + ' in Bad (at **)', + ]); }); it('should return error if render is not defined', async () => { @@ -1225,16 +1227,17 @@ describe('ReactCompositeComponent', () => { const root = ReactDOMClient.createRoot(document.createElement('div')); await expect(async () => { - await expect(async () => { - await act(() => { - root.render(); - }); - }).rejects.toThrow(); - }).toErrorDev([ + await act(() => { + root.render(); + }); + }).rejects.toThrow(); + assertConsoleErrorDev([ 'No `render` method found on the RenderTestUndefinedRender instance: ' + - 'you may have forgotten to define `render`.', + 'you may have forgotten to define `render`.\n' + + ' in RenderTestUndefinedRender (at **)', 'No `render` method found on the RenderTestUndefinedRender instance: ' + - 'you may have forgotten to define `render`.', + 'you may have forgotten to define `render`.\n' + + ' in RenderTestUndefinedRender (at **)', ]); }); @@ -1386,13 +1389,18 @@ describe('ReactCompositeComponent', () => { } const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); - expect(() => { - ReactDOM.flushSync(() => { - root.render(); - }); - }).toErrorDev( - 'Cannot update a component (`A`) while rendering a different component (`B`)', - ); + ReactDOM.flushSync(() => { + root.render(); + }); + assertConsoleErrorDev([ + 'Cannot update a component (`A`) while rendering a different component (`B`). ' + + 'To locate the bad setState() call inside `B`, ' + + 'follow the stack trace as described in https://react.dev/link/setstate-in-render\n' + + (gate('enableOwnerStacks') + ? '' + : ' in B (at **)\n' + ' in div (at **)\n') + + ' in Parent (at **)', + ]); // We error, but still update the state. expect(ref.textContent).toBe('1'); diff --git a/packages/react-dom/src/__tests__/ReactCompositeComponentState-test.js b/packages/react-dom/src/__tests__/ReactCompositeComponentState-test.js index a651a477a8e76..d0d1c36e514a9 100644 --- a/packages/react-dom/src/__tests__/ReactCompositeComponentState-test.js +++ b/packages/react-dom/src/__tests__/ReactCompositeComponentState-test.js @@ -17,13 +17,14 @@ let Scheduler; let assertLog; let TestComponent; let testComponentInstance; +let assertConsoleErrorDev; describe('ReactCompositeComponent-state', () => { beforeEach(() => { React = require('react'); ReactDOM = require('react-dom'); ReactDOMClient = require('react-dom/client'); - act = require('internal-test-utils').act; + ({act, assertConsoleErrorDev} = require('internal-test-utils')); Scheduler = require('scheduler'); const InternalTestUtils = require('internal-test-utils'); @@ -469,15 +470,15 @@ describe('ReactCompositeComponent-state', () => { root.render(); }); // Update - expect(() => { - ReactDOM.flushSync(() => { - root.render(); - }); - }).toErrorDev( + ReactDOM.flushSync(() => { + root.render(); + }); + assertConsoleErrorDev([ 'Test.componentWillReceiveProps(): Assigning directly to ' + "this.state is deprecated (except inside a component's constructor). " + - 'Use setState instead.', - ); + 'Use setState instead.\n' + + ' in Test (at **)', + ]); assertLog([ 'render -- step: 1, extra: true', @@ -518,15 +519,15 @@ describe('ReactCompositeComponent-state', () => { // Mount const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); - expect(() => { - ReactDOM.flushSync(() => { - root.render(); - }); - }).toErrorDev( + ReactDOM.flushSync(() => { + root.render(); + }); + assertConsoleErrorDev([ 'Test.componentWillMount(): Assigning directly to ' + "this.state is deprecated (except inside a component's constructor). " + - 'Use setState instead.', - ); + 'Use setState instead.\n' + + ' in Test (at **)', + ]); assertLog([ 'render -- step: 3, extra: false', @@ -566,13 +567,16 @@ describe('ReactCompositeComponent-state', () => { }); expect(el.textContent).toBe('A'); - expect(() => { - ReactDOM.flushSync(() => { - root.render(); - }); - }).toErrorDev( - "Can't perform a React state update on a component that hasn't mounted yet", - ); + ReactDOM.flushSync(() => { + root.render(); + }); + assertConsoleErrorDev([ + "Can't perform a React state update on a component that hasn't mounted yet. " + + 'This indicates that you have a side-effect in your render function that ' + + 'asynchronously later calls tries to update the component. ' + + 'Move this work to useEffect instead.\n' + + ' in B (at **)', + ]); }); // @gate !disableLegacyMode diff --git a/packages/react-dom/src/__tests__/ReactDOM-test.js b/packages/react-dom/src/__tests__/ReactDOM-test.js index c6c90d1cf15f2..92571ad69e10c 100644 --- a/packages/react-dom/src/__tests__/ReactDOM-test.js +++ b/packages/react-dom/src/__tests__/ReactDOM-test.js @@ -14,7 +14,7 @@ let ReactDOM; let findDOMNode; let ReactDOMClient; let ReactDOMServer; - +let assertConsoleErrorDev; let act; describe('ReactDOM', () => { @@ -28,7 +28,7 @@ describe('ReactDOM', () => { ReactDOM.__DOM_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE .findDOMNode; - act = require('internal-test-utils').act; + ({act, assertConsoleErrorDev} = require('internal-test-utils')); }); it('should bubble onSubmit', async () => { @@ -188,15 +188,14 @@ describe('ReactDOM', () => { const myDiv = document.createElement('div'); await expect(async () => { - await expect(async () => { - await act(() => { - ReactDOM.render(, myDiv, 'no'); - }); - }).rejects.toThrowError( - 'Invalid argument passed as callback. Expected a function. Instead ' + - 'received: no', - ); - }).toErrorDev( + await act(() => { + ReactDOM.render(, myDiv, 'no'); + }); + }).rejects.toThrowError( + 'Invalid argument passed as callback. Expected a function. Instead ' + + 'received: no', + ); + assertConsoleErrorDev( [ 'Expected the last optional `callback` argument to be a function. Instead received: no.', 'Expected the last optional `callback` argument to be a function. Instead received: no.', @@ -205,15 +204,14 @@ describe('ReactDOM', () => { ); await expect(async () => { - await expect(async () => { - await act(() => { - ReactDOM.render(, myDiv, {foo: 'bar'}); - }); - }).rejects.toThrowError( - 'Invalid argument passed as callback. Expected a function. Instead ' + - 'received: [object Object]', - ); - }).toErrorDev( + await act(() => { + ReactDOM.render(, myDiv, {foo: 'bar'}); + }); + }).rejects.toThrowError( + 'Invalid argument passed as callback. Expected a function. Instead ' + + 'received: [object Object]', + ); + assertConsoleErrorDev( [ "Expected the last optional `callback` argument to be a function. Instead received: { foo: 'bar' }", "Expected the last optional `callback` argument to be a function. Instead received: { foo: 'bar' }.", @@ -222,15 +220,14 @@ describe('ReactDOM', () => { ); await expect(async () => { - await expect(async () => { - await act(() => { - ReactDOM.render(, myDiv, new Foo()); - }); - }).rejects.toThrowError( - 'Invalid argument passed as callback. Expected a function. Instead ' + - 'received: [object Object]', - ); - }).toErrorDev( + await act(() => { + ReactDOM.render(, myDiv, new Foo()); + }); + }).rejects.toThrowError( + 'Invalid argument passed as callback. Expected a function. Instead ' + + 'received: [object Object]', + ); + assertConsoleErrorDev( [ 'Expected the last optional `callback` argument to be a function. Instead received: Foo { a: 1, b: 2 }.', 'Expected the last optional `callback` argument to be a function. Instead received: Foo { a: 1, b: 2 }.', @@ -257,15 +254,14 @@ describe('ReactDOM', () => { const myDiv = document.createElement('div'); ReactDOM.render(, myDiv); await expect(async () => { - await expect(async () => { - await act(() => { - ReactDOM.render(, myDiv, 'no'); - }); - }).rejects.toThrowError( - 'Invalid argument passed as callback. Expected a function. Instead ' + - 'received: no', - ); - }).toErrorDev( + await act(() => { + ReactDOM.render(, myDiv, 'no'); + }); + }).rejects.toThrowError( + 'Invalid argument passed as callback. Expected a function. Instead ' + + 'received: no', + ); + assertConsoleErrorDev( [ 'Expected the last optional `callback` argument to be a function. Instead received: no.', 'Expected the last optional `callback` argument to be a function. Instead received: no.', @@ -275,15 +271,14 @@ describe('ReactDOM', () => { ReactDOM.render(, myDiv); // Re-mount await expect(async () => { - await expect(async () => { - await act(() => { - ReactDOM.render(, myDiv, {foo: 'bar'}); - }); - }).rejects.toThrowError( - 'Invalid argument passed as callback. Expected a function. Instead ' + - 'received: [object Object]', - ); - }).toErrorDev( + await act(() => { + ReactDOM.render(, myDiv, {foo: 'bar'}); + }); + }).rejects.toThrowError( + 'Invalid argument passed as callback. Expected a function. Instead ' + + 'received: [object Object]', + ); + assertConsoleErrorDev( [ "Expected the last optional `callback` argument to be a function. Instead received: { foo: 'bar' }.", "Expected the last optional `callback` argument to be a function. Instead received: { foo: 'bar' }.", @@ -293,15 +288,14 @@ describe('ReactDOM', () => { ReactDOM.render(, myDiv); // Re-mount await expect(async () => { - await expect(async () => { - await act(() => { - ReactDOM.render(, myDiv, new Foo()); - }); - }).rejects.toThrowError( - 'Invalid argument passed as callback. Expected a function. Instead ' + - 'received: [object Object]', - ); - }).toErrorDev( + await act(() => { + ReactDOM.render(, myDiv, new Foo()); + }); + }).rejects.toThrowError( + 'Invalid argument passed as callback. Expected a function. Instead ' + + 'received: [object Object]', + ); + assertConsoleErrorDev( [ 'Expected the last optional `callback` argument to be a function. Instead received: Foo { a: 1, b: 2 }.', 'Expected the last optional `callback` argument to be a function. Instead received: Foo { a: 1, b: 2 }.', @@ -544,11 +538,10 @@ describe('ReactDOM', () => { } const root = ReactDOMClient.createRoot(document.createElement('div')); - await expect(async () => { - await act(() => { - root.render(); - }); - }).toErrorDev([ + await act(() => { + root.render(); + }); + assertConsoleErrorDev([ // ReactDOM(App > div > span) 'Invalid ARIA attribute `ariaTypo`. ARIA attributes follow the pattern aria-* and must be lowercase.\n' + ' in span (at **)\n' + diff --git a/packages/react-dom/src/__tests__/ReactDOMAttribute-test.js b/packages/react-dom/src/__tests__/ReactDOMAttribute-test.js index 9b7d9a30cbd53..cd1d055c09d1c 100644 --- a/packages/react-dom/src/__tests__/ReactDOMAttribute-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMAttribute-test.js @@ -13,12 +13,15 @@ describe('ReactDOM unknown attribute', () => { let React; let ReactDOMClient; let act; + let assertConsoleErrorDev; beforeEach(() => { jest.resetModules(); React = require('react'); ReactDOMClient = require('react-dom/client'); act = require('internal-test-utils').act; + assertConsoleErrorDev = + require('internal-test-utils').assertConsoleErrorDev; }); async function testUnknownAttributeRemoval(givenValue) { @@ -62,12 +65,13 @@ describe('ReactDOM unknown attribute', () => { }); it('changes values true, false to null, and also warns once', async () => { - await expect(() => testUnknownAttributeAssignment(true, null)).toErrorDev( + await testUnknownAttributeAssignment(true, null); + assertConsoleErrorDev([ 'Received `true` for a non-boolean attribute `unknown`.\n\n' + 'If you want to write it to the DOM, pass a string instead: ' + 'unknown="true" or unknown={value.toString()}.\n' + ' in div (at **)', - ); + ]); await testUnknownAttributeAssignment(false, null); }); @@ -92,11 +96,9 @@ describe('ReactDOM unknown attribute', () => { const el = document.createElement('div'); const root = ReactDOMClient.createRoot(el); - await expect(async () => { - await act(() => { - root.render(
); - }); - }).toErrorDev([]); + await act(() => { + root.render(
); + }); expect(el.firstChild.getAttribute('inert')).toBe(true ? '' : null); }); @@ -105,15 +107,15 @@ describe('ReactDOM unknown attribute', () => { const el = document.createElement('div'); const root = ReactDOMClient.createRoot(el); - await expect(async () => { - await act(() => { - root.render(
); - }); - }).toErrorDev([ + await act(() => { + root.render(
); + }); + assertConsoleErrorDev([ 'Received an empty string for a boolean attribute `inert`. ' + 'This will treat the attribute as if it were false. ' + 'Either pass `false` to silence this warning, or ' + - 'pass `true` if you used an empty string in earlier versions of React to indicate this attribute is true.', + 'pass `true` if you used an empty string in earlier versions of React to indicate this attribute is true.\n' + + ' in div (at **)', ]); expect(el.firstChild.getAttribute('inert')).toBe(true ? null : ''); @@ -136,11 +138,12 @@ describe('ReactDOM unknown attribute', () => { }); it('coerces NaN to strings and warns', async () => { - await expect(() => testUnknownAttributeAssignment(NaN, 'NaN')).toErrorDev( + await testUnknownAttributeAssignment(NaN, 'NaN'); + assertConsoleErrorDev([ 'Received NaN for the `unknown` attribute. ' + 'If this is expected, cast the value to a string.\n' + ' in div (at **)', - ); + ]); }); it('coerces objects to strings and warns', async () => { @@ -167,52 +170,52 @@ describe('ReactDOM unknown attribute', () => { } const test = () => testUnknownAttributeAssignment(new TemporalLike(), null); - await expect(() => - expect(test).rejects.toThrowError(new TypeError('prod message')), - ).toErrorDev( + + await expect(test).rejects.toThrowError(new TypeError('prod message')); + assertConsoleErrorDev([ 'The provided `unknown` attribute is an unsupported type TemporalLike.' + - ' This value must be coerced to a string before using it here.', - ); + ' This value must be coerced to a string before using it here.\n' + + ' in div (at **)', + ]); }); it('removes symbols and warns', async () => { - await expect(() => testUnknownAttributeRemoval(Symbol('foo'))).toErrorDev( + await testUnknownAttributeRemoval(Symbol('foo')); + assertConsoleErrorDev([ 'Invalid value for prop `unknown` on
tag. Either remove it ' + 'from the element, or pass a string or number value to keep it ' + 'in the DOM. For details, see https://react.dev/link/attribute-behavior \n' + ' in div (at **)', - ); + ]); }); it('removes functions and warns', async () => { - await expect(() => - testUnknownAttributeRemoval(function someFunction() {}), - ).toErrorDev( + await testUnknownAttributeRemoval(function someFunction() {}); + assertConsoleErrorDev([ 'Invalid value for prop `unknown` on
tag. Either remove ' + 'it from the element, or pass a string or number value to ' + 'keep it in the DOM. For details, see ' + 'https://react.dev/link/attribute-behavior \n' + ' in div (at **)', - ); + ]); }); it('allows camelCase unknown attributes and warns', async () => { const el = document.createElement('div'); - await expect(async () => { - const root = ReactDOMClient.createRoot(el); + const root = ReactDOMClient.createRoot(el); - await act(() => { - root.render(
); - }); - }).toErrorDev( + await act(() => { + root.render(
); + }); + assertConsoleErrorDev([ 'React does not recognize the `helloWorld` prop on a DOM element. ' + 'If you intentionally want it to appear in the DOM as a custom ' + 'attribute, spell it as lowercase `helloworld` instead. ' + 'If you accidentally passed it from a parent component, remove ' + 'it from the DOM element.\n' + ' in div (at **)', - ); + ]); expect(el.firstChild.getAttribute('helloworld')).toBe('something'); }); diff --git a/packages/react-dom/src/__tests__/ReactDeprecationWarnings-test.js b/packages/react-dom/src/__tests__/ReactDeprecationWarnings-test.js index e0022dd99004b..a617770cf5f3b 100644 --- a/packages/react-dom/src/__tests__/ReactDeprecationWarnings-test.js +++ b/packages/react-dom/src/__tests__/ReactDeprecationWarnings-test.js @@ -12,6 +12,7 @@ let React; let ReactNoop; let waitForAll; +let assertConsoleErrorDev; describe('ReactDeprecationWarnings', () => { beforeEach(() => { @@ -20,6 +21,7 @@ describe('ReactDeprecationWarnings', () => { ReactNoop = require('react-noop-renderer'); const InternalTestUtils = require('internal-test-utils'); waitForAll = InternalTestUtils.waitForAll; + assertConsoleErrorDev = InternalTestUtils.assertConsoleErrorDev; }); // @gate !disableDefaultPropsExceptForClasses || !__DEV__ @@ -33,11 +35,13 @@ describe('ReactDeprecationWarnings', () => { }; ReactNoop.render(); - await expect(async () => await waitForAll([])).toErrorDev( + await waitForAll([]); + assertConsoleErrorDev([ 'FunctionalComponent: Support for defaultProps ' + 'will be removed from function components in a future major ' + - 'release. Use JavaScript default parameters instead.', - ); + 'release. Use JavaScript default parameters instead.\n' + + ' in FunctionalComponent (at **)', + ]); }); // @gate !disableDefaultPropsExceptForClasses || !__DEV__ @@ -55,10 +59,12 @@ describe('ReactDeprecationWarnings', () => {
, ); - await expect(async () => await waitForAll([])).toErrorDev( + await waitForAll([]); + assertConsoleErrorDev([ 'FunctionalComponent: Support for defaultProps ' + 'will be removed from memo components in a future major ' + - 'release. Use JavaScript default parameters instead.', - ); + 'release. Use JavaScript default parameters instead.\n' + + ' in div (at **)', + ]); }); }); diff --git a/packages/react-dom/src/__tests__/findDOMNodeFB-test.js b/packages/react-dom/src/__tests__/findDOMNodeFB-test.js index 417ae0c40e7f8..28212c0d5cf82 100644 --- a/packages/react-dom/src/__tests__/findDOMNodeFB-test.js +++ b/packages/react-dom/src/__tests__/findDOMNodeFB-test.js @@ -12,6 +12,8 @@ const React = require('react'); const ReactDOM = require('react-dom'); const StrictMode = React.StrictMode; +const assertConsoleErrorDev = + require('internal-test-utils').assertConsoleErrorDev; describe('findDOMNode', () => { // @gate www && classic @@ -128,8 +130,8 @@ describe('findDOMNode', () => { container, ); - let match; - expect(() => (match = ReactDOM.findDOMNode(parent))).toErrorDev([ + const match = ReactDOM.findDOMNode(parent); + assertConsoleErrorDev([ 'findDOMNode is deprecated in StrictMode. ' + 'findDOMNode was passed an instance of ContainsStrictModeChild which renders StrictMode children. ' + 'Instead, add a ref directly to the element you want to reference. ' + @@ -161,8 +163,8 @@ describe('findDOMNode', () => { container, ); - let match; - expect(() => (match = ReactDOM.findDOMNode(parent))).toErrorDev([ + const match = ReactDOM.findDOMNode(parent); + assertConsoleErrorDev([ 'findDOMNode is deprecated in StrictMode. ' + 'findDOMNode was passed an instance of IsInStrictMode which is inside StrictMode. ' + 'Instead, add a ref directly to the element you want to reference. ' + diff --git a/packages/react-dom/src/__tests__/utils/ReactDOMServerIntegrationTestUtils.js b/packages/react-dom/src/__tests__/utils/ReactDOMServerIntegrationTestUtils.js index 76dea3e732632..67d29fd33efdc 100644 --- a/packages/react-dom/src/__tests__/utils/ReactDOMServerIntegrationTestUtils.js +++ b/packages/react-dom/src/__tests__/utils/ReactDOMServerIntegrationTestUtils.js @@ -87,7 +87,7 @@ module.exports = function (initModules) { console.error.mockClear(); } else { // TODO: Rewrite tests that use this helper to enumerate expected errors. - // This will enable the helper to use the .toErrorDev() matcher instead of spying. + // This will enable the helper to use the assertConsoleErrorDev instead of spying. spyOnDev(console, 'error').mockImplementation(() => {}); } From d8a08f8e39972978cd0666f277409a1657083bb5 Mon Sep 17 00:00:00 2001 From: Ricky Date: Thu, 2 Jan 2025 15:28:15 -0500 Subject: [PATCH 0029/1160] [assert helpers] ReactDOMComponent-test (#31898) Splitting out ReactDOMComponent to it's own PR because it's huge. --- .../src/__tests__/ReactDOMComponent-test.js | 2020 +++++++++-------- 1 file changed, 1117 insertions(+), 903 deletions(-) diff --git a/packages/react-dom/src/__tests__/ReactDOMComponent-test.js b/packages/react-dom/src/__tests__/ReactDOMComponent-test.js index ce71a6334ee64..65f82dcd3690a 100644 --- a/packages/react-dom/src/__tests__/ReactDOMComponent-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMComponent-test.js @@ -19,6 +19,7 @@ describe('ReactDOMComponent', () => { let act; let assertLog; let Scheduler; + let assertConsoleErrorDev; beforeEach(() => { jest.resetModules(); @@ -28,6 +29,8 @@ describe('ReactDOMComponent', () => { ReactDOMServer = require('react-dom/server'); Scheduler = require('scheduler'); act = require('internal-test-utils').act; + assertConsoleErrorDev = + require('internal-test-utils').assertConsoleErrorDev; assertLog = require('internal-test-utils').assertLog; }); @@ -189,73 +192,72 @@ describe('ReactDOMComponent', () => { it('should warn for unknown prop', async () => { const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); - await expect(async () => { - await act(() => { - root.render(
{}} />); - }); - }).toErrorDev( + await act(() => { + root.render(
{}} />); + }); + assertConsoleErrorDev([ 'Invalid value for prop `foo` on
tag. Either remove it ' + 'from the element, or pass a string or number value to keep ' + 'it in the DOM. For details, see https://react.dev/link/attribute-behavior ' + '\n in div (at **)', - ); + ]); }); it('should group multiple unknown prop warnings together', async () => { const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); - await expect(async () => { - await act(() => { - root.render(
{}} baz={() => {}} />); - }); - }).toErrorDev( + await act(() => { + root.render(
{}} baz={() => {}} />); + }); + assertConsoleErrorDev([ 'Invalid values for props `foo`, `baz` on
tag. Either remove ' + 'them from the element, or pass a string or number value to keep ' + 'them in the DOM. For details, see https://react.dev/link/attribute-behavior ' + '\n in div (at **)', - ); + ]); }); it('should warn for onDblClick prop', async () => { const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); - await expect(async () => { - await act(() => { - root.render(
{}} />); - }); - }).toErrorDev( - 'Invalid event handler property `onDblClick`. Did you mean `onDoubleClick`?\n in div (at **)', - ); + await act(() => { + root.render(
{}} />); + }); + assertConsoleErrorDev([ + 'Invalid event handler property `onDblClick`. Did you mean `onDoubleClick`?\n' + + ' in div (at **)', + ]); }); it('should warn for unknown string event handlers', async () => { const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); - await expect(async () => { - await act(() => { - root.render(
); - }); - }).toErrorDev( - 'Unknown event handler property `onUnknown`. It will be ignored.\n in div (at **)', - ); + await act(() => { + root.render(
); + }); + assertConsoleErrorDev([ + 'Unknown event handler property `onUnknown`. It will be ignored.\n' + + ' in div (at **)', + ]); expect(container.firstChild.hasAttribute('onUnknown')).toBe(false); expect(container.firstChild.onUnknown).toBe(undefined); - await expect(async () => { - await act(() => { - root.render(
); - }); - }).toErrorDev( - 'Unknown event handler property `onunknown`. It will be ignored.\n in div (at **)', - ); + await act(() => { + root.render(
); + }); + assertConsoleErrorDev([ + 'Unknown event handler property `onunknown`. It will be ignored.\n' + + ' in div (at **)', + ]); expect(container.firstChild.hasAttribute('onunknown')).toBe(false); expect(container.firstChild.onunknown).toBe(undefined); - await expect(async () => { - await act(() => { - root.render(
); - }); - }).toErrorDev( - 'Unknown event handler property `on-unknown`. It will be ignored.\n in div (at **)', - ); + + await act(() => { + root.render(
); + }); + assertConsoleErrorDev([ + 'Unknown event handler property `on-unknown`. It will be ignored.\n' + + ' in div (at **)', + ]); expect(container.firstChild.hasAttribute('on-unknown')).toBe(false); expect(container.firstChild['on-unknown']).toBe(undefined); }); @@ -263,31 +265,31 @@ describe('ReactDOMComponent', () => { it('should warn for unknown function event handlers', async () => { const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); - await expect(async () => { - await act(() => { - root.render(
); - }); - }).toErrorDev( - 'Unknown event handler property `onUnknown`. It will be ignored.\n in div (at **)', - ); + await act(() => { + root.render(
); + }); + assertConsoleErrorDev([ + 'Unknown event handler property `onUnknown`. It will be ignored.\n' + + ' in div (at **)', + ]); expect(container.firstChild.hasAttribute('onUnknown')).toBe(false); expect(container.firstChild.onUnknown).toBe(undefined); - await expect(async () => { - await act(() => { - root.render(
); - }); - }).toErrorDev( - 'Unknown event handler property `onunknown`. It will be ignored.\n in div (at **)', - ); + await act(() => { + root.render(
); + }); + assertConsoleErrorDev([ + 'Unknown event handler property `onunknown`. It will be ignored.\n' + + ' in div (at **)', + ]); expect(container.firstChild.hasAttribute('onunknown')).toBe(false); expect(container.firstChild.onunknown).toBe(undefined); - await expect(async () => { - await act(() => { - root.render(
); - }); - }).toErrorDev( - 'Unknown event handler property `on-unknown`. It will be ignored.\n in div (at **)', - ); + await act(() => { + root.render(
); + }); + assertConsoleErrorDev([ + 'Unknown event handler property `on-unknown`. It will be ignored.\n' + + ' in div (at **)', + ]); expect(container.firstChild.hasAttribute('on-unknown')).toBe(false); expect(container.firstChild['on-unknown']).toBe(undefined); }); @@ -295,13 +297,13 @@ describe('ReactDOMComponent', () => { it('should warn for badly cased React attributes', async () => { const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); - await expect(async () => { - await act(() => { - root.render(
); - }); - }).toErrorDev( - 'Invalid DOM property `CHILDREN`. Did you mean `children`?\n in div (at **)', - ); + await act(() => { + root.render(
); + }); + assertConsoleErrorDev([ + 'Invalid DOM property `CHILDREN`. Did you mean `children`?\n' + + ' in div (at **)', + ]); expect(container.firstChild.getAttribute('CHILDREN')).toBe('5'); }); @@ -323,14 +325,13 @@ describe('ReactDOMComponent', () => { const style = {fontSize: NaN}; const div = document.createElement('div'); const root = ReactDOMClient.createRoot(div); - await expect(async () => { - await act(() => { - root.render(); - }); - }).toErrorDev( - '`NaN` is an invalid value for the `fontSize` css style property.' + - '\n in span (at **)', - ); + await act(() => { + root.render(); + }); + assertConsoleErrorDev([ + '`NaN` is an invalid value for the `fontSize` css style property.\n' + + ' in span (at **)', + ]); await act(() => { root.render(); }); @@ -350,15 +351,18 @@ describe('ReactDOMComponent', () => { const style = {fontSize: new TemporalLike()}; const root = ReactDOMClient.createRoot(document.createElement('div')); await expect(async () => { - await expect(async () => { - await act(() => { - root.render(); - }); - }).toErrorDev( - 'The provided `fontSize` CSS property is an unsupported type TemporalLike.' + - ' This value must be coerced to a string before using it here.', - ); + await act(() => { + root.render(); + }); }).rejects.toThrowError(new TypeError('prod message')); + assertConsoleErrorDev([ + 'The provided `fontSize` CSS property is an unsupported type TemporalLike.' + + ' This value must be coerced to a string before using it here.\n' + + ' in span (at **)', + 'The provided `fontSize` CSS property is an unsupported type TemporalLike.' + + ' This value must be coerced to a string before using it here.\n' + + ' in span (at **)', + ]); }); it('should update styles if initially null', async () => { @@ -590,16 +594,16 @@ describe('ReactDOMComponent', () => { it('should not add an empty src attribute', async () => { const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); - await expect(async () => { - await act(() => { - root.render(); - }); - }).toErrorDev( + await act(() => { + root.render(); + }); + assertConsoleErrorDev([ 'An empty string ("") was passed to the src attribute. ' + 'This may cause the browser to download the whole page again over the network. ' + 'To fix this, either do not render the element at all ' + - 'or pass null to src instead of an empty string.', - ); + 'or pass null to src instead of an empty string.\n' + + ' in img (at **)', + ]); const node = container.firstChild; expect(node.hasAttribute('src')).toBe(false); @@ -608,31 +612,31 @@ describe('ReactDOMComponent', () => { }); expect(node.hasAttribute('src')).toBe(true); - await expect(async () => { - await act(() => { - root.render(); - }); - }).toErrorDev( + await act(() => { + root.render(); + }); + assertConsoleErrorDev([ 'An empty string ("") was passed to the src attribute. ' + 'This may cause the browser to download the whole page again over the network. ' + 'To fix this, either do not render the element at all ' + - 'or pass null to src instead of an empty string.', - ); + 'or pass null to src instead of an empty string.\n' + + ' in img (at **)', + ]); expect(node.hasAttribute('src')).toBe(false); }); it('should not add an empty href attribute', async () => { const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); - await expect(async () => { - await act(() => { - root.render(); - }); - }).toErrorDev( + await act(() => { + root.render(); + }); + assertConsoleErrorDev([ 'An empty string ("") was passed to the href attribute. ' + 'To fix this, either do not render the element at all ' + - 'or pass null to href instead of an empty string.', - ); + 'or pass null to href instead of an empty string.\n' + + ' in link (at **)', + ]); const node = container.firstChild; expect(node.hasAttribute('href')).toBe(false); @@ -641,15 +645,15 @@ describe('ReactDOMComponent', () => { }); expect(node.hasAttribute('href')).toBe(true); - await expect(async () => { - await act(() => { - root.render(); - }); - }).toErrorDev( + await act(() => { + root.render(); + }); + assertConsoleErrorDev([ 'An empty string ("") was passed to the href attribute. ' + 'To fix this, either do not render the element at all ' + - 'or pass null to href instead of an empty string.', - ); + 'or pass null to href instead of an empty string.\n' + + ' in link (at **)', + ]); expect(node.hasAttribute('href')).toBe(false); }); @@ -871,204 +875,235 @@ describe('ReactDOMComponent', () => { }); it('should reject attribute key injection attack on markup for regular DOM (SSR)', () => { - expect(() => { - for (let i = 0; i < 3; i++) { - const element1 = React.createElement( - 'div', - {'blah" onclick="beevil" noise="hi': 'selected'}, - null, - ); - const element2 = React.createElement( - 'div', - {'>
': 'selected'}, - null, - ); - const result1 = ReactDOMServer.renderToString(element1); - const result2 = ReactDOMServer.renderToString(element2); - expect(result1.toLowerCase()).not.toContain('onclick'); - expect(result2.toLowerCase()).not.toContain('script'); - } - }).toErrorDev([ - 'Invalid attribute name: `blah" onclick="beevil" noise="hi`', - 'Invalid attribute name: `>
`', + for (let i = 0; i < 3; i++) { + const element1 = React.createElement( + 'div', + {'blah" onclick="beevil" noise="hi': 'selected'}, + null, + ); + const element2 = React.createElement( + 'div', + {'>
': 'selected'}, + null, + ); + const result1 = ReactDOMServer.renderToString(element1); + const result2 = ReactDOMServer.renderToString(element2); + expect(result1.toLowerCase()).not.toContain('onclick'); + expect(result2.toLowerCase()).not.toContain('script'); + } + assertConsoleErrorDev([ + 'Invalid attribute name: `blah" onclick="beevil" noise="hi`\n' + + ' in div (at **)', + 'Invalid attribute name: `>
`\n' + + ' in div (at **)', ]); }); it('should reject attribute key injection attack on markup for custom elements (SSR)', () => { - expect(() => { - for (let i = 0; i < 3; i++) { - const element1 = React.createElement( - 'x-foo-component', - {'blah" onclick="beevil" noise="hi': 'selected'}, - null, - ); - const element2 = React.createElement( - 'x-foo-component', - {'>': 'selected'}, - null, - ); - const result1 = ReactDOMServer.renderToString(element1); - const result2 = ReactDOMServer.renderToString(element2); - expect(result1.toLowerCase()).not.toContain('onclick'); - expect(result2.toLowerCase()).not.toContain('script'); - } - }).toErrorDev([ - 'Invalid attribute name: `blah" onclick="beevil" noise="hi`', - 'Invalid attribute name: `>`', + for (let i = 0; i < 3; i++) { + const element1 = React.createElement( + 'x-foo-component', + {'blah" onclick="beevil" noise="hi': 'selected'}, + null, + ); + const element2 = React.createElement( + 'x-foo-component', + {'>': 'selected'}, + null, + ); + const result1 = ReactDOMServer.renderToString(element1); + const result2 = ReactDOMServer.renderToString(element2); + expect(result1.toLowerCase()).not.toContain('onclick'); + expect(result2.toLowerCase()).not.toContain('script'); + } + assertConsoleErrorDev([ + 'Invalid attribute name: `blah" onclick="beevil" noise="hi`\n' + + ' in x-foo-component (at **)', + 'Invalid attribute name: `>`\n' + + ' in x-foo-component (at **)', ]); }); it('should reject attribute key injection attack on mount for regular DOM', async () => { - await expect(async () => { - for (let i = 0; i < 3; i++) { - const container = document.createElement('div'); - let root = ReactDOMClient.createRoot(container); - await act(() => { - root.render( - React.createElement( - 'div', - {'blah" onclick="beevil" noise="hi': 'selected'}, - null, - ), - ); - }); - - expect(container.firstChild.attributes.length).toBe(0); - await act(() => { - root.unmount(); - }); - root = ReactDOMClient.createRoot(container); - await act(() => { - root.render( - React.createElement( - 'div', - {'>
': 'selected'}, - null, - ), - ); - }); - - expect(container.firstChild.attributes.length).toBe(0); + for (let i = 0; i < 3; i++) { + const container = document.createElement('div'); + let root = ReactDOMClient.createRoot(container); + await act(() => { + root.render( + React.createElement( + 'div', + {'blah" onclick="beevil" noise="hi': 'selected'}, + null, + ), + ); + }); + + expect(container.firstChild.attributes.length).toBe(0); + if (i === 0) { + assertConsoleErrorDev([ + 'Invalid attribute name: `blah" onclick="beevil" noise="hi`\n' + + ' in div (at **)', + ]); } - }).toErrorDev([ - 'Invalid attribute name: `blah" onclick="beevil" noise="hi`', - 'Invalid attribute name: `>
`', - ]); + await act(() => { + root.unmount(); + }); + root = ReactDOMClient.createRoot(container); + await act(() => { + root.render( + React.createElement( + 'div', + {'>
': 'selected'}, + null, + ), + ); + }); + if (i === 0) { + assertConsoleErrorDev([ + 'Invalid attribute name: `>
`\n' + + ' in div (at **)', + ]); + } + + expect(container.firstChild.attributes.length).toBe(0); + } }); it('should reject attribute key injection attack on mount for custom elements', async () => { - await expect(async () => { - for (let i = 0; i < 3; i++) { - const container = document.createElement('div'); - let root = ReactDOMClient.createRoot(container); - - await act(() => { - root.render( - React.createElement( - 'x-foo-component', - {'blah" onclick="beevil" noise="hi': 'selected'}, - null, - ), - ); - }); - - expect(container.firstChild.attributes.length).toBe(0); - await act(() => { - root.unmount(); - }); - root = ReactDOMClient.createRoot(container); - await act(() => { - root.render( - React.createElement( - 'x-foo-component', - {'>': 'selected'}, - null, - ), - ); - }); - - expect(container.firstChild.attributes.length).toBe(0); + for (let i = 0; i < 3; i++) { + const container = document.createElement('div'); + let root = ReactDOMClient.createRoot(container); + + await act(() => { + root.render( + React.createElement( + 'x-foo-component', + {'blah" onclick="beevil" noise="hi': 'selected'}, + null, + ), + ); + }); + + if (i === 0) { + assertConsoleErrorDev([ + 'Invalid attribute name: `blah" onclick="beevil" noise="hi`\n' + + ' in x-foo-component (at **)', + ]); } - }).toErrorDev([ - 'Invalid attribute name: `blah" onclick="beevil" noise="hi`', - 'Invalid attribute name: `>`', - ]); + expect(container.firstChild.attributes.length).toBe(0); + await act(() => { + root.unmount(); + }); + + root = ReactDOMClient.createRoot(container); + await act(() => { + root.render( + React.createElement( + 'x-foo-component', + {'>': 'selected'}, + null, + ), + ); + }); + + if (i === 0) { + assertConsoleErrorDev([ + 'Invalid attribute name: `>`\n' + + ' in x-foo-component (at **)', + ]); + } + expect(container.firstChild.attributes.length).toBe(0); + } }); it('should reject attribute key injection attack on update for regular DOM', async () => { - await expect(async () => { - for (let i = 0; i < 3; i++) { - const container = document.createElement('div'); - const beforeUpdate = React.createElement('div', {}, null); - const root = ReactDOMClient.createRoot(container); - await act(() => { - root.render(beforeUpdate); - }); - await act(() => { - root.render( - React.createElement( - 'div', - {'blah" onclick="beevil" noise="hi': 'selected'}, - null, - ), - ); - }); - - expect(container.firstChild.attributes.length).toBe(0); - await act(() => { - root.render( - React.createElement( - 'div', - {'>
': 'selected'}, - null, - ), - ); - }); - - expect(container.firstChild.attributes.length).toBe(0); + for (let i = 0; i < 3; i++) { + const container = document.createElement('div'); + const beforeUpdate = React.createElement('div', {}, null); + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render(beforeUpdate); + }); + await act(() => { + root.render( + React.createElement( + 'div', + {'blah" onclick="beevil" noise="hi': 'selected'}, + null, + ), + ); + }); + + if (i === 0) { + assertConsoleErrorDev([ + 'Invalid attribute name: `blah" onclick="beevil" noise="hi`\n' + + ' in div (at **)', + ]); } - }).toErrorDev([ - 'Invalid attribute name: `blah" onclick="beevil" noise="hi`', - 'Invalid attribute name: `>
`', - ]); + expect(container.firstChild.attributes.length).toBe(0); + await act(() => { + root.render( + React.createElement( + 'div', + {'>
': 'selected'}, + null, + ), + ); + }); + if (i === 0) { + assertConsoleErrorDev([ + 'Invalid attribute name: `>
`\n' + + ' in div (at **)', + ]); + } + + expect(container.firstChild.attributes.length).toBe(0); + } }); it('should reject attribute key injection attack on update for custom elements', async () => { - await expect(async () => { - for (let i = 0; i < 3; i++) { - const container = document.createElement('div'); - const beforeUpdate = React.createElement('x-foo-component', {}, null); - const root = ReactDOMClient.createRoot(container); - await act(() => { - root.render(beforeUpdate); - }); - await act(() => { - root.render( - React.createElement( - 'x-foo-component', - {'blah" onclick="beevil" noise="hi': 'selected'}, - null, - ), - ); - }); - - expect(container.firstChild.attributes.length).toBe(0); - await act(() => { - root.render( - React.createElement( - 'x-foo-component', - {'>': 'selected'}, - null, - ), - ); - }); - - expect(container.firstChild.attributes.length).toBe(0); + for (let i = 0; i < 3; i++) { + const container = document.createElement('div'); + const beforeUpdate = React.createElement('x-foo-component', {}, null); + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render(beforeUpdate); + }); + await act(() => { + root.render( + React.createElement( + 'x-foo-component', + {'blah" onclick="beevil" noise="hi': 'selected'}, + null, + ), + ); + }); + + if (i === 0) { + assertConsoleErrorDev([ + 'Invalid attribute name: `blah" onclick="beevil" noise="hi`\n' + + ' in x-foo-component (at **)', + ]); } - }).toErrorDev([ - 'Invalid attribute name: `blah" onclick="beevil" noise="hi`', - 'Invalid attribute name: `>`', - ]); + expect(container.firstChild.attributes.length).toBe(0); + await act(() => { + root.render( + React.createElement( + 'x-foo-component', + {'>': 'selected'}, + null, + ), + ); + }); + + if (i === 0) { + assertConsoleErrorDev([ + 'Invalid attribute name: `>`\n' + + ' in x-foo-component (at **)', + ]); + } + expect(container.firstChild.attributes.length).toBe(0); + } }); it('should update arbitrary attributes for tags containing dashes', async () => { @@ -1382,36 +1417,38 @@ describe('ReactDOMComponent', () => { }); expect(nodeValueSetter).toHaveBeenCalledTimes(1); - await expect(async () => { - await act(() => { - root.render(); - }); - }).toErrorDev( + await act(() => { + root.render(); + }); + assertConsoleErrorDev([ 'A component is changing a controlled input to be uncontrolled. This is likely caused by ' + 'the value changing from a defined to undefined, which should not happen. Decide between ' + - 'using a controlled or uncontrolled input element for the lifetime of the component.', - ); + 'using a controlled or uncontrolled input element for the lifetime of the component. ' + + 'More info: https://react.dev/link/controlled-components\n' + + ' in input (at **)', + ]); expect(nodeValueSetter).toHaveBeenCalledTimes(1); - await expect(async () => { - await act(() => { - root.render(); - }); - }).toErrorDev( - 'value` prop on `input` should not be null. Consider using an empty string to clear the ' + - 'component or `undefined` for uncontrolled components.', - ); + await act(() => { + root.render(); + }); + assertConsoleErrorDev([ + '`value` prop on `input` should not be null. Consider using an empty string to clear the ' + + 'component or `undefined` for uncontrolled components.\n' + + ' in input (at **)', + ]); expect(nodeValueSetter).toHaveBeenCalledTimes(1); - await expect(async () => { - await act(() => { - root.render(); - }); - }).toErrorDev( + await act(() => { + root.render(); + }); + assertConsoleErrorDev([ 'A component is changing an uncontrolled input to be controlled. This is likely caused by ' + 'the value changing from undefined to a defined value, which should not happen. Decide between ' + - 'using a controlled or uncontrolled input element for the lifetime of the component.', - ); + 'using a controlled or uncontrolled input element for the lifetime of the component. ' + + 'More info: https://react.dev/link/controlled-components\n' + + ' in input (at **)', + ]); expect(nodeValueSetter).toHaveBeenCalledTimes(2); await act(() => { @@ -1462,14 +1499,14 @@ describe('ReactDOMComponent', () => { it('should warn about non-string "is" attribute', async () => { const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); - await expect(async () => { - await act(() => { - root.render(