From 94867f33be327a52bfffda89a14c85897180e43e Mon Sep 17 00:00:00 2001 From: Ricky Date: Mon, 23 Dec 2024 14:58:20 -0500 Subject: [PATCH 01/61] [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 | 323 ++++++------ .../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 | 163 +++--- .../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, 1434 insertions(+), 1016 deletions(-) diff --git a/packages/react-reconciler/src/__tests__/ReactLazy-test.internal.js b/packages/react-reconciler/src/__tests__/ReactLazy-test.internal.js index ddcd751957..1b665b2b8b 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 3ad6aa0235..5fab9d7d0b 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 359857af15..66597f0dd5 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.', - ]); - - // Warnings should be deduped by component type - let container = document.createElement('div'); - let root = ReactDOMClient.createRoot(container); + 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 **)', + ]); - 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.', + // Warnings should be deduped by component type + await act(() => { + root.render(); + }); + 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,7 @@ 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.', - ]); - - // Warnings should be deduped by component type - let container = document.createElement('div'); - let root = ReactDOMClient.createRoot(container); + const root = ReactDOMClient.createRoot(document.createElement('div')); await act(() => { root.render( @@ -455,19 +465,41 @@ 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.', + 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 + await act(() => { + root.render( + + + , + ); + }); + + 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 392f89979e..44952536ac 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 616c62e00c..bf7166d8cf 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 eceeab18c5..640ae7ef37 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 fb0bfe2df8..b2e791f826 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 4383a6472d..1191aa7ec2 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 12e54e5c96..d4e78f1b1c 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); + 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.'); + 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); + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); - await act(() => { - root.render(); - }); - }).toErrorDev( + 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 e3de4dbf5e..663c935caf 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 f0caf6b494..5075c4a9a0 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 d8fc623a3d..20395118c5 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 9efdf183ee..c54945ff25 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 9e689ac6e7..00387dbb26 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 f28c70a871..6d44a28881 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 00e3a8b3ff..5f51cc6f38 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 4aabc11b52..6bd3f06921 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 487e0d3003..c3eaf5690a 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 02/61] [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 6c7f60c2e2..4a26e272cb 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 6e275241e9..0000000000 --- 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 03/61] [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 7c1b55f61d..91c5523564 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 04/61] [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 575ec847a5..d72f168c56 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 05/61] 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 7555510475..f08f7a110b 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 f6457ce570..bac5be6ec6 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 06/61] [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 30d340ac56..7c424d45b3 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 d2860e407c..123878526d 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 07/61] 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 7195c19492..bdb2c0441b 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 08/61] [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 | 817 +++++++++--------- ...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, 482 insertions(+), 438 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 c991e46bfe..d2abb05a8f 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 eaf367216e..810fbb628a 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 93c632db7b..ab7748dc8c 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 326acb6a22..384a25dd74 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== +"@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.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/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== +"@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.1789+0b82b13d6" + "@parcel/plugin" "2.0.0-dev.1795+9f297b15c" -"@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== +"@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.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/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.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/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/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.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/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.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== +"@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.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/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== +"@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.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/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.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== +"@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.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" -"@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== +"@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.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" "@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== +"@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.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/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== +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.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/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 55be327967..89b834784f 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 3c86f9ab07..626f26903e 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 1ed0f6825f..466d21164c 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 f99eb85acb..128ef9f5e1 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 0000000000..31346b2255 --- /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 0000000000..c4ebcde941 --- /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 46cb46cadc..1d4e6dbd9c 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 09/61] 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 ff07f0ef16..a80a11fde1 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 d754140f96..699d230945 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 10/61] [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 c05e0e70d3..b25031b996 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 77e785c464..b95cc89b39 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 4d81356409..743e4cc844 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 117974ad6b..0c2492d140 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 260359dd0c..2399f2160c 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 b4c72ff3c5..21f771c93f 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 11/61] [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 2e8b443d22..8e75135ff6 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 0720ab2a88..f1332a2bfd 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 4ea9499e6c..ed06661eed 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 dddd80aeea..2220891b7b 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 95826007dc..2c5bc8f297 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 81060cfafb..1c2fbb4847 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 e0e9906d52..8e96038f44 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 cf514f93d4..658443aea1 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 12/61] [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 61bfd5cf7f..6e0ebc4e98 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 dcaadc5a6e..e3791f4c8d 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 8e75135ff6..133c91f518 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 d408ba0ff7..d3bdf6f6a9 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 13/61] [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 43b726e981..a244c65e3e 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 6e0ebc4e98..ae48ed3834 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,18 @@ 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++) { @@ -182,14 +189,60 @@ 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: + 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++) { + 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: '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 133c91f518..ded26ed6e2 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 d3bdf6f6a9..bc289bcab6 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 14/61] [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 66e9249f15..0fef260ab2 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 15/61] [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 a244c65e3e..80a27c4502 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 ded26ed6e2..26544788f1 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 16/61] [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 | 308 ++++++++++-------- .../src/ReactInternalTypes.js | 4 - .../src/__tests__/ReactDeferredValue-test.js | 1 + 6 files changed, 181 insertions(+), 161 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberLane.js b/packages/react-reconciler/src/ReactFiberLane.js index 4c96207bd6..71a1e973c3 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 29bc7923d8..4971bb4c2b 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 e3791f4c8d..d57458fdbf 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 26544788f1..ce253f5436 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,78 +3307,135 @@ 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; - - // 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); - - // The next phase is the mutation phase, where we mutate the host tree. - commitMutationEffects(root, finishedWork, lanes); - - if (enableCreateEventHandleAPI) { - if (shouldFireAfterActiveInstanceBlur) { - afterActiveInstanceBlur(); - } + 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; } - 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(); - - executionContext = prevExecutionContext; - - // Reset the priority to the previous non-sync value. - setCurrentUpdatePriority(previousPriority); - ReactSharedInternals.T = prevTransition; - } else { - // No effects. - root.current = finishedWork; } + flushMutationEffects(root, finishedWork, lanes); + flushLayoutEffects( + root, + finishedWork, + lanes, + recoverableErrors, + didIncludeRenderPhaseUpdate, + suspendedCommitReason, + completedRenderEndTime, + ); +} + +function flushMutationEffects( + root: FiberRoot, + finishedWork: Fiber, + lanes: Lanes, +): void { + const subtreeMutationHasEffects = + (finishedWork.subtreeFlags & MutationMask) !== NoFlags; + const rootMutationHasEffect = (finishedWork.flags & MutationMask) !== NoFlags; + + 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(); + } + } + resetAfterCommit(root.containerInfo); + } finally { + // Reset the priority to the previous non-sync value. + executionContext = prevExecutionContext; + setCurrentUpdatePriority(previousPriority); + ReactSharedInternals.T = prevTransition; + } + } + + // 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; +} + +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; + + 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(); @@ -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 afe853202f..859da37cad 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 bbd01cd551..f57fc0ac02 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 17/61] [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 bda95b8434..b5671cf759 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 cecc71b45a..ef09e49bf3 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 d7045d2756..8bff125c82 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 1c1475534f..ba0c028fa8 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 6459114697..c59ba61e01 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 428dac1f1e..75e4cebe80 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 30b5ced5cf..3d5f61c1ed 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 a651a477a8..d0d1c36e51 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 c6c90d1cf1..92571ad69e 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 9b7d9a30cb..cd1d055c09 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 e0022dd990..a617770cf5 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 417ae0c40e..28212c0d5c 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 76dea3e732..67d29fd33e 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 18/61] [assert helpers] ReactDOMComponent-test (#31898) Splitting out ReactDOMComponent to it's own PR because it's huge. --- .../src/__tests__/ReactDOMComponent-test.js | 2006 +++++++++-------- 1 file changed, 1110 insertions(+), 896 deletions(-) diff --git a/packages/react-dom/src/__tests__/ReactDOMComponent-test.js b/packages/react-dom/src/__tests__/ReactDOMComponent-test.js index ce71a6334e..65f82dcd36 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, - ), - ); - }); + 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); + 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); + 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, - ), - ); - }); + 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); + 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, - ), - ); - }); + 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); + 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, - ), - ); - }); + 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); + 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(