diff --git a/compiler/packages/babel-plugin-react-compiler/src/CompilerError.ts b/compiler/packages/babel-plugin-react-compiler/src/CompilerError.ts index 5ea6f98628..3a3010dd2f 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/CompilerError.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/CompilerError.ts @@ -188,6 +188,7 @@ export class CompilerError extends Error { constructor(...args: Array) { super(...args); this.name = 'ReactCompilerError'; + this.details = []; } override get message(): string { @@ -197,7 +198,10 @@ export class CompilerError extends Error { override set message(_message: string) {} override toString(): string { - return this.details.map(detail => detail.toString()).join('\n\n'); + if (Array.isArray(this.details)) { + return this.details.map(detail => detail.toString()).join('\n\n'); + } + return this.name; } push(options: CompilerErrorDetailOptions): CompilerErrorDetail { diff --git a/packages/react-client/src/ReactFlightClient.js b/packages/react-client/src/ReactFlightClient.js index 0eaf513a67..3234814952 100644 --- a/packages/react-client/src/ReactFlightClient.js +++ b/packages/react-client/src/ReactFlightClient.js @@ -64,7 +64,10 @@ import { rendererPackageName, } from './ReactFlightClientConfig'; -import {createBoundServerReference} from './ReactFlightReplyClient'; +import { + createBoundServerReference, + registerBoundServerReference, +} from './ReactFlightReplyClient'; import {readTemporaryReference} from './ReactFlightTemporaryReferences'; @@ -1096,7 +1099,14 @@ function loadServerReference, T>( let promise: null | Thenable = preloadModule(serverReference); if (!promise) { if (!metaData.bound) { - return (requireModule(serverReference): any); + const resolvedValue = (requireModule(serverReference): any); + registerBoundServerReference( + resolvedValue, + metaData.id, + metaData.bound, + response._encodeFormAction, + ); + return resolvedValue; } else { promise = Promise.resolve(metaData.bound); } @@ -1128,6 +1138,13 @@ function loadServerReference, T>( resolvedValue = resolvedValue.bind.apply(resolvedValue, boundArgs); } + registerBoundServerReference( + resolvedValue, + metaData.id, + metaData.bound, + response._encodeFormAction, + ); + parentObject[key] = resolvedValue; // If this is the root object for a model reference, where `handler.value` diff --git a/packages/react-client/src/ReactFlightReplyClient.js b/packages/react-client/src/ReactFlightReplyClient.js index 65d1129b53..3fa37cd00c 100644 --- a/packages/react-client/src/ReactFlightReplyClient.js +++ b/packages/react-client/src/ReactFlightReplyClient.js @@ -1125,11 +1125,12 @@ function createFakeServerFunction, T>( } } -function registerServerReference( - proxy: any, - reference: {id: ServerReferenceId, bound: null | Thenable>}, +export function registerBoundServerReference( + reference: T, + id: ServerReferenceId, + bound: null | Thenable>, encodeFormAction: void | EncodeFormActionCallback, -) { +): void { // Expose encoder for use by SSR, as well as a special bind that can be used to // keep server capabilities. if (usedWithSSR) { @@ -1147,13 +1148,22 @@ function registerServerReference( encodeFormAction, ); }; - Object.defineProperties((proxy: any), { + Object.defineProperties((reference: any), { $$FORM_ACTION: {value: $$FORM_ACTION}, $$IS_SIGNATURE_EQUAL: {value: isSignatureEqual}, bind: {value: bind}, }); } - knownServerReferences.set(proxy, reference); + knownServerReferences.set(reference, {id, bound}); +} + +export function registerServerReference( + reference: T, + id: ServerReferenceId, + encodeFormAction?: EncodeFormActionCallback, +): ServerReference { + registerBoundServerReference(reference, id, null, encodeFormAction); + return reference; } // $FlowFixMe[method-unbinding] @@ -1258,7 +1268,7 @@ export function createBoundServerReference, T>( ); } } - registerServerReference(action, {id, bound}, encodeFormAction); + registerBoundServerReference(action, id, bound, encodeFormAction); return action; } @@ -1358,6 +1368,6 @@ export function createServerReference, T>( ); } } - registerServerReference(action, {id, bound: null}, encodeFormAction); + registerBoundServerReference(action, id, null, encodeFormAction); return action; } diff --git a/packages/react-dom-bindings/src/client/ReactDOMComponent.js b/packages/react-dom-bindings/src/client/ReactDOMComponent.js index efdfc66ff2..48b4d9472f 100644 --- a/packages/react-dom-bindings/src/client/ReactDOMComponent.js +++ b/packages/react-dom-bindings/src/client/ReactDOMComponent.js @@ -1280,6 +1280,8 @@ export function setInitialProperties( return; } case 'dialog': { + listenToNonDelegatedEvent('beforetoggle', domElement); + listenToNonDelegatedEvent('toggle', domElement); listenToNonDelegatedEvent('cancel', domElement); listenToNonDelegatedEvent('close', domElement); break; diff --git a/packages/react-dom/src/__tests__/ReactDOMEventPropagation-test.js b/packages/react-dom/src/__tests__/ReactDOMEventPropagation-test.js index 1598329340..ebd3f9a540 100644 --- a/packages/react-dom/src/__tests__/ReactDOMEventPropagation-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMEventPropagation-test.js @@ -1302,6 +1302,38 @@ describe('ReactDOMEventListener', () => { }); }); + it('onBeforeToggle Dialog API', async () => { + await testEmulatedBubblingEvent({ + type: 'dialog', + reactEvent: 'onBeforeToggle', + reactEventType: 'beforetoggle', + nativeEvent: 'beforetoggle', + dispatch(node) { + const e = new Event('beforetoggle', { + bubbles: false, + cancelable: true, + }); + node.dispatchEvent(e); + }, + }); + }); + + it('onToggle Dialog API', async () => { + await testEmulatedBubblingEvent({ + type: 'dialog', + reactEvent: 'onToggle', + reactEventType: 'toggle', + nativeEvent: 'toggle', + dispatch(node) { + const e = new Event('toggle', { + bubbles: false, + cancelable: true, + }); + node.dispatchEvent(e); + }, + }); + }); + it('onVolumeChange', async () => { await testEmulatedBubblingEvent({ type: 'video', diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzForm-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzForm-test.js index f64cdd8bb8..c7f52b1c68 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFizzForm-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFizzForm-test.js @@ -201,25 +201,24 @@ describe('ReactDOMFizzForm', () => { await act(async () => { ReactDOMClient.hydrateRoot(container, ); }); - assertConsoleErrorDev( - [ - "A tree hydrated but some attributes of the server rendered HTML didn't match the client properties. " + - "This won't be patched up. This can happen if a SSR-ed Client Component used:\n\n" + - "- A server/client branch `if (typeof window !== 'undefined')`.\n" + - "- Variable input such as `Date.now()` or `Math.random()` which changes each time it's called.\n" + - "- Date formatting in a user's locale which doesn't match the server.\n" + - '- External changing data without sending a snapshot of it along with the HTML.\n' + - '- Invalid HTML tag nesting.\n\n' + - 'It can also happen if the client has a browser extension installed which messes with the HTML before React loaded.\n\n' + - 'https://react.dev/link/hydration-mismatch\n\n' + - ' \n' + - ' \n', - ], - {withoutStack: true}, - ); + assertConsoleErrorDev([ + "A tree hydrated but some attributes of the server rendered HTML didn't match the client properties. " + + "This won't be patched up. This can happen if a SSR-ed Client Component used:\n\n" + + "- A server/client branch `if (typeof window !== 'undefined')`.\n" + + "- Variable input such as `Date.now()` or `Math.random()` which changes each time it's called.\n" + + "- Date formatting in a user's locale which doesn't match the server.\n" + + '- External changing data without sending a snapshot of it along with the HTML.\n' + + '- Invalid HTML tag nesting.\n\n' + + 'It can also happen if the client has a browser extension installed which messes with the HTML before React loaded.\n\n' + + 'https://react.dev/link/hydration-mismatch\n\n' + + ' \n' + + ' \n' + + '\n in form (at **)' + + '\n in App (at **)', + ]); }); it('should ideally warn when passing a string during SSR and function during hydration', async () => { @@ -392,40 +391,39 @@ describe('ReactDOMFizzForm', () => { await act(async () => { root = ReactDOMClient.hydrateRoot(container, ); }); - assertConsoleErrorDev( - [ - "A tree hydrated but some attributes of the server rendered HTML didn't match the client properties. " + - "This won't be patched up. This can happen if a SSR-ed Client Component used:\n\n" + - "- A server/client branch `if (typeof window !== 'undefined')`.\n" + - "- Variable input such as `Date.now()` or `Math.random()` which changes each time it's called.\n" + - "- Date formatting in a user's locale which doesn't match the server.\n" + - '- External changing data without sending a snapshot of it along with the HTML.\n' + - '- Invalid HTML tag nesting.\n\n' + - 'It can also happen if the client has a browser extension installed which messes with the HTML before React loaded.\n\n' + - 'https://react.dev/link/hydration-mismatch\n\n' + - ' \n' + - ' \n' + - ' \n' + - ' \n', - ], - {withoutStack: true}, - ); + assertConsoleErrorDev([ + "A tree hydrated but some attributes of the server rendered HTML didn't match the client properties. " + + "This won't be patched up. This can happen if a SSR-ed Client Component used:\n\n" + + "- A server/client branch `if (typeof window !== 'undefined')`.\n" + + "- Variable input such as `Date.now()` or `Math.random()` which changes each time it's called.\n" + + "- Date formatting in a user's locale which doesn't match the server.\n" + + '- External changing data without sending a snapshot of it along with the HTML.\n' + + '- Invalid HTML tag nesting.\n\n' + + 'It can also happen if the client has a browser extension installed which messes with the HTML before React loaded.\n\n' + + 'https://react.dev/link/hydration-mismatch\n\n' + + ' \n' + + ' \n' + + ' \n' + + ' \n' + + '\n in input (at **)' + + '\n in App (at **)', + ]); await act(async () => { root.render(); }); diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js index 5b44d9e02e..7542582528 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js @@ -10233,7 +10233,7 @@ describe('ReactDOMFizzServer', () => { '\n+ client' + '\n- server' + '\n' + - '\n in Suspense (at **)' + + '\n in meta (at **)' + '\n in ClientApp (at **)', ]); } diff --git a/packages/react-dom/src/__tests__/ReactDOMHydrationDiff-test.js b/packages/react-dom/src/__tests__/ReactDOMHydrationDiff-test.js index 2efcfd01d6..c445f458e5 100644 --- a/packages/react-dom/src/__tests__/ReactDOMHydrationDiff-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMHydrationDiff-test.js @@ -23,6 +23,7 @@ function errorHandler() { describe('ReactDOMServerHydration', () => { let container; + let ownerStacks; beforeEach(() => { jest.resetModules(); @@ -32,7 +33,15 @@ describe('ReactDOMServerHydration', () => { act = React.act; window.addEventListener('error', errorHandler); - console.error = jest.fn(); + ownerStacks = []; + console.error = jest.fn(() => { + const ownerStack = React.captureOwnerStack(); + if (typeof ownerStack === 'string') { + ownerStacks.push(ownerStack === '' ? ' ' : ownerStack); + } else { + ownerStacks.push(' ' + String(ownerStack)); + } + }); container = document.createElement('div'); document.body.appendChild(container); }); @@ -44,15 +53,25 @@ describe('ReactDOMServerHydration', () => { }); function normalizeCodeLocInfo(str) { - return ( - typeof str === 'string' && - str.replace(/\n +(?:at|in) ([\S]+)[^\n]*/g, function (m, name) { - return '\n in ' + name + ' (at **)'; - }) - ); + return typeof str === 'string' + ? str.replace(/\n +(?:at|in) ([\S]+)[^\n]*/g, function (m, name) { + return '\n in ' + name + ' (at **)'; + }) + : str; } - function formatMessage(args) { + function formatMessage(args, index) { + const ownerStack = ownerStacks[index]; + + if (ownerStack === undefined) { + throw new Error( + 'Expected an owner stack for message ' + + index + + ':\n' + + util.format(...args), + ); + } + const [format, ...rest] = args; if (format instanceof Error) { if (format.cause instanceof Error) { @@ -61,13 +80,23 @@ describe('ReactDOMServerHydration', () => { format.message + ']\n Cause [' + format.cause.message + - ']' + ']\n Owner Stack:' + + normalizeCodeLocInfo(ownerStack) ); } - return 'Caught [' + format.message + ']'; + return ( + 'Caught [' + + format.message + + ']\n Owner Stack:' + + normalizeCodeLocInfo(ownerStack) + ); } rest[rest.length - 1] = normalizeCodeLocInfo(rest[rest.length - 1]); - return util.format(format, ...rest); + return ( + util.format(format, ...rest) + + '\n Owner Stack:' + + normalizeCodeLocInfo(ownerStack) + ); } function formatConsoleErrors() { @@ -115,7 +144,10 @@ describe('ReactDOMServerHydration', () => {
+ client - server - ]", + ] + Owner Stack: + in main (at **) + in Mismatch (at **)", ] `); } else { @@ -138,7 +170,10 @@ describe('ReactDOMServerHydration', () => {
+ client - server - ", + + Owner Stack: + in main (at **) + in Mismatch (at **)", ] `); } @@ -177,7 +212,10 @@ describe('ReactDOMServerHydration', () => {
+ This markup contains an nbsp entity:   client text - This markup contains an nbsp entity:   server text - ]", + ] + Owner Stack: + in div (at **) + in Mismatch (at **)", ] `); } else { @@ -199,7 +237,10 @@ describe('ReactDOMServerHydration', () => {
+ This markup contains an nbsp entity:   client text - This markup contains an nbsp entity:   server text - ", + + Owner Stack: + in div (at **) + in Mismatch (at **)", ] `); } @@ -245,7 +286,10 @@ describe('ReactDOMServerHydration', () => { - __html: "server" }} > - ", + + Owner Stack: + in main (at **) + in Mismatch (at **)", ] `); }); @@ -286,7 +330,10 @@ describe('ReactDOMServerHydration', () => { + dir="ltr" - dir="rtl" > - ", + + Owner Stack: + in main (at **) + in Mismatch (at **)", ] `); }); @@ -327,7 +374,10 @@ describe('ReactDOMServerHydration', () => { + dir="ltr" - dir={null} > - ", + + Owner Stack: + in main (at **) + in Mismatch (at **)", ] `); }); @@ -368,7 +418,10 @@ describe('ReactDOMServerHydration', () => { + dir={null} - dir="rtl" > - ", + + Owner Stack: + in main (at **) + in Mismatch (at **)", ] `); }); @@ -409,7 +462,10 @@ describe('ReactDOMServerHydration', () => { + dir={null} - dir="rtl" > - ", + + Owner Stack: + in main (at **) + in Mismatch (at **)", ] `); }); @@ -449,7 +505,78 @@ describe('ReactDOMServerHydration', () => { + style={{opacity:1}} - style={{opacity:"0"}} > - ", + + Owner Stack: + in main (at **) + in Mismatch (at **)", + ] + `); + }); + + // @gate __DEV__ + it('picks the DFS-first Fiber as the error Owner', () => { + function LeftMismatch({isClient}) { + return
; + } + + function LeftIndirection({isClient}) { + return ; + } + + function MiddleMismatch({isClient}) { + return ; + } + + function RightMisMatch({isClient}) { + return

; + } + + function App({isClient}) { + return ( + <> + + + + + ); + } + expect(testMismatch(App)).toMatchInlineSnapshot(` + [ + "A tree hydrated but some attributes of the server rendered HTML didn't match the client properties. This won't be patched up. This can happen if a SSR-ed Client Component used: + + - A server/client branch \`if (typeof window !== 'undefined')\`. + - Variable input such as \`Date.now()\` or \`Math.random()\` which changes each time it's called. + - Date formatting in a user's locale which doesn't match the server. + - External changing data without sending a snapshot of it along with the HTML. + - Invalid HTML tag nesting. + + It can also happen if the client has a browser extension installed which messes with the HTML before React loaded. + + https://react.dev/link/hydration-mismatch + + + + +

+ + + +

+ + Owner Stack: + in div (at **) + in LeftMismatch (at **) + in LeftIndirection (at **) + in App (at **)", ] `); }); @@ -483,7 +610,10 @@ describe('ReactDOMServerHydration', () => {

+
- ]", + ] + Owner Stack: + in main (at **) + in Mismatch (at **)", ] `); }); @@ -518,7 +648,10 @@ describe('ReactDOMServerHydration', () => { +
-
... - ]", + ] + Owner Stack: + in header (at **) + in Mismatch (at **)", ] `); }); @@ -554,7 +687,10 @@ describe('ReactDOMServerHydration', () => { +
-