diff --git a/.changelog/5971-fix-react-19-follow-ups.md b/.changelog/5971-fix-react-19-follow-ups.md new file mode 100644 index 0000000000..f81937d90d --- /dev/null +++ b/.changelog/5971-fix-react-19-follow-ups.md @@ -0,0 +1,3 @@ +### Fixed + +- Fixed the IIFE build of the Fluent theme crashing on load with React 19, in PR [#5971](https://github.com/microsoft/BotFramework-WebChat/pull/5971), by [@OEvgeny](https://github.com/OEvgeny) diff --git a/__tests__/html2/react19/codeBlockCopyButton.html b/__tests__/html2/react19/codeBlockCopyButton.html new file mode 100644 index 0000000000..44caa881a2 --- /dev/null +++ b/__tests__/html2/react19/codeBlockCopyButton.html @@ -0,0 +1,92 @@ + + + + React 19: renders the copy button of code blocks + + + + + + + + +
+ + + diff --git a/__tests__/html2/react19/codeBlockCopyButton.html.snap-1.png b/__tests__/html2/react19/codeBlockCopyButton.html.snap-1.png new file mode 100644 index 0000000000..f931af3deb Binary files /dev/null and b/__tests__/html2/react19/codeBlockCopyButton.html.snap-1.png differ diff --git a/__tests__/html2/react19/fluentTheme.iife.html b/__tests__/html2/react19/fluentTheme.iife.html new file mode 100644 index 0000000000..d94578f86f --- /dev/null +++ b/__tests__/html2/react19/fluentTheme.iife.html @@ -0,0 +1,80 @@ + + + + React 19: the IIFE build of the Fluent theme sends a message and shows the reply + + + + + + + + + +
+ + + diff --git a/__tests__/html2/react19/fluentTheme.iife.html.snap-1.png b/__tests__/html2/react19/fluentTheme.iife.html.snap-1.png new file mode 100644 index 0000000000..ca3f67cbf3 Binary files /dev/null and b/__tests__/html2/react19/fluentTheme.iife.html.snap-1.png differ diff --git a/__tests__/html2/react19/renderWebChat.esm.html b/__tests__/html2/react19/renderWebChat.esm.html index ac79ff7a4d..708d95f68c 100644 --- a/__tests__/html2/react19/renderWebChat.esm.html +++ b/__tests__/html2/react19/renderWebChat.esm.html @@ -8,7 +8,7 @@
diff --git a/packages/bundle/src/renderWebChat.spec.tsx b/packages/bundle/src/renderWebChat.spec.tsx index 1ca156f321..b569dfcada 100644 --- a/packages/bundle/src/renderWebChat.spec.tsx +++ b/packages/bundle/src/renderWebChat.spec.tsx @@ -44,6 +44,18 @@ test('Root should be rendered by root.render() instead of ReactDOM.render()', () expect(render).not.toHaveBeenCalled(); }); +test('Error from root.render() should be thrown synchronously to the host', () => { + // React throws this when rendering with a root after `root.unmount()`. + const root = { + render: jest.fn(() => { + throw new Error('Cannot update an unmounted root.'); + }) + }; + + expect(() => renderWebChat(ReactWebChat, {}, root)).toThrow('Cannot update an unmounted root.'); + expect(root.render).toHaveBeenCalledTimes(1); +}); + test('Element on React 19 should throw an error asking for a root', () => { // React 19 removed `ReactDOM.render()`. Object.assign(ReactDOM, { render: undefined }); @@ -55,5 +67,5 @@ test('Element on React 19 should throw an error asking for a root', () => { test('Neither a container nor a root should throw', () => expect(() => renderWebChat(ReactWebChat, {}, {} as unknown as HTMLElement)).toThrow( - 'renderWebChat() expects an HTMLElement or a root from createRoot().' + 'renderWebChat() expects an HTMLElement or a root from ReactDOM.createRoot().' )); diff --git a/packages/bundle/src/renderWebChat.tsx b/packages/bundle/src/renderWebChat.tsx index d1e9230573..9f12077a9d 100644 --- a/packages/bundle/src/renderWebChat.tsx +++ b/packages/bundle/src/renderWebChat.tsx @@ -1,6 +1,7 @@ +import { reactNode } from '@msinternal/botframework-webchat-react-valibot'; import React, { type ComponentType, type ReactNode } from 'react'; import ReactDOM from 'react-dom'; -import { custom, function_, is, object, picklist } from 'valibot'; +import { args, custom, function_, is, object, picklist, pipe, tuple } from 'valibot'; /** A root from `createRoot()` of "react-dom/client". */ type RenderWebChatRoot = { render(children: ReactNode): void }; @@ -26,13 +27,16 @@ const DOCUMENT_NODE = 9; const DOCUMENT_FRAGMENT_NODE = 11; // Containers `ReactDOM.render()` accepts: element, document and document fragment, by `nodeType` to work across realms. -// Checked before `renderSchema`: some custom elements (e.g. Lit) have their own `render()` method. +// Checked before `reactRootSchema`: some custom elements (e.g. Lit) have their own `render()` method. const containerSchema = custom>(value => is(object({ nodeType: picklist([DOCUMENT_FRAGMENT_NODE, DOCUMENT_NODE, ELEMENT_NODE]) }), value) ); -// Both `ReactDOM.render()` (React 18 or earlier) and `root.render()`. -const renderSchema = object({ render: function_() }); +// `ReactDOM.render(children, container)`, React 18 or earlier. +const legacyReactDOMSchema = object({ render: pipe(function_(), args(tuple([reactNode(), containerSchema]))) }); + +// `root.render(children)`, root from `createRoot()` of "react-dom/client". +const reactRootSchema = object({ render: pipe(function_(), args(tuple([reactNode()]))) }); export default function renderWebChat( ReactWebChat: ComponentType, @@ -43,7 +47,7 @@ export default function renderWebChat( if (is(containerSchema, elementOrRoot)) { // React 19 or later removed `ReactDOM.render()`: Web Chat does not import "react-dom/client", the host creates the root. - if (!is(renderSchema, ReactDOM)) { + if (!is(legacyReactDOMSchema, ReactDOM)) { throw new Error( 'botframework-webchat: renderWebChat() on React 19 or later needs a root: pass createRoot(element) from "react-dom/client" instead of the element.' ); @@ -51,11 +55,13 @@ export default function renderWebChat( // React 16 to 18, and Preact. Also the IIFE build, which bundles React DOM 16.8.6 unless `window.ReactDOM` is set. ReactDOM.render(children, elementOrRoot); - } else if (is(renderSchema, elementOrRoot)) { + } else if (is(reactRootSchema, elementOrRoot)) { // The host created the root: render with it on any React version, it is the host's to unmount. elementOrRoot.render(children); } else { - throw new Error('botframework-webchat: renderWebChat() expects an HTMLElement or a root from createRoot().'); + throw new Error( + 'botframework-webchat: renderWebChat() expects an HTMLElement or a root from ReactDOM.createRoot().' + ); } } diff --git a/packages/fluent-theme/src/external.umd/react-dom.ts b/packages/fluent-theme/src/external.umd/react-dom.ts new file mode 100644 index 0000000000..ea199e65a0 --- /dev/null +++ b/packages/fluent-theme/src/external.umd/react-dom.ts @@ -0,0 +1,4 @@ +/// + +// The React DOM of the page, as with "react": a bundled React DOM cannot initialize with another React, such as React 19. +module.exports = (globalThis as any).ReactDOM; diff --git a/packages/fluent-theme/tsup.config.ts b/packages/fluent-theme/tsup.config.ts index 291ddd36e2..d665a89a7b 100644 --- a/packages/fluent-theme/tsup.config.ts +++ b/packages/fluent-theme/tsup.config.ts @@ -16,6 +16,12 @@ const umdResolvePlugin = { path: join(fileURLToPath(import.meta.url), '../src/external.umd/react.ts') })); + // ESBuild use Go regular expressions and does not understand Unicode flag. + // eslint-disable-next-line require-unicode-regexp + build.onResolve({ filter: /^react-dom$/ }, () => ({ + path: join(fileURLToPath(import.meta.url), '../src/external.umd/react-dom.ts') + })); + // ESBuild use Go regular expressions and does not understand Unicode flag. // eslint-disable-next-line require-unicode-regexp build.onResolve({ filter: /^botframework-webchat-api$/ }, () => ({